SH2SC-EDT
Self-Healing Hardware and Software Complex for Encrypted Data Transmission
Loading...
Searching...
No Matches
ReceiverNode.ino
Go to the documentation of this file.
1
17#include "receiver.h"
18#include <LiquidCrystal_AIP31068_I2C.h>
19
33static const uint16_t universal_notes[NOTE_DICT_SIZE] = {
34 262, // 0 — C4
35 277, // 1 — C#4
36 294, // 2 — D4
37 311, // 3 — D#4
38 330, // 4 — E4
39 349, // 5 — F4
40 370, // 6 — F#4
41 392, // 7 — G4
42 415, // 8 — G#4
43 440, // 9 — A4
44 466, // 10 — A#4
45 494, // 11 — B4
46 523, // 12 — C5
47 554, // 13 — C#5
48 587, // 14 — D5
49 622, // 15 — D#5
50 659, // 16 — E5
51 698, // 17 — F5
52 740, // 18 — F#5
53 784, // 19 — G5
54 831 // 20 — G#5
55};
56
58static LiquidCrystal_AIP31068_I2C lcd(RX_LCD_ADDR, RX_LCD_COLS, RX_LCD_ROWS);
59
68
74static uint8_t rx_buffer[sizeof(DataPacket)];
76static uint8_t rx_index = 0;
78static uint8_t expected_length = 0;
79
81static bool s_sessionActive = false;
84
86static ChaChaPoly s_cipher;
87
89static uint32_t noteStartMs = 0;
91static uint16_t noteLengthMs = 0;
93static bool isPlayingNote = false;
94
96const uint16_t CHK_ERR_DISPLAY_MS = 500;
98static bool isShowingError = false;
100static uint32_t errorDisplayStart = 0;
101
107const uint16_t RX_PARSER_TIMEOUT_MS = 20;
109static uint32_t rxLastByteMs = 0;
110
116const uint32_t NETWORK_GRACE_PERIOD_MS = 3000;
123static uint32_t current_timeout_limit = 5000;
125static uint32_t last_valid_packet_time = 0;
126
129
138void updateRxDisplay(RxState state, uint16_t seqNum, bool macOk) {
139 lcd.clear();
140
141 // Row 0: FSM state label
142 lcd.setCursor(0, 0);
143 switch (state) {
144 case RxState::WAITING_SYNC_1: lcd.print("WAIT SYNC"); break;
145 case RxState::WAITING_SYNC_2: lcd.print("WAIT SYNC2"); break;
146 case RxState::WAITING_FOR_TYPE: lcd.print(s_sessionActive ? "READY" : "NO KEY"); break;
147 case RxState::READING_HELLO: lcd.print("HANDSHAKE"); break;
148 case RxState::READING_DATA: lcd.print("READING..."); break;
149 case RxState::GOT_HELLO: lcd.print("KEY SET"); break;
150 case RxState::GOT_DATA: lcd.print("VERIFYING"); break;
151 case RxState::EXECUTING_ACTION: lcd.print("PLAYING"); break;
152 }
153
154 // Row 1: sequence number — shown only after at least one packet has been processed
155 lcd.setCursor(0, 1);
156 if (state == RxState::EXECUTING_ACTION || state == RxState::GOT_DATA) {
157 lcd.print("SEQ:");
158 lcd.print(seqNum);
159 lcd.print(macOk ? " OK" : " MAC!");
160 }
161}
162
170static void resetParser() {
171 // Drain any garbage in the 64-byte hardware UART RX FIFO.
172 while (Serial.available() > 0) {
173 Serial.read();
174 }
175 rx_index = 0;
178}
179
188void processReceivedByte(uint8_t inByte) {
189 switch (parseState) {
190
191 // Preamble scan: ALL packet types (HELLO and DATA) are prefixed with 0xAA 0x55.
192 // Nothing can enter the body-collection path without passing this gate.
193
194 case WAIT_AA:
195 if (inByte == SYNC_BYTE_1) {
197 }
198 // Any other byte (including FLAG_SYN=0x01 without preamble) — channel noise, stay.
199 break;
200
201 case WAIT_55:
202 if (inByte == SYNC_BYTE_2) {
203 rx_index = 0;
205 } else if (inByte != SYNC_BYTE_1) {
206 // If a second 0xAA arrives stay in WAIT_55 (overlapping preambles);
207 // anything else means the preamble was corrupted — restart from WAIT_AA.
209 }
210 break;
211
212 // Packet-type discriminator: preamble confirmed.
213 // Bitwise-AND (not ==) for flag matching: tolerates noise-resilient multi-flag frames.
214
215 case READ_TYPE:
216 rx_buffer[0] = inByte;
217 rx_index = 1;
218 if (inByte & FLAG_SYN) {
219 // SYN frame — HelloPacket (13 bytes): flags(1) + nonce(12).
220 expected_length = static_cast<uint8_t>(sizeof(HelloPacket));
223 updateRxDisplay(currentState, 0, false);
224 } else if (inByte & FLAG_DAT) {
225 // DAT frame — DataPacket (15 bytes): flags(1) + seq_num(2) + payload(4) + mac(8).
226 expected_length = static_cast<uint8_t>(sizeof(DataPacket));
229 } else if (inByte & FLAG_FIN) {
230 // FIN frame — DataPacket-sized session-teardown frame.
231 expected_length = static_cast<uint8_t>(sizeof(DataPacket));
234 } else {
235 // No recognised flag bit — channel noise, restart preamble scan.
236 rx_index = 0;
238 }
239 break;
240
241 // Body collection: accumulate bytes until the full frame is in rx_buffer, then dispatch.
242 // rx_buffer[0] = flags byte, rx_buffer[1..] = remaining struct fields.
243
244 case READ_PAYLOAD: {
245 rx_buffer[rx_index++] = inByte;
246 if (rx_index == expected_length) {
247 if (rx_buffer[0] & FLAG_SYN) {
248 // SYN frame complete — processHelloBody() reads nonce from rx_buffer[1..12].
251 updateRxDisplay(currentState, 0, false);
252 } else if (rx_buffer[0] & FLAG_DAT) {
253 // DAT frame — authenticate and play the note.
254 const DataPacket* pkt = reinterpret_cast<const DataPacket*>(rx_buffer);
255 if (s_sessionActive) {
257 } else {
258 // No session key yet — reject and request a new HELLO.
259 Serial.write(NACK_BYTE);
260 }
261 } else if (rx_buffer[0] & FLAG_FIN) {
262 // FIN frame — verify MAC, send ACK, erase session key.
263 const DataPacket* pkt = reinterpret_cast<const DataPacket*>(rx_buffer);
264 if (s_sessionActive) {
265 processFinPacket(pkt);
266 } else {
267 // No active session — nothing to tear down, reset silently.
268 resetParser();
269 }
270 } else {
271 // No recognised flag — should not reach here; noise guard.
272 resetParser();
273 }
274 // Return to preamble scan regardless of the dispatch outcome.
275 rx_index = 0;
277 }
278 break;
279 }
280 }
281}
282
291 // rx_buffer[0] = flags byte (FLAG_SYN confirmed).
292 // rx_buffer[1..12] = 12-byte CSPRNG nonce from TX sendHelloPacket().
294
295 // Pre-install the key so that per-packet handling only needs setIV().
296 s_cipher.clear();
297 s_cipher.setKey(MASTER_PSK, 32u);
298
299 s_sessionActive = true;
300
301 // Confirm to TX that the nonce was received and the session key is armed.
302 // Without this ACK, TX's WAITING_HELLO_ACK state would time out and keep
303 // retransmitting a new HelloPacket on every cycle.
304 Serial.write(ACK_BYTE);
305
306 // Arm the session watchdog: start the silence timer from this moment.
307 // Timeout is capped at 5 s until the first DATA packet reveals note duration.
309 last_valid_packet_time = millis();
310}
311
328 const uint16_t seqNum = pkt->seq_num;
329
330 // Step 1 — Derive per-packet nonce from a local copy of s_sessionNonce.
331 // NEVER XOR directly into s_sessionNonce — would corrupt every subsequent IV.
332 uint8_t packetNonce[HELLO_NONCE_SIZE];
333 memcpy(packetNonce, s_sessionNonce, HELLO_NONCE_SIZE);
334 packetNonce[10] ^= static_cast<uint8_t>((seqNum >> 8u) & 0xFFu);
335 packetNonce[11] ^= static_cast<uint8_t>(seqNum & 0xFFu);
336
337 // Step 2 — 3-byte AAD: flags + both bytes of seq_num (must match TX byte-for-byte).
338 // Any tamper to the flags byte or sequence number will fail the MAC.
339 const uint8_t aad[3] = {
340 pkt->flags,
341 static_cast<uint8_t>(seqNum & 0xFFu), // seq_num low byte
342 static_cast<uint8_t>((seqNum >> 8u) & 0xFFu) // seq_num high byte
343 };
344
345 // Step 3 — Configure cipher for this specific packet.
346 s_cipher.clear();
347 s_cipher.setKey(MASTER_PSK, 32u);
348 s_cipher.setIV(packetNonce, HELLO_NONCE_SIZE);
349 s_cipher.addAuthData(aad, sizeof(aad));
350
351 // Step 4 — Decrypt 4-byte payload; decrypt() simultaneously advances Poly1305 state.
352 uint16_t plaintext[2]; // [0]=note_index, [1]=duration_ms
353 s_cipher.decrypt(
354 reinterpret_cast<uint8_t*>(plaintext),
355 reinterpret_cast<const uint8_t*>(pkt->payload),
357 );
358
359 // Step 5 — Verify truncated 8-byte Poly1305 MAC.
360 // computeTag()+memcmp() is used because the library's checkTag() requires the full 16-byte tag.
361 uint8_t expectedMac[16];
362 s_cipher.computeTag(expectedMac, 16u);
363
364 if (memcmp(expectedMac, pkt->mac, TRUNCATED_MAC_SIZE) != 0) {
365 // MAC mismatch: noise corruption or forgery — NEVER play a note on failed MAC.
366 // Flush UART FIFO via resetParser(); the same burst likely left more garbage.
367 resetParser();
368 Serial.write(NACK_BYTE);
369 lcd.clear();
370 lcd.setCursor(0, 0);
371 lcd.print("MAC FAIL! NACK");
372 lcd.setCursor(0, 1);
373 lcd.print("SEQ:");
374 lcd.print(seqNum);
375 errorDisplayStart = millis();
376 isShowingError = true;
377 return; // Discard — do NOT play any note.
378 }
379
380 // Step 6 — Authentication PASSED. Send ACK.
381 Serial.write(ACK_BYTE);
382
383 // Step 7 — Extract decoded fields; duration is in direct milliseconds (no scaling).
384 const uint16_t noteIndex = plaintext[0];
385 const uint16_t durationMs = plaintext[1];
386
387 // Step 8 — Play note or REST.
388 if (noteIndex == REST_INDEX) {
389 stopNote();
390 } else if (noteIndex < NOTE_DICT_SIZE) {
391 startNote(universal_notes[noteIndex], durationMs);
392 }
393 // noteIndex out of range with a valid MAC: ignore (should never happen).
394
397
398 // Dynamic watchdog: extend timeout by note duration + grace period.
399 // Prevents a long note or post-note silence from triggering a false session reset.
400 current_timeout_limit = static_cast<uint32_t>(durationMs) + NETWORK_GRACE_PERIOD_MS;
401 last_valid_packet_time = millis();
402}
403
416void processFinPacket(const DataPacket* pkt) {
417 const uint16_t seqNum = pkt->seq_num;
418
419 // Step 1 — Derive per-packet nonce (same derivation as TX sendFinPacket()).
420 uint8_t packetNonce[HELLO_NONCE_SIZE];
421 memcpy(packetNonce, s_sessionNonce, HELLO_NONCE_SIZE);
422 packetNonce[10] ^= static_cast<uint8_t>((seqNum >> 8u) & 0xFFu);
423 packetNonce[11] ^= static_cast<uint8_t>(seqNum & 0xFFu);
424
425 // Step 2 — 3-byte AAD: FLAG_FIN + seq_num bytes (must match TX sendFinPacket() exactly).
426 const uint8_t aad[3] = {
427 FLAG_FIN,
428 static_cast<uint8_t>(seqNum & 0xFFu),
429 static_cast<uint8_t>((seqNum >> 8u) & 0xFFu)
430 };
431
432 // Step 3 — Configure cipher.
433 s_cipher.clear();
434 s_cipher.setKey(MASTER_PSK, 32u);
435 s_cipher.setIV(packetNonce, HELLO_NONCE_SIZE);
436 s_cipher.addAuthData(aad, sizeof(aad));
437
438 // Step 4 — Decrypt 4-byte payload into a local discard buffer.
439 // TX encrypts zeros; decrypt() MUST run before computeTag() to advance Poly1305 state.
440 // Skipping decrypt() produces an incorrect expected MAC and causes false NACK on valid FIN.
441 uint8_t discardBuf[DATA_PAYLOAD_SIZE];
442 s_cipher.decrypt(
443 discardBuf,
444 reinterpret_cast<const uint8_t*>(pkt->payload),
446 );
447
448 // Step 5 — Verify truncated (8-byte) MAC.
449 uint8_t expectedMac[16];
450 s_cipher.computeTag(expectedMac, 16u);
451
452 if (memcmp(expectedMac, pkt->mac, TRUNCATED_MAC_SIZE) != 0) {
453 // MAC mismatch — noise or injection; do NOT close the session.
454 // Session remains active so TX can retransmit the FIN.
455 Serial.write(NACK_BYTE);
456 lcd.clear();
457 lcd.setCursor(0, 0);
458 lcd.print("FIN MAC FAIL!");
459 lcd.setCursor(0, 1);
460 lcd.print("NACK sent");
461 resetParser();
462 return;
463 }
464
465 // Step 6 — Authentication passed; confirm to TX.
466 Serial.write(ACK_BYTE);
467
468 // Step 7 — Display session-closed message for the operator.
469 lcd.clear();
470 lcd.setCursor(0, 0);
471 lcd.print("SESSION CLOSED");
472 lcd.setCursor(0, 1);
473 lcd.print("SEQ:");
474 lcd.print(seqNum);
475
476 // Step 8 — CRITICAL: erase the session nonce from RAM (forward secrecy).
477 // With the nonce gone, no future replay of captured ciphertext can be decrypted.
479 s_sessionActive = false;
480
481 // Step 9 — Return parser to clean idle state, ready for the next SYN.
482 resetParser();
483}
484
494void startNote(uint16_t frequencyHz, uint16_t durationMs) {
495 tone(RX_BUZZER_PIN, frequencyHz);
496 noteStartMs = millis();
497 noteLengthMs = durationMs;
498 isPlayingNote = true;
499}
500
506void stopNote() {
507 noTone(RX_BUZZER_PIN);
508 isPlayingNote = false;
509}
510
512static volatile uint32_t s_ringOscPulses = 0;
514static void onRingOscPulse() { ++s_ringOscPulses; }
515
524static inline uint32_t mixEntropy(uint32_t pool, uint32_t bits) {
525 return ((pool << 1u) | (pool >> 31u)) ^ bits;
526}
527
545void generateEntropyPool(uint8_t* outputSeed) {
546 // SRAM base: 64 uninitialised bytes starting at 0x0100 on ATmega328P.
547 // Each word draws from a distinct non-overlapping 8-byte slice.
548 const uint8_t* sramBase = reinterpret_cast<const uint8_t*>(0x0100);
549
550 for (uint8_t wordIndex = 0; wordIndex < 8u; ++wordIndex) {
551 uint32_t pool = 0;
552
553 // Source 1: Ring oscillator gate (pin 2, INT0) — fresh 2 ms window per word.
554 s_ringOscPulses = 0;
555 attachInterrupt(digitalPinToInterrupt(ENTROPY_RING_OSC_PIN),
556 onRingOscPulse, RISING);
557 delay(2);
558 detachInterrupt(digitalPinToInterrupt(ENTROPY_RING_OSC_PIN));
559 pool = mixEntropy(pool, s_ringOscPulses);
560
561 // Source 2: SRAM chaos — 8 unique bytes per word (slice: wordIndex*8 .. +7).
562 for (uint8_t i = 0; i < 8u; ++i) {
563 pool = mixEntropy(pool, sramBase[wordIndex * 8u + i]);
564 }
565
566 // Source 3: On-die temperature ADC (channel 8, 1.1 V ref) — 8 LSBs per word.
567 {
568 const uint8_t savedAdmux = ADMUX;
569 ADMUX = _BV(REFS1) | _BV(REFS0) | _BV(MUX3); // 0xC8
570 ADCSRA |= _BV(ADEN);
571 // Discard first conversion after reference change
572 ADCSRA |= _BV(ADSC); while (ADCSRA & _BV(ADSC)) {}
573 uint8_t adcEntropy = 0;
574 for (uint8_t i = 0; i < 8u; ++i) {
575 ADCSRA |= _BV(ADSC); while (ADCSRA & _BV(ADSC)) {}
576 adcEntropy = static_cast<uint8_t>((adcEntropy << 1u) | (ADCL & 0x01u));
577 }
578 pool = mixEntropy(pool, adcEntropy);
579 ADMUX = savedAdmux;
580 }
581
582 // Source 4: TCNT1 timer jitter — unique value at each iteration boundary.
583 pool = mixEntropy(pool, static_cast<uint32_t>(TCNT1));
584
585 // Source 5: A0 white noise generator — 8 LSBs per word.
586 {
587 uint8_t a0Entropy = 0;
588 for (uint8_t i = 0; i < 8u; ++i) {
589 a0Entropy = static_cast<uint8_t>(
590 (a0Entropy << 1u) | (static_cast<uint8_t>(analogRead(A0)) & 0x01u)
591 );
592 }
593 pool = mixEntropy(pool, a0Entropy);
594 }
595
596 // NOTE: micros() human-timing jitter is intentionally absent on RX — no button present.
597
598 // Source 6: Arduino software PRNG (obfuscation layer).
599 pool = mixEntropy(pool, static_cast<uint32_t>(random()));
600
601 // Write the 32-bit word into the output seed as 4 bytes (little-endian).
602 outputSeed[wordIndex * 4u + 0u] = static_cast<uint8_t>(pool);
603 outputSeed[wordIndex * 4u + 1u] = static_cast<uint8_t>(pool >> 8u);
604 outputSeed[wordIndex * 4u + 2u] = static_cast<uint8_t>(pool >> 16u);
605 outputSeed[wordIndex * 4u + 3u] = static_cast<uint8_t>(pool >> 24u);
606 }
607}
608
615void rx_setup() {
616 // UART: 9600 8N1 — must match the transmitter exactly.
617 Serial.begin(BAUD_RATE);
618
619 // Buzzer pin configured as output; stays silent until a valid note arrives.
620 pinMode(RX_BUZZER_PIN, OUTPUT);
621
622 // I2C LCD (Aip31068 compatible, address 0x27).
623 lcd.init();
624 // lcd.backlight();
625
626 // Reset parser and buffer — start at sync preamble acquisition.
628 rx_index = 0;
630
631 updateRxDisplay(currentState, 0, false);
632
633 // Harvest 256-bit hardware entropy and seed the ChaCha20 CSPRNG.
634 // ~16 ms total (8 ring-oscillator gate windows of 2 ms each) — one-time cost.
635 // The seed is scrubbed from the stack immediately after handing it to the cipher.
636 {
637 uint8_t entropySeed[32];
638 generateEntropyPool(entropySeed);
639 initCSPRNG(entropySeed);
640 memset(entropySeed, 0, sizeof(entropySeed));
641 }
642
643 // ENTROPY TEST (remove after validation)
644 // Harvest entropy immediately after hardware init so SRAM chaos bytes retain
645 // their power-on state and the ring oscillator has a fresh count window.
646//{ uint8_t seedBuf[32];
647// generateEntropyPool(seedBuf);
648// // Display first 4 bytes (word 0) as hex for quick visual check.
649// const uint32_t previewWord =
650// (static_cast<uint32_t>(seedBuf[3]) << 24u) |
651// (static_cast<uint32_t>(seedBuf[2]) << 16u) |
652// (static_cast<uint32_t>(seedBuf[1]) << 8u) |
653// static_cast<uint32_t>(seedBuf[0]);
654// lcd.clear();
655// lcd.setCursor(0, 0);
656// lcd.print("RX KEY:");
657// lcd.setCursor(0, 1);
658// lcd.print(previewWord, HEX); // e.g. "5D8E0F41"
659// delay(3000); // Hold result on screen for 3 s
660// lcd.clear();
661// updateRxDisplay(currentState, 0, false);
662//}
663 // END ENTROPY TEST
664}
665
672void rx_loop() {
673 // MAC error-flash expiry: restore normal display after CHK_ERR_DISPLAY_MS.
674 if (isShowingError && ((millis() - errorDisplayStart) >= CHK_ERR_DISPLAY_MS)) {
675 isShowingError = false;
677 }
678
679 // Non-blocking note duration management.
680 if (isPlayingNote && ((millis() - noteStartMs) >= noteLengthMs)) {
681 stopNote();
684 updateRxDisplay(currentState, 0, false);
685 }
686 }
687
688 // SESSION WATCHDOG — prevents deadlock if TX disappears without sending FLAG_FIN.
689 // current_timeout_limit is updated dynamically: 5 s initially, then
690 // (last note duration + NETWORK_GRACE_PERIOD_MS) after each authenticated packet.
691 if (s_sessionActive &&
693 lcd.clear();
694 lcd.setCursor(0, 0);
695 lcd.print("TIMEOUT! DROP");
696 lcd.setCursor(0, 1);
697 lcd.print("Session reset");
698 // Erase key material so replayed captured ciphertext cannot be decrypted.
700 s_sessionActive = false;
701 resetParser();
702 }
703
704 // Non-blocking UART reading; processReceivedByte() handles all parsing and crypto.
705 while (Serial.available() > 0) {
706 const uint8_t inByte = static_cast<uint8_t>(Serial.read());
707 rxLastByteMs = millis();
708 processReceivedByte(inByte);
709 }
710
711 // PARSER TIMEOUT — self-healing against mid-packet desync caused by a noise burst.
712 if (rx_index > 0 && parseState != WAIT_AA &&
713 (millis() - rxLastByteMs) >= RX_PARSER_TIMEOUT_MS) {
714 resetParser();
716 }
717}
718
720void setup() { rx_setup(); }
722void loop() { rx_loop(); }
static volatile uint32_t s_ringOscPulses
Pulse counter incremented by the ring oscillator ISR on INT0 (pin 2).
void authenticateAndPlay(const DataPacket *pkt)
Run the full ChaCha20-Poly1305 pipeline; play the note only on MAC success.
void processFinPacket(const DataPacket *pkt)
Authenticate and process a FLAG_FIN session-teardown packet.
static void onRingOscPulse()
Ring oscillator ISR — increments pulse counter on each RISING edge.
static uint32_t noteStartMs
Timestamp (millis()) when the current note started playing.
static LiquidCrystal_AIP31068_I2C lcd(RX_LCD_ADDR, RX_LCD_COLS, RX_LCD_ROWS)
I2C LCD display object (Aip31068, 16×2, address 0x3E).
static bool isShowingError
True when the LCD is currently showing a MAC-error message.
static RxState currentState
Tracks the current RxState for LCD display updates; independent of the byte-parser FSM.
ParseState
Four-state byte-level frame assembly FSM states.
@ READ_TYPE
@ READ_PAYLOAD
@ WAIT_55
@ WAIT_AA
void rx_loop()
Execute one non-blocking C2P-ARQ FSM tick for the Receiver node.
static uint32_t errorDisplayStart
Timestamp (millis()) when the MAC-error display was last activated.
void processReceivedByte(uint8_t inByte)
Consume one incoming UART byte and advance the frame-assembly FSM.
const uint16_t RX_PARSER_TIMEOUT_MS
Stale byte accumulation timeout for the byte-level frame parser.
void startNote(uint16_t frequencyHz, uint16_t durationMs)
Start non-blocking note playback on the piezo buzzer.
static uint16_t noteLengthMs
Duration in milliseconds that the current note should sound.
static uint32_t mixEntropy(uint32_t pool, uint32_t bits)
Single-step cryptographic entropy mixer — rotate-left XOR fold.
void setup()
Arduino entry point — delegates to rx_setup().
void processHelloBody()
Store the received session nonce and install MASTER_PSK into the cipher.
static uint32_t current_timeout_limit
Current Dynamic Watchdog timeout in milliseconds.
static uint8_t expected_length
Total expected bytes for the current frame; set in READ_TYPE state.
static uint8_t s_sessionNonce[HELLO_NONCE_SIZE]
12-byte session nonce delivered by the last accepted HelloPacket (FLAG_SYN).
static bool s_sessionActive
Session active flag — set after a valid HelloPacket handshake; cleared on FIN or watchdog timeout.
static uint8_t rx_buffer[sizeof(DataPacket)]
Raw receive buffer sized to hold the largest frame body (DataPacket, 15 bytes).
static const uint16_t universal_notes[NOTE_DICT_SIZE]
Universal note frequency dictionary for the C2P-ARQ Receiver.
void generateEntropyPool(uint8_t *outputSeed)
Harvest 256 bits of hardware entropy and fill outputSeed (RX variant).
void stopNote()
Stop the currently playing note and clear the playback flag.
static void resetParser()
Flush the UART RX FIFO and reset the byte-parser FSM to WAIT_AA.
static uint8_t rx_index
Number of bytes written to rx_buffer for the frame currently being collected.
static uint32_t last_valid_packet_time
Timestamp (millis()) of the most recently successfully authenticated DataPacket.
static ParseState parseState
Current byte-parser FSM state; reset to WAIT_AA by resetParser().
static ChaChaPoly s_cipher
ChaCha20-Poly1305 cipher instance; re-initialised per packet via clear().
void updateRxDisplay(RxState state, uint16_t seqNum, bool macOk)
Refresh the RX LCD with the current FSM state and last packet result.
const uint32_t NETWORK_GRACE_PERIOD_MS
Network grace period added to the last note's duration for the Dynamic Smart Watchdog.
static uint32_t rxLastByteMs
Timestamp (millis()) of the most recently received UART byte.
const uint16_t CHK_ERR_DISPLAY_MS
Duration of the MAC-error message shown on the LCD before returning to idle display.
static bool isPlayingNote
True while a note is actively sounding; cleared by stopNote().
void rx_setup()
Initialise RX hardware, seed the CSPRNG, and display the idle screen.
void loop()
Arduino main loop — delegates to rx_loop().
void initCSPRNG(const uint8_t *hardwareSeed)
Seed the ChaCha20 CSPRNG with 256 bits of hardware entropy.
Definition csprng.h:102
static uint16_t seqNum
Packet sequence number (0–65535, wraps).
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 REST_INDEX
Special note index value representing a rest (silence).
Definition protocol.h:149
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 RX_BUZZER_PIN
PWM-capable pin connected to the piezo buzzer.
Definition receiver.h:24
const uint8_t RX_LCD_ADDR
I2C address of the Aip31068 16x2 LCD display.
Definition receiver.h:26
const uint8_t RX_LCD_ROWS
Number of rows on the RX LCD.
Definition receiver.h:28
const uint8_t RX_LCD_COLS
Number of columns on the RX LCD.
Definition receiver.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 ENTROPY_RING_OSC_PIN
Hardware ring oscillator entropy source (INT0).
Definition transmitter.h:23
const uint16_t BAUD_RATE
UART baud rate in bps (8N1). Chosen for clear bit-width visibility on SimulIDE oscilloscope.
Definition protocol.h:165
SH2SC-EDT — Receiver Node B ("The Synthesizer") public interface.
const uint8_t NOTE_DICT_SIZE
Size of the universal note frequency dictionary on RX.
Definition receiver.h:36
RxState
Byte-level parser Finite State Machine states for the C2P-ARQ Receiver (Node B).
Definition receiver.h:55
@ WAITING_SYNC_2
SYNC_BYTE_1 confirmed — waiting for SYNC_BYTE_2 (0x55).
@ GOT_DATA
Full DataPacket buffered; authenticateAndPlay() will be called by rx_loop().
@ READING_DATA
Accumulating the 14-byte body (seq_num + payload + mac) of a DataPacket.
@ READING_HELLO
Accumulating the 12-byte nonce body of a HelloPacket into the RX buffer.
@ WAITING_FOR_TYPE
Sync preamble complete — waiting for the frame flags byte.
@ GOT_HELLO
Full HelloPacket buffered; processHelloBody() will be called by rx_loop().
@ EXECUTING_ACTION
MAC verified; note is sounding — non-blocking timer wait for note end.
@ WAITING_SYNC_1
Initial/reset state — scanning the UART stream for SYNC_BYTE_1 (0xAA).
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