diff --git a/src/CMRI.cpp b/src/CMRI.cpp index e359b1e..5b3463d 100644 --- a/src/CMRI.cpp +++ b/src/CMRI.cpp @@ -39,7 +39,7 @@ CMRI::CMRI(unsigned int address, unsigned int input_bits, unsigned int output_bi // parsing state , - _mode(PREAMBLE_1), _rx_index(0), _rx_data_len(0), _init_handler(nullptr) + _mode(PREAMBLE_1), _rx_index(0), _rx_data_len(0), _init_handler(nullptr), _last_byte_time_ms(0) { // clear to zero @@ -164,6 +164,20 @@ void CMRI::transmit() // Private methods uint8_t CMRI::_decode(uint8_t c) { + // Inter-byte timeout: if a gap between bytes exceeds INTER_BYTE_TIMEOUT_MS, + // assume the current frame was truncated and reset the parser to wait for a + // new preamble. This prevents a corrupt frame from consuming subsequent data. + unsigned long now = millis(); + if (_mode != PREAMBLE_1 && _mode != PREAMBLE_2 && _mode != PREAMBLE_3) + { + if (now - _last_byte_time_ms > INTER_BYTE_TIMEOUT_MS) + { + _mode = PREAMBLE_1; + _rx_index = 0; + } + } + _last_byte_time_ms = now; + switch (_mode) { case PREAMBLE_1: diff --git a/src/CMRI.h b/src/CMRI.h index 52b453a..75fcc22 100644 --- a/src/CMRI.h +++ b/src/CMRI.h @@ -91,6 +91,14 @@ class CMRI // parsing state variables int _mode; int _rx_index; + unsigned long _last_byte_time_ms; + +#ifndef INTER_BYTE_TIMEOUT_MS +// 1 char time = 11 bits / baud_rate (8N2 framing: 1 start + 8 data + 2 stop) +// default = 5 char times at 9600 bps = 5.73ms +// For other baud rates: (5UL * 11 * 1000) / baud_rate +#define INTER_BYTE_TIMEOUT_MS 6 +#endif uint8_t _decode(uint8_t c); // process one character received from serial port }; diff --git a/test/mock/Arduino.h b/test/mock/Arduino.h index b1d835b..8840064 100644 --- a/test/mock/Arduino.h +++ b/test/mock/Arduino.h @@ -62,6 +62,16 @@ inline void delayMicroseconds(unsigned int) { } +extern unsigned long _mock_millis; +inline unsigned long millis() +{ + return _mock_millis; +} +inline void mock_advance_millis(unsigned long ms) +{ + _mock_millis += ms; +} + // The default argument of the CMRI constructor references Serial; the test // translation unit defines it. extern Stream Serial; diff --git a/test/test_cmri/test_main.cpp b/test/test_cmri/test_main.cpp index e4b60aa..0513712 100644 --- a/test/test_cmri/test_main.cpp +++ b/test/test_cmri/test_main.cpp @@ -24,8 +24,12 @@ // which always pass an explicit Stream, but needed to satisfy the symbol). Stream Serial; +// Definition of the mock millis variable (declared extern in mock/Arduino.h). +unsigned long _mock_millis = 0; + void setUp(void) { + _mock_millis = 0; } void tearDown(void) @@ -194,6 +198,75 @@ void test_preamble_resync_after_garbage(void) TEST_ASSERT_EQUAL_UINT8(CMRI::GET, s.tx[4]); } +// A truncated frame does not corrupt the next frame after a timeout. +void test_truncated_frame_does_not_corrupt_next(void) +{ + Stream s; + CMRI cmri(0, 24, 48, s); + + // Feed a partial SET without ETX (truncated) + s.feed(0xFF); + s.feed(0xFF); + s.feed(CMRI::STX); + s.feed('A' + 0); + s.feed(CMRI::SET); + s.feed(0xAA); + s.feed(0xBB); + s.feed(0xCC); + + // Process the partial frame -- should not return true + TEST_ASSERT_FALSE(cmri.process()); + + // Advance time past the inter-byte timeout + mock_advance_millis(10); + + // Now send a complete SET (skip 0x03 -- protocol byte) + uint8_t full[6] = {0x01, 0x02, 0x04, 0x05, 0x06, 0x07}; + feed_packet(s, 0, CMRI::SET, full, 6); + + TEST_ASSERT_TRUE(cmri.process()); + + // Only the second SET's data should be in the output buffer + TEST_ASSERT_EQUAL_UINT8(0x01, cmri.get_byte(0)); + TEST_ASSERT_EQUAL_UINT8(0x02, cmri.get_byte(1)); + TEST_ASSERT_EQUAL_UINT8(0x04, cmri.get_byte(2)); + TEST_ASSERT_EQUAL_UINT8(0x05, cmri.get_byte(3)); + TEST_ASSERT_EQUAL_UINT8(0x06, cmri.get_byte(4)); + TEST_ASSERT_EQUAL_UINT8(0x07, cmri.get_byte(5)); +} + +// A DLE at the end of a truncated frame does not corrupt the next frame. +void test_dle_at_end_of_truncated_frame(void) +{ + Stream s; + CMRI cmri(0, 24, 48, s); + + // Feed a partial SET ending with ESC byte (no following byte) + s.feed(0xFF); + s.feed(0xFF); + s.feed(CMRI::STX); + s.feed('A' + 0); + s.feed(CMRI::SET); + s.feed(CMRI::ESC); + + // Process the partial frame -- should not return true + TEST_ASSERT_FALSE(cmri.process()); + + // Advance time past the inter-byte timeout + mock_advance_millis(10); + + // Now send a complete SET (skip 0x03 -- protocol byte) + uint8_t full[6] = {0x01, 0x02, 0x04, 0x05, 0x06, 0x07}; + feed_packet(s, 0, CMRI::SET, full, 6); + + TEST_ASSERT_TRUE(cmri.process()); + + TEST_ASSERT_EQUAL_UINT8(0x01, cmri.get_byte(0)); + TEST_ASSERT_EQUAL_UINT8(0x02, cmri.get_byte(1)); + TEST_ASSERT_EQUAL_UINT8(0x04, cmri.get_byte(2)); + TEST_ASSERT_EQUAL_UINT8(0x05, cmri.get_byte(3)); +} + int main(int, char **) { UNITY_BEGIN(); @@ -206,5 +279,7 @@ int main(int, char **) RUN_TEST(test_address_filtering); RUN_TEST(test_transmit_escapes_control_bytes); RUN_TEST(test_preamble_resync_after_garbage); + RUN_TEST(test_truncated_frame_does_not_corrupt_next); + RUN_TEST(test_dle_at_end_of_truncated_frame); return UNITY_END(); }