SH2SC-EDT
Self-Healing Hardware and Software Complex for Encrypted Data Transmission
Loading...
Searching...
No Matches
TransmitterNode.ino
Go to the documentation of this file.
1
20#include "transmitter.h"
21#include "melody.h"
22#include <LiquidCrystal_AIP31068_I2C.h>
23
24
26static LiquidCrystal_AIP31068_I2C lcd(TX_LCD_ADDR, TX_LCD_COLS, TX_LCD_ROWS);
27
29static uint16_t melodyIndex = 0;
30static uint16_t seqNum = 0;
31static uint8_t retryCount = 0;
32static uint32_t ackWaitStart = 0;
33
35static uint32_t lastReconnectAttempt = 0;
36
48
50static ChaChaPoly s_cipher;
51
53static uint8_t pendingNoteIndex = 0;
55static uint16_t pendingNoteDuration = 0;
56
58static uint32_t noteWaitStart = 0;
59
60// Debounce state (millis-based). INPUT_PULLUP wiring: idle = HIGH, pressed = LOW.
61static bool btnLastRawState = HIGH;
62static bool btnStableState = HIGH;
63static uint32_t btnLastChangeMs = 0;
64
72 // Cast to bool: LOW = 0 = false, HIGH = 1 = true (INPUT_PULLUP logic).
73 const bool rawReading = (digitalRead(TX_BUTTON_PIN) != LOW);
74
75 // Any change in the raw signal resets the stability timer.
76 if (rawReading != btnLastRawState) {
77 btnLastChangeMs = millis();
78 btnLastRawState = rawReading;
79 }
80
81 // Promote the raw reading to the stable state only after DEBOUNCE_DELAY_MS
82 // of continuous stability — this rejects contact-bounce glitches.
83 if ((millis() - btnLastChangeMs) >= DEBOUNCE_DELAY_MS) {
84 if (rawReading != btnStableState) {
85 btnStableState = rawReading;
86 // Falling edge on INPUT_PULLUP line = operator pressed the button.
87 if (btnStableState == false) {
88 return true;
89 }
90 }
91 }
92
93 return false;
94}
95
104static inline void drainRxFifo() {
105 while (Serial.available() > 0) {
106 Serial.read();
107 }
108}
109
119 // Fill s_sessionNonce with 12 CSPRNG bytes (3 × 32-bit words).
120 for (uint8_t i = 0; i < HELLO_NONCE_SIZE; i += 4u) {
121 const uint32_t word = getSecureRandom32();
122 s_sessionNonce[i + 0u] = static_cast<uint8_t>(word);
123 s_sessionNonce[i + 1u] = static_cast<uint8_t>(word >> 8u);
124 s_sessionNonce[i + 2u] = static_cast<uint8_t>(word >> 16u);
125 s_sessionNonce[i + 3u] = static_cast<uint8_t>(word >> 24u);
126 }
127
128 HelloPacket hello;
129 hello.flags = FLAG_SYN;
130 memcpy(hello.nonce, s_sessionNonce, HELLO_NONCE_SIZE);
131
132 // Drain stale responses before transmitting; see drainRxFifo() comment.
133 drainRxFifo();
134 // Prefix every packet (both HELLO and DATA) with the sync preamble so the
135 // RX parser always requires 0xAA 0x55 before accepting any frame type.
136 // Without this, a noise-generated 0x01 byte could hijack the session nonce.
137 Serial.write(SYNC_BYTE_1);
138 Serial.write(SYNC_BYTE_2);
139 Serial.write(reinterpret_cast<const uint8_t*>(&hello), sizeof(HelloPacket));
140}
141
142void sendPacket(uint8_t noteIndex, uint16_t noteDurationMs, uint16_t seqNumber) {
143 // Step 1 — Derive per-packet IV using the full 16-bit sequence number.
144 // Spreading seqNumber across bytes[10..11] of the 12-byte nonce ensures
145 // all 65536 possible sequence numbers produce a distinct IV.
146 uint8_t packetNonce[HELLO_NONCE_SIZE];
147 memcpy(packetNonce, s_sessionNonce, HELLO_NONCE_SIZE);
148 packetNonce[10] ^= static_cast<uint8_t>((seqNumber >> 8u) & 0xFFu);
149 packetNonce[11] ^= static_cast<uint8_t>(seqNumber & 0xFFu);
150
151 // Step 2 — Populate packet header (used also as the 3-byte AAD).
152 DataPacket pkt;
153 pkt.flags = FLAG_DAT;
154 pkt.seq_num = seqNumber;
155
156 // Step 3 — AAD is the 3 open header bytes: flags(1) + seq_num(2).
157 // Any tampering with these fields causes MAC verification to fail.
158 // We feed the raw struct bytes so the byte order matches what the receiver
159 // will see on the wire (little-endian seq_num on AVR).
160 const uint8_t aad[3] = {
161 FLAG_DAT,
162 static_cast<uint8_t>(seqNumber & 0xFFu), // seq_num low byte
163 static_cast<uint8_t>((seqNumber >> 8u) & 0xFFu) // seq_num high byte
164 };
165
166 // Step 4 — Build 4-byte plain-text payload.
167 // payload[0] = note_index as uint16_t (values 0–20 or REST_INDEX=255)
168 // payload[1] = duration in ms as uint16_t (direct, no DURATION_UNIT_MS encoding)
169 const uint16_t plaintext[2] = {
170 static_cast<uint16_t>(noteIndex),
171 noteDurationMs
172 };
173
174 // Step 5 — Run ChaCha20-Poly1305.
175 s_cipher.clear();
176 s_cipher.setKey(MASTER_PSK, 32u);
177 s_cipher.setIV(packetNonce, HELLO_NONCE_SIZE);
178 s_cipher.addAuthData(aad, sizeof(aad));
179 s_cipher.encrypt(
180 reinterpret_cast<uint8_t*>(pkt.payload),
181 reinterpret_cast<const uint8_t*>(plaintext),
182 DATA_PAYLOAD_SIZE // 4 bytes
183 );
184
185 // Step 6 — Truncated MAC: compute full 16-byte Poly1305 tag, transmit only
186 // the first TRUNCATED_MAC_SIZE (8) bytes. This halves MAC overhead while
187 // still providing 64-bit authentication strength — sufficient for a
188 // noise-resilience demo over a short-range UART link.
189 uint8_t full_mac[16];
190 s_cipher.computeTag(full_mac, 16u);
191 memcpy(pkt.mac, full_mac, TRUNCATED_MAC_SIZE);
192
193 // Step 7 — Transmit: drain FIFO, then 2 sync bytes + 15-byte DataPacket = 17 bytes.
194 // The sync preamble lets the receiver re-lock onto the frame boundary
195 // after a noise burst without waiting for a new HelloPacket.
196 drainRxFifo();
197 Serial.write(SYNC_BYTE_1);
198 Serial.write(SYNC_BYTE_2);
199 Serial.write(reinterpret_cast<const uint8_t*>(&pkt), sizeof(DataPacket));
200}
201
209void formAndSendPacket(uint8_t note_idx, uint16_t duration_ms) {
210 // Snapshot the plain-text payload so the FSM can retransmit on NACK
211 // without re-reading the melody arrays.
212 pendingNoteIndex = note_idx;
213 pendingNoteDuration = duration_ms;
214
215 sendPacket(note_idx, duration_ms, seqNum);
216}
217
228void updateTxDisplay(TxState state, uint16_t seqNumber, uint8_t retries) {
229 lcd.clear();
230
231 // Row 0: packet number + retry counter
232 // Example: "PKT:5 RTY:2" — gives operator live ARQ visibility.
233 lcd.setCursor(0, 0);
234 lcd.print("PKT:");
235 lcd.print(seqNumber);
236 lcd.print(" RTY:");
237 lcd.print(retries);
238
239 // Row 1: FSM state label
240 lcd.setCursor(0, 1);
241 switch (state) {
242 case TxState::IDLE: lcd.print("IDLE"); break;
243 case TxState::RECONNECTING: lcd.print("RECONNECTING"); break;
244 case TxState::SENDING_HELLO: lcd.print("SEND SYN"); break;
245 case TxState::WAITING_HELLO_ACK: lcd.print("WAIT SYN"); break;
246 case TxState::SENDING: lcd.print("SENDING"); break;
247 case TxState::WAITING_ACK: lcd.print("WAIT ACK"); break;
248 case TxState::WAIT_BETWEEN_NOTES: lcd.print("WAIT NOTE"); break;
249 case TxState::SENDING_FIN: lcd.print("SEND FIN"); break;
250 case TxState::WAITING_FIN_ACK: lcd.print("WAIT FIN"); break;
251 }
252}
253
267 // Step 1 — Derive per-packet nonce (identical derivation to sendPacket()).
268 uint8_t packetNonce[HELLO_NONCE_SIZE];
269 memcpy(packetNonce, s_sessionNonce, HELLO_NONCE_SIZE);
270 packetNonce[10] ^= static_cast<uint8_t>((seqNum >> 8u) & 0xFFu);
271 packetNonce[11] ^= static_cast<uint8_t>(seqNum & 0xFFu);
272
273 // Step 2 — Build packet header.
274 DataPacket finPkt;
275 finPkt.flags = FLAG_FIN;
276 finPkt.seq_num = seqNum;
277 finPkt.payload[0] = 0;
278 finPkt.payload[1] = 0;
279
280 // Step 3 — 3-byte AAD: FLAG_FIN(1) + seq_num(2).
281 // Binding the flags byte to the MAC prevents any node from flipping
282 // FLAG_DAT into FLAG_FIN mid-stream without MAC failure on the other side.
283 const uint8_t aad[3] = {
284 FLAG_FIN,
285 static_cast<uint8_t>(seqNum & 0xFFu),
286 static_cast<uint8_t>((seqNum >> 8u) & 0xFFu)
287 };
288
289 // Step 4 — Encrypt zero payload so ciphertext is indistinguishable from data.
290 const uint16_t plaintext[2] = {0u, 0u};
291 s_cipher.clear();
292 s_cipher.setKey(MASTER_PSK, 32u);
293 s_cipher.setIV(packetNonce, HELLO_NONCE_SIZE);
294 s_cipher.addAuthData(aad, sizeof(aad));
295 s_cipher.encrypt(
296 reinterpret_cast<uint8_t*>(finPkt.payload),
297 reinterpret_cast<const uint8_t*>(plaintext),
299 );
300
301 // Step 5 — Truncated MAC (8 bytes of 16-byte Poly1305 tag).
302 uint8_t full_mac[16];
303 s_cipher.computeTag(full_mac, 16u);
304 memcpy(finPkt.mac, full_mac, TRUNCATED_MAC_SIZE);
305
306 // Step 6 — Transmit: drain FIFO, then 2 sync bytes + 15-byte DataPacket = 17 bytes.
307 drainRxFifo();
308 Serial.write(SYNC_BYTE_1);
309 Serial.write(SYNC_BYTE_2);
310 Serial.write(reinterpret_cast<const uint8_t*>(&finPkt), sizeof(DataPacket));
311}
312
315static volatile uint32_t s_ringOscPulses = 0;
316
318static void onRingOscPulse() { ++s_ringOscPulses; }
319
329static inline uint32_t mixEntropy(uint32_t pool, uint32_t bits) {
330 return ((pool << 1u) | (pool >> 31u)) ^ bits;
331}
332
345void generateEntropyPool(uint8_t* outputSeed) {
346 // SRAM base: 64 uninitialised bytes starting at 0x0100 on ATmega328P.
347 // Each word consumes a distinct 8-byte slice so the slices never repeat.
348 const uint8_t* sramBase = reinterpret_cast<const uint8_t*>(0x0100);
349
350 for (uint8_t wordIndex = 0; wordIndex < 8u; ++wordIndex) {
351 uint32_t pool = 0;
352
353 // Source 1: Ring oscillator gate (pin 2, INT0) — fresh 2 ms window per word.
354 s_ringOscPulses = 0;
355 attachInterrupt(digitalPinToInterrupt(ENTROPY_RING_OSC_PIN),
356 onRingOscPulse, RISING);
357 delay(2);
358 detachInterrupt(digitalPinToInterrupt(ENTROPY_RING_OSC_PIN));
359 pool = mixEntropy(pool, s_ringOscPulses);
360
361 // Source 2: SRAM chaos — 8 unique bytes per word (slice: wordIndex*8 .. +7).
362 for (uint8_t i = 0; i < 8u; ++i) {
363 pool = mixEntropy(pool, sramBase[wordIndex * 8u + i]);
364 }
365
366 // Source 3: On-die temperature ADC (channel 8, 1.1 V ref) — 8 LSBs per word.
367 {
368 const uint8_t savedAdmux = ADMUX;
369 ADMUX = _BV(REFS1) | _BV(REFS0) | _BV(MUX3); // 0xC8
370 ADCSRA |= _BV(ADEN);
371 // Discard first conversion after reference change
372 ADCSRA |= _BV(ADSC); while (ADCSRA & _BV(ADSC)) {}
373 uint8_t adcEntropy = 0;
374 for (uint8_t i = 0; i < 8u; ++i) {
375 ADCSRA |= _BV(ADSC); while (ADCSRA & _BV(ADSC)) {}
376 adcEntropy = static_cast<uint8_t>((adcEntropy << 1u) | (ADCL & 0x01u));
377 }
378 pool = mixEntropy(pool, adcEntropy);
379 ADMUX = savedAdmux;
380 }
381
382 // Source 4: TCNT1 timer jitter — unique value at each iteration boundary.
383 pool = mixEntropy(pool, static_cast<uint32_t>(TCNT1));
384
385 // Source 5: A0 white noise generator — 8 LSBs per word.
386 {
387 uint8_t a0Entropy = 0;
388 for (uint8_t i = 0; i < 8u; ++i) {
389 a0Entropy = static_cast<uint8_t>(
390 (a0Entropy << 1u) | (static_cast<uint8_t>(analogRead(A0)) & 0x01u)
391 );
392 }
393 pool = mixEntropy(pool, a0Entropy);
394 }
395
396 // Source 6: Human-timing jitter (TX only) — micros() drifts between iterations.
397 pool = mixEntropy(pool, micros());
398
399 // Source 7: Arduino software PRNG (obfuscation layer).
400 pool = mixEntropy(pool, static_cast<uint32_t>(random()));
401
402 // Write the 32-bit word into the output seed as 4 bytes (little-endian).
403 outputSeed[wordIndex * 4u + 0u] = static_cast<uint8_t>(pool);
404 outputSeed[wordIndex * 4u + 1u] = static_cast<uint8_t>(pool >> 8u);
405 outputSeed[wordIndex * 4u + 2u] = static_cast<uint8_t>(pool >> 16u);
406 outputSeed[wordIndex * 4u + 3u] = static_cast<uint8_t>(pool >> 24u);
407 }
408}
409
427 lcd.clear();
428 lcd.setCursor(0, 0);
429 lcd.print("LINK LOST!");
430 lcd.setCursor(0, 1);
431 lcd.print("RECONNECTING...");
432
433 // Erase key material — the old nonce is no longer safe to use.
435 // melodyIndex intentionally NOT reset — resume from point of failure.
436 seqNum = 0;
437 retryCount = 0;
439
440 // Pre-arm the timer so the first auto-ping waits a full interval.
441 lastReconnectAttempt = millis();
442
443 delay(2000); // Hold error message so the operator can read it.
444}
445
454void tx_setup() {
455 // UART: 9600 8N1 — matches protocol specification and SimulIDE oscilloscope.
456 Serial.begin(BAUD_RATE);
457
458 // Button: internal pull-up keeps the line HIGH until the button pulls it LOW.
459 pinMode(TX_BUTTON_PIN, INPUT_PULLUP);
460
461 // I2C LCD (Aip31068 compatible, address 0x27).
462 lcd.init();
463 // lcd.backlight();
464
465 // Harvest 256-bit hardware entropy and seed the ChaCha20 CSPRNG.
466 // ~16 ms total (8 ring-oscillator gate windows of 2 ms each) — one-time cost.
467 // The seed is scrubbed from the stack immediately after handing it to the cipher.
468 {
469 uint8_t entropySeed[32];
470 generateEntropyPool(entropySeed);
471 initCSPRNG(entropySeed);
472 memset(entropySeed, 0, sizeof(entropySeed));
473 }
474
477}
478
486void tx_loop() {
487 // readButtonPress() must run every iteration so the debounce timer
488 // keeps accumulating even when the FSM is not in IDLE.
489 const bool buttonPressed = readButtonPress();
490
491 switch (currentState) {
492
493 case TxState::IDLE:
494 // Wait for the operator to press the start button before transmitting.
495 if (buttonPressed) {
496 melodyIndex = 0;
497 seqNum = 0;
498 retryCount = 0; // Fresh start — reset the retry display counter.
501 }
502 break;
503
505 // Generate a fresh CSPRNG nonce and broadcast it inside a SYN frame.
506 // drainRxFifo() runs inside sendHelloPacket(), so any NACK bytes that
507 // accumulated during a pre-session noise burst are purged first.
509 ackWaitStart = millis(); // Open the ACK receive window.
512 break;
513
515 if (Serial.available() > 0) {
516 const uint8_t response = static_cast<uint8_t>(Serial.read());
517
518 if (response == ACK_BYTE) {
519 // RX confirmed the nonce — session is live, start sending melody.
520 retryCount = 0;
523 } else {
524 // NACK or noise: RX rejected the SYN (parser desync or burst).
525 // Brief cooldown before retransmitting a fresh HelloPacket so the
526 // RX UART FIFO has time to drain, and a new nonce is generated to
527 // keep the replay window always moving forward.
528 retryCount++;
529 if (retryCount >= MAX_RETRIES) {
531 } else {
532 delay(10);
535 }
536 }
537
538 } else if ((millis() - ackWaitStart) >= ACK_TIMEOUT_MS) {
539 // Timeout: HELLO was lost or RX FIFO was overwhelmed — retransmit.
540 retryCount++;
541 if (retryCount >= MAX_RETRIES) {
543 } else {
546 }
547 }
548 break;
549
550 case TxState::SENDING:
551 if (melodyIndex >= MELODY_LENGTH) {
552 // All notes delivered — initiate session teardown instead of going idle.
553 // This guard is a safety net; in normal flow WAIT_BETWEEN_NOTES detects
554 // melody completion and transitions to SENDING_FIN directly.
557 break;
558 }
559 // Build, encrypt, and transmit the current note.
560 // seqNum is NOT yet incremented — it advances only on ACK so that
561 // every retransmission of the same note reuses the same key.
563 static_cast<uint8_t>(pgm_read_word(&melody[melodyIndex][0])),
564 pgm_read_word(&melody[melodyIndex][1])
565 );
566 ackWaitStart = millis(); // Open the ACK receive window (50 ms).
569 break;
570
572 if (Serial.available() > 0) {
573 const uint8_t response = static_cast<uint8_t>(Serial.read());
574
575 if (response == ACK_BYTE) {
576 // ACK: packet intact -> enter the inter-note pause before advancing.
577 // melodyIndex is NOT incremented here — WAIT_BETWEEN_NOTES still
578 // needs pgm_read_word(&melody[melodyIndex][1]) to determine how long to pause.
579 retryCount = 0;
580 noteWaitStart = millis();
583
584 } else if (response == NACK_BYTE) {
585 // NACK: RX detected corruption -> wait briefly then retransmit the SAME packet.
586 // delay(10) gives the RX UART buffer time to drain residual noise bytes
587 // before the retransmission arrives, reducing cascading NACK storms.
588 retryCount++;
589 if (retryCount >= MAX_RETRIES) {
591 } else {
592 delay(10);
594 ackWaitStart = millis();
596 }
597 }
598 // Any other byte (noise on the feedback line) is silently ignored.
599
600 } else if ((millis() - ackWaitStart) >= ACK_TIMEOUT_MS) {
601 // Timeout: no response within 50 ms -> channel or ACK was lost.
602 // Retransmit the SAME packet with the SAME seqNum.
603 retryCount++;
604 if (retryCount >= MAX_RETRIES) {
606 } else {
608 ackWaitStart = millis();
610 }
611 }
612 break;
613
615 // Hold for the duration of the just-acknowledged note/pause before
616 // sending the next packet. This preserves melody timing exactly,
617 // including long pauses that exceed a single uint8_t in milliseconds.
618 if ((millis() - noteWaitStart) >= pgm_read_word(&melody[melodyIndex][1])) {
619 melodyIndex++;
620 seqNum++; // Advance together with melodyIndex so keys stay in sync.
621 retryCount = 0;
622 // When the last note has been acknowledged, move to teardown rather
623 // than back to SENDING — the melody is complete.
624 if (melodyIndex >= MELODY_LENGTH) {
626 } else {
628 }
630 }
631 break;
632
634 // Transmit the FLAG_FIN teardown packet with the current seqNum.
635 // The packet is fully authenticated (ChaChaPoly), so RX can verify
636 // it is a genuine end-of-session signal and not injected noise.
637 retryCount = 0;
639 ackWaitStart = millis();
642 break;
643
645 if (Serial.available() > 0) {
646 const uint8_t response = static_cast<uint8_t>(Serial.read());
647
648 if (response == ACK_BYTE) {
649 // RX confirmed the FIN — session closed successfully (clean close).
650 // CRITICAL: erase the session nonce from RAM so it cannot be
651 // recovered by subsequent code or a reset-based side-channel.
653 melodyIndex = 0; // Clean close — restart melody from the beginning.
654 seqNum = 0;
655 retryCount = 0;
658
659 } else if (response == NACK_BYTE) {
660 // RX rejected the FIN (MAC failure) — retransmit after a brief drain.
661 retryCount++;
662 if (retryCount >= MAX_RETRIES) {
664 } else {
665 delay(10);
668 }
669 }
670 // Any other byte — noise on the feedback line, stay in WAITING_FIN_ACK.
671
672 } else if ((millis() - ackWaitStart) >= ACK_TIMEOUT_MS) {
673 // Timeout: ACK lost in transit — retransmit the FIN packet.
674 retryCount++;
675 if (retryCount >= MAX_RETRIES) {
677 } else {
680 }
681 }
682 break;
683
685 // Auto-Resume state: entered after suspendSession() when the link drops
686 // mid-melody. melodyIndex is preserved so we resume from where we left off.
687 //
688 // Button press = operator forces a hard restart from note 0.
689 // Auto-timer = silent HELLO ping every RECONNECT_INTERVAL_MS.
690 if (buttonPressed) {
691 // Manual override: discard progress, restart melody from the beginning.
692 melodyIndex = 0;
693 seqNum = 0;
694 retryCount = 0;
697 break;
698 }
699 if ((millis() - lastReconnectAttempt) >= RECONNECT_INTERVAL_MS) {
700 // Auto-ping: attempt a new HELLO handshake to resume the session.
701 // On success WAITING_HELLO_ACK -> SENDING will pick up at melodyIndex.
702 // On MAX_RETRIES suspendSession() re-enters RECONNECTING (keeps trying).
703 lastReconnectAttempt = millis();
706 }
707 break;
708 }
709}
710
712void setup() { tx_setup(); }
714void loop() { tx_loop(); }
uint32_t getSecureRandom32()
Return 32 bits of CSPRNG output (non-blocking).
Definition csprng.h:126
void initCSPRNG(const uint8_t *hardwareSeed)
Seed the ChaCha20 CSPRNG with 256 bits of hardware entropy.
Definition csprng.h:102
static uint8_t pendingNoteIndex
Snapshot of the last transmitted note index — allows retransmission without re-reading PROGMEM.
static volatile uint32_t s_ringOscPulses
Ring oscillator pulse counter — incremented by the INT0 ISR on pin 2.
static uint16_t seqNum
Packet sequence number (0–65535, wraps).
static void onRingOscPulse()
INT0 interrupt service routine — counts ring oscillator rising edges.
static uint16_t pendingNoteDuration
Snapshot of the last transmitted note duration (ms) — allows retransmission without re-reading PROGME...
void suspendSession()
Perform an unclean session teardown and enter the Self-Healing reconnect loop.
void updateTxDisplay(TxState state, uint16_t seqNumber, uint8_t retries)
Refresh the TX LCD with the current ARQ status and FSM state label.
static void drainRxFifo()
Discard all bytes currently waiting in the hardware UART RX FIFO.
static uint32_t noteWaitStart
millis() timestamp marking the start of the current WAIT_BETWEEN_NOTES pause.
static uint32_t mixEntropy(uint32_t pool, uint32_t bits)
One mixing step of the entropy accumulation sponge.
void setup()
Arduino sketch entry point — delegates to tx_setup().
static uint16_t melodyIndex
Current position in melody[][] (preserved across RECONNECTING).
void sendHelloPacket()
Generate a fresh 12-byte CSPRNG nonce and broadcast it as a FLAG_SYN HelloPacket.
void sendFinPacket()
Construct and transmit a FLAG_FIN session-teardown packet.
static uint32_t lastReconnectAttempt
Auto-reconnect countdown; a new HelloPacket is broadcast when (millis() - lastReconnectAttempt) >= RE...
static bool btnLastRawState
Raw digitalRead() result from the previous call.
static uint8_t retryCount
Consecutive retransmission counter (displayed on LCD).
static uint32_t ackWaitStart
millis() timestamp when the current ACK-wait window opened.
static uint8_t s_sessionNonce[HELLO_NONCE_SIZE]
96-bit session nonce generated once per button press by sendHelloPacket().
void generateEntropyPool(uint8_t *outputSeed)
Harvest 256 bits of hardware entropy and write them to outputSeed (TX variant).
static uint32_t btnLastChangeMs
millis() timestamp of the last raw state transition.
void tx_loop()
Execute one non-blocking C2P-ARQ FSM tick. Called repeatedly from loop().
static bool btnStableState
Debounce-confirmed stable button state.
static LiquidCrystal_AIP31068_I2C lcd(TX_LCD_ADDR, TX_LCD_COLS, TX_LCD_ROWS)
I2C LCD — 16 columns x 2 rows, Aip31068-compatible controller.
void tx_setup()
Initialise TX hardware and seed the CSPRNG. Called once from setup().
static ChaChaPoly s_cipher
ChaCha20-Poly1305 cipher instance — re-initialised per packet via clear().
bool readButtonPress()
Read and debounce the start button (millis-based falling-edge detector).
static TxState currentState
Active FSM state.
void formAndSendPacket(uint8_t note_idx, uint16_t duration_ms)
Snapshot the note payload and delegate to sendPacket() for encryption and transmission.
void loop()
Arduino sketch main loop — delegates to tx_loop() on every iteration.
void sendPacket(uint8_t noteIndex, uint16_t noteDurationMs, uint16_t seqNumber)
const uint32_t ACK_TIMEOUT_MS
Maximum milliseconds TX waits for an ACK before retransmitting.
Definition protocol.h:157
const uint8_t MAX_RETRIES
Retransmission limit per packet; exhausting this triggers suspendSession().
Definition protocol.h:158
const uint8_t FLAG_FIN
Session close — instructs RX to teardown the current session.
Definition protocol.h:47
const uint8_t FLAG_DAT
Data frame — DataPacket carrying an encrypted note (index + duration).
Definition protocol.h:46
const uint8_t FLAG_SYN
Session open — HelloPacket carrying the 12-byte CSPRNG nonce.
Definition protocol.h:45
const uint8_t HELLO_NONCE_SIZE
ChaCha20 IV length in bytes (IETF 96-bit nonce format).
Definition protocol.h:54
const uint8_t DATA_PAYLOAD_SIZE
Byte size of the encrypted payload in a DataPacket.
Definition protocol.h:63
const uint8_t TRUNCATED_MAC_SIZE
Bytes of Poly1305 MAC actually placed on the wire (first 8 of 16).
Definition protocol.h:56
const uint8_t MASTER_PSK[32]
256-bit (32-byte) Pre-Shared Master Key for ChaCha20-Poly1305 AEAD.
Definition protocol.h:27
const uint8_t NACK_BYTE
Negative acknowledgement — MAC mismatch; TX must retransmit.
Definition protocol.h:136
const uint8_t ACK_BYTE
Positive acknowledgement — MAC verified, note played (or FIN accepted).
Definition protocol.h:135
const uint8_t SYNC_BYTE_2
Second synchronisation byte (preamble marker).
Definition protocol.h:75
const uint8_t SYNC_BYTE_1
First synchronisation byte (preamble marker).
Definition protocol.h:74
const uint8_t TX_BUTTON_PIN
Tactile start button (INPUT_PULLUP, active LOW).
Definition transmitter.h:22
const uint8_t TX_LCD_COLS
Number of columns on the TX LCD.
Definition transmitter.h:25
const uint8_t ENTROPY_RING_OSC_PIN
Hardware ring oscillator entropy source (INT0).
Definition transmitter.h:23
const uint8_t TX_LCD_ROWS
Number of rows on the TX LCD.
Definition transmitter.h:26
const uint8_t TX_LCD_ADDR
I2C address of the Aip31068 16x2 LCD display.
Definition transmitter.h:24
const uint16_t DEBOUNCE_DELAY_MS
Definition transmitter.h:33
const uint32_t RECONNECT_INTERVAL_MS
Auto-reconnect HelloPacket broadcast interval (ms) while in RECONNECTING state.
Definition transmitter.h:35
const uint16_t BAUD_RATE
UART baud rate in bps (8N1). Chosen for clear bit-width visibility on SimulIDE oscilloscope.
Definition protocol.h:165
PROGMEM melody data for SH2SC-EDT — Transmitter Node A.
static const uint16_t MELODY_LENGTH
Total number of note/rest entries in the PROGMEM melody table.
Definition melody.h:30
static const uint16_t melody[MELODY_LENGTH][2]
PROGMEM melody table — note indices and durations for the Imperial March.
Definition melody.h:39
Authenticated data packet (FLAG_DAT) and session-close packet (FLAG_FIN).
Definition protocol.h:122
uint16_t payload[2]
ChaCha20-encrypted payload: [0] = note_index, [1] = duration_ms.
Definition protocol.h:125
uint8_t flags
Frame type — FLAG_DAT (0x02) for data; FLAG_FIN (0x04) for teardown. Part of plaintext AAD.
Definition protocol.h:123
uint16_t seq_num
16-bit packet sequence number (little-endian). Part of plaintext AAD.
Definition protocol.h:124
uint8_t mac[TRUNCATED_MAC_SIZE]
First 8 bytes of the Poly1305 authentication tag.
Definition protocol.h:126
Session-open handshake packet (FLAG_SYN).
Definition protocol.h:93
uint8_t nonce[HELLO_NONCE_SIZE]
96-bit session nonce for ChaChaPoly per-packet IV derivation.
Definition protocol.h:95
uint8_t flags
Frame type — must equal FLAG_SYN (0x01).
Definition protocol.h:94
SH2SC-EDT — Transmitter Node A ("The Conductor") public interface.
TxState
Finite State Machine states for the C2P-ARQ Transmitter (Node A).
Definition transmitter.h:72
@ SENDING_FIN
All notes delivered; transmitting the FLAG_FIN teardown packet.
@ WAITING_ACK
DataPacket sent; listening on the feedback line for ACK or NACK.
@ SENDING_HELLO
Generating a CSPRNG nonce and transmitting the SYN handshake packet.
@ IDLE
Waiting for a button press to begin melody playback.
@ SENDING
Constructing, encrypting, and transmitting the current DataPacket.
@ WAITING_HELLO_ACK
HelloPacket sent; awaiting RX nonce-acceptance ACK.
@ WAIT_BETWEEN_NOTES
ACK received; holding the inter-note gap before advancing melodyIndex.
@ RECONNECTING
Link lost mid-melody; broadcasting HelloPackets every RECONNECT_INTERVAL_MS.
@ WAITING_FIN_ACK
FIN packet sent; awaiting RX acknowledgement before erasing the session nonce.