diff --git a/platformio.ini b/platformio.ini index adfc42c..20b0701 100644 --- a/platformio.ini +++ b/platformio.ini @@ -109,7 +109,7 @@ lib_deps = platform = native test_framework = unity test_build_src = yes -build_src_filter = -<*> + + + + + + + +build_src_filter = -<*> + + + + + + + + build_flags = -std=gnu++17 -Isrc diff --git a/src/PulseDecoder.cpp b/src/PulseDecoder.cpp new file mode 100644 index 0000000..151f4e9 --- /dev/null +++ b/src/PulseDecoder.cpp @@ -0,0 +1,119 @@ +#include "PulseDecoder.h" +#if defined(ESP32) +#include +#define PULSE_DECODER_ATTR IRAM_ATTR +#else +#define PULSE_DECODER_ATTR +#endif + +// Each timing is accepted within 30%. +#define TOLERANCE_MIN 0.7 +#define TOLERANCE_MAX 1.3 +static const uint32_t tempo_wakeup_pulse = 9415; +static const uint32_t tempo_wakeup_min = 9415 * TOLERANCE_MIN; +static const uint32_t tempo_wakeup_max = 9415 * TOLERANCE_MAX; +static const uint32_t tempo_wakeup_silence = 89565; +static const uint32_t tempo_wakeup_silence_min = 89565 * TOLERANCE_MIN; +static const uint32_t tempo_wakeup_silence_max = 89565 * TOLERANCE_MAX; +static const uint32_t tempo_synchro_hw_min = SYMBOL * 4 * TOLERANCE_MIN; +static const uint32_t tempo_synchro_hw_max = SYMBOL * 4 * TOLERANCE_MAX; +static const uint32_t tempo_synchro_sw_min = 4850 * TOLERANCE_MIN; +static const uint32_t tempo_synchro_sw_max = 4850 * TOLERANCE_MAX; +static const uint32_t tempo_half_symbol_min = SYMBOL * TOLERANCE_MIN; +static const uint32_t tempo_half_symbol_max = SYMBOL * TOLERANCE_MAX; +static const uint32_t tempo_symbol_min = SYMBOL * 2 * TOLERANCE_MIN; +static const uint32_t tempo_symbol_max = SYMBOL * 2 * TOLERANCE_MAX; +static const uint32_t tempo_if_gap = 30415; // Gap between frames +static int16_t bitMin = SYMBOL * TOLERANCE_MIN; +PULSE_DECODER_ATTR PulseResult decodePulse(somfy_rx_t &rx, uint32_t durationUs) { + const unsigned int duration = durationUs; + if (duration < bitMin) { + // The incoming bit is < 448us so it is probably a glitch so blow it off. + // We need to ignore this bit. + // REMOVE THIS AFTER WE DETERMINE THAT THE out-of-bounds stuff isn't a problem. If there are bits + // from the previous frame then we will capture this data here. + if(rx.pulseCount < MAX_TIMINGS && rx.cpt_synchro_hw > 0) rx.pulses[rx.pulseCount++] = duration; + return PulseResult::glitch; + } + switch (rx.status) { + case waiting_synchro: + if(rx.pulseCount < MAX_TIMINGS) rx.pulses[rx.pulseCount++] = duration; + if (duration > tempo_synchro_hw_min && duration < tempo_synchro_hw_max) { + // We have found a hardware sync bit. There should be at least 4 of these. + ++rx.cpt_synchro_hw; + } + else if (duration > tempo_synchro_sw_min && duration < tempo_synchro_sw_max && rx.cpt_synchro_hw >= 4) { + // If we have a full hardware sync then we should look for the software sync. If we have a software sync + // bit and enough hardware sync bits then we should start receiving data. It turns out that a 56 bit packet + // with give 4 or 14 bits of hardware sync. An 80 bit packet gives 12, 13 or 24 bits of hw sync. Early on + // I had some shorter and longer hw syncs but I can no longer repeat this. + memset(rx.payload, 0x00, sizeof(rx.payload)); + rx.previous_bit = 0x00; + rx.waiting_half_symbol = false; + rx.cpt_bits = 0; + // Keep an eye on this as it is possible that we might get fewer or more synchro bits. + if (rx.cpt_synchro_hw <= 7) rx.bit_length = 56; + else if (rx.cpt_synchro_hw == 14) rx.bit_length = 56; + else if (rx.cpt_synchro_hw == 13) rx.bit_length = 80; // The RS485 device sends this sync. + else if (rx.cpt_synchro_hw == 12) rx.bit_length = 80; + else if (rx.cpt_synchro_hw > 17) rx.bit_length = 80; + else rx.bit_length = 56; + //rx.bit_length = 80; + rx.status = receiving_data; + } + else { + // Reset and start looking for hardware sync again. + rx.cpt_synchro_hw = 0; + // Try to capture the wakeup pulse. + if(duration > tempo_wakeup_min && duration < tempo_wakeup_max) + { + memset(&rx.payload, 0x00, sizeof(rx.payload)); + rx.previous_bit = 0x00; + rx.waiting_half_symbol = false; + rx.cpt_bits = 0; + rx.bit_length = 56; + } + else if((rx.pulseCount > 20 && rx.cpt_synchro_hw == 0) || duration > 250000) { + rx.pulseCount = 0; + } + } + break; + case receiving_data: + if(rx.pulseCount < MAX_TIMINGS) rx.pulses[rx.pulseCount++] = duration; + // We should be receiving data at this point. + if (duration > tempo_symbol_min && duration < tempo_symbol_max && !rx.waiting_half_symbol) { + rx.previous_bit = 1 - rx.previous_bit; + // Bits come in high order bit first. + rx.payload[rx.cpt_bits / 8] += rx.previous_bit << (7 - rx.cpt_bits % 8); + ++rx.cpt_bits; + } + else if (duration > tempo_half_symbol_min && duration < tempo_half_symbol_max) { + if (rx.waiting_half_symbol) { + rx.waiting_half_symbol = false; + rx.payload[rx.cpt_bits / 8] += rx.previous_bit << (7 - rx.cpt_bits % 8); + ++rx.cpt_bits; + } + else { + rx.waiting_half_symbol = true; + } + } + else { + //++rx.cpt_bits; + // Start over we are not within our parameters for bit timing. + memset(&rx.payload, 0x00, sizeof(rx.payload)); + rx.pulseCount = 1; + rx.cpt_synchro_hw = 0; + rx.previous_bit = 0x00; + rx.waiting_half_symbol = false; + rx.cpt_bits = 0; + rx.bit_length = 56; + rx.status = waiting_synchro; + rx.pulses[0] = duration; + } + break; + default: + break; + } + return rx.status == receiving_data && rx.cpt_bits >= rx.bit_length ? PulseResult::frame : PulseResult::more; +} + diff --git a/src/PulseDecoder.h b/src/PulseDecoder.h new file mode 100644 index 0000000..57f4c32 --- /dev/null +++ b/src/PulseDecoder.h @@ -0,0 +1,53 @@ +#ifndef PULSE_DECODER_H +#define PULSE_DECODER_H +#include +#include + +// Turns the time between edges of a received RTS transmission into frame +// bits. Runs in the receive interrupt, so it stays in IRAM on the ESP32, and +// is kept free of the radio so it can be unit tested on the PC +// (pio test -e native). + +// Half an RTS bit in us. A bit is two symbols with an edge in the middle. +#define SYMBOL 640 + +#define MAX_TIMINGS 300 + +typedef enum { + waiting_synchro = 0, + receiving_data = 1, + complete = 2 +} t_status; + +struct somfy_rx_t { + void clear() { + this->status = t_status::waiting_synchro; + this->bit_length = 56; + this->cpt_synchro_hw = 0; + this->cpt_bits = 0; + this->previous_bit = 0; + this->waiting_half_symbol = false; + memset(this->payload, 0, sizeof(this->payload)); + memset(this->pulses, 0, sizeof(this->pulses)); + this->pulseCount = 0; + } + t_status status; + uint8_t bit_length = 56; + uint8_t cpt_synchro_hw = 0; + uint8_t cpt_bits = 0; + uint8_t previous_bit = 0; + bool waiting_half_symbol; + uint8_t payload[10]; + unsigned int pulses[MAX_TIMINGS]; + uint16_t pulseCount = 0; +}; + +enum class PulseResult : uint8_t { + glitch, // Too short to be a symbol. The caller times the next edge from the last one. + more, // Taken, the frame isn't complete yet. + frame // rx holds a complete frame. +}; +// Feeds the time since the previous edge to rx. +PulseResult decodePulse(somfy_rx_t &rx, uint32_t durationUs); +#endif + diff --git a/src/Transceiver.cpp b/src/Transceiver.cpp index d11ca68..24b13bd 100644 --- a/src/Transceiver.cpp +++ b/src/Transceiver.cpp @@ -19,7 +19,6 @@ extern SocketEmitter sockEmit; extern ConfigSettings settings; uint8_t rxmode = 0; // Indicates whether the radio is in receive mode. Just to ensure there isn't more than one interrupt hooked. -#define SYMBOL 640 #if defined(ESP8266) #define RECEIVE_ATTR ICACHE_RAM_ATTR #elif defined(ESP32) @@ -75,24 +74,6 @@ void somfy_frame_t::decodeFrame(somfy_rx_t *rx) { this->decodeFrame(rx->payload); } // Transceiver Implementation -#define TOLERANCE_MIN 0.7 -#define TOLERANCE_MAX 1.3 -static const uint32_t tempo_wakeup_pulse = 9415; -static const uint32_t tempo_wakeup_min = 9415 * TOLERANCE_MIN; -static const uint32_t tempo_wakeup_max = 9415 * TOLERANCE_MAX; -static const uint32_t tempo_wakeup_silence = 89565; -static const uint32_t tempo_wakeup_silence_min = 89565 * TOLERANCE_MIN; -static const uint32_t tempo_wakeup_silence_max = 89565 * TOLERANCE_MAX; -static const uint32_t tempo_synchro_hw_min = SYMBOL * 4 * TOLERANCE_MIN; -static const uint32_t tempo_synchro_hw_max = SYMBOL * 4 * TOLERANCE_MAX; -static const uint32_t tempo_synchro_sw_min = 4850 * TOLERANCE_MIN; -static const uint32_t tempo_synchro_sw_max = 4850 * TOLERANCE_MAX; -static const uint32_t tempo_half_symbol_min = SYMBOL * TOLERANCE_MIN; -static const uint32_t tempo_half_symbol_max = SYMBOL * TOLERANCE_MAX; -static const uint32_t tempo_symbol_min = SYMBOL * 2 * TOLERANCE_MIN; -static const uint32_t tempo_symbol_max = SYMBOL * 2 * TOLERANCE_MAX; -static const uint32_t tempo_if_gap = 30415; // Gap between frames -static int16_t bitMin = SYMBOL * TOLERANCE_MIN; static somfy_rx_t somfy_rx; static somfy_rx_queue_t rx_queue; static somfy_tx_queue_t tx_queue; @@ -210,95 +191,10 @@ void RECEIVE_ATTR Transceiver::handleReceive() { static unsigned long last_time = 0; const long time = micros(); const unsigned int duration = time - last_time; - if (duration < bitMin) { - // The incoming bit is < 448us so it is probably a glitch so blow it off. - // We need to ignore this bit. - // REMOVE THIS AFTER WE DETERMINE THAT THE out-of-bounds stuff isn't a problem. If there are bits - // from the previous frame then we will capture this data here. - if(somfy_rx.pulseCount < MAX_TIMINGS && somfy_rx.cpt_synchro_hw > 0) somfy_rx.pulses[somfy_rx.pulseCount++] = duration; - return; - } + const PulseResult result = decodePulse(somfy_rx, duration); + if (result == PulseResult::glitch) return; last_time = time; - switch (somfy_rx.status) { - case waiting_synchro: - if(somfy_rx.pulseCount < MAX_TIMINGS) somfy_rx.pulses[somfy_rx.pulseCount++] = duration; - if (duration > tempo_synchro_hw_min && duration < tempo_synchro_hw_max) { - // We have found a hardware sync bit. There should be at least 4 of these. - ++somfy_rx.cpt_synchro_hw; - } - else if (duration > tempo_synchro_sw_min && duration < tempo_synchro_sw_max && somfy_rx.cpt_synchro_hw >= 4) { - // If we have a full hardware sync then we should look for the software sync. If we have a software sync - // bit and enough hardware sync bits then we should start receiving data. It turns out that a 56 bit packet - // with give 4 or 14 bits of hardware sync. An 80 bit packet gives 12, 13 or 24 bits of hw sync. Early on - // I had some shorter and longer hw syncs but I can no longer repeat this. - memset(somfy_rx.payload, 0x00, sizeof(somfy_rx.payload)); - somfy_rx.previous_bit = 0x00; - somfy_rx.waiting_half_symbol = false; - somfy_rx.cpt_bits = 0; - // Keep an eye on this as it is possible that we might get fewer or more synchro bits. - if (somfy_rx.cpt_synchro_hw <= 7) somfy_rx.bit_length = 56; - else if (somfy_rx.cpt_synchro_hw == 14) somfy_rx.bit_length = 56; - else if (somfy_rx.cpt_synchro_hw == 13) somfy_rx.bit_length = 80; // The RS485 device sends this sync. - else if (somfy_rx.cpt_synchro_hw == 12) somfy_rx.bit_length = 80; - else if (somfy_rx.cpt_synchro_hw > 17) somfy_rx.bit_length = 80; - else somfy_rx.bit_length = 56; - //somfy_rx.bit_length = 80; - somfy_rx.status = receiving_data; - } - else { - // Reset and start looking for hardware sync again. - somfy_rx.cpt_synchro_hw = 0; - // Try to capture the wakeup pulse. - if(duration > tempo_wakeup_min && duration < tempo_wakeup_max) - { - memset(&somfy_rx.payload, 0x00, sizeof(somfy_rx.payload)); - somfy_rx.previous_bit = 0x00; - somfy_rx.waiting_half_symbol = false; - somfy_rx.cpt_bits = 0; - somfy_rx.bit_length = 56; - } - else if((somfy_rx.pulseCount > 20 && somfy_rx.cpt_synchro_hw == 0) || duration > 250000) { - somfy_rx.pulseCount = 0; - } - } - break; - case receiving_data: - if(somfy_rx.pulseCount < MAX_TIMINGS) somfy_rx.pulses[somfy_rx.pulseCount++] = duration; - // We should be receiving data at this point. - if (duration > tempo_symbol_min && duration < tempo_symbol_max && !somfy_rx.waiting_half_symbol) { - somfy_rx.previous_bit = 1 - somfy_rx.previous_bit; - // Bits come in high order bit first. - somfy_rx.payload[somfy_rx.cpt_bits / 8] += somfy_rx.previous_bit << (7 - somfy_rx.cpt_bits % 8); - ++somfy_rx.cpt_bits; - } - else if (duration > tempo_half_symbol_min && duration < tempo_half_symbol_max) { - if (somfy_rx.waiting_half_symbol) { - somfy_rx.waiting_half_symbol = false; - somfy_rx.payload[somfy_rx.cpt_bits / 8] += somfy_rx.previous_bit << (7 - somfy_rx.cpt_bits % 8); - ++somfy_rx.cpt_bits; - } - else { - somfy_rx.waiting_half_symbol = true; - } - } - else { - //++somfy_rx.cpt_bits; - // Start over we are not within our parameters for bit timing. - memset(&somfy_rx.payload, 0x00, sizeof(somfy_rx.payload)); - somfy_rx.pulseCount = 1; - somfy_rx.cpt_synchro_hw = 0; - somfy_rx.previous_bit = 0x00; - somfy_rx.waiting_half_symbol = false; - somfy_rx.cpt_bits = 0; - somfy_rx.bit_length = 56; - somfy_rx.status = waiting_synchro; - somfy_rx.pulses[0] = duration; - } - break; - default: - break; - } - if (somfy_rx.status == receiving_data && somfy_rx.cpt_bits >= somfy_rx.bit_length) { + if (result == PulseResult::frame) { // Since we are operating within the interrupt all data really needs to be static // for the handoff to the frame decoder. For this reason we are buffering up to // 3 total frames. Althought it may not matter considering the length of a packet diff --git a/src/Transceiver.h b/src/Transceiver.h index 23cdbca..642f342 100644 --- a/src/Transceiver.h +++ b/src/Transceiver.h @@ -6,41 +6,12 @@ #include "SomfyFrame.h" #include "TxBuffer.h" #include "RadioScheduler.h" +#include "PulseDecoder.h" // The CC1101 radio: receive decoding, the radio task that transmits radio // jobs, the frequency scan and the radio config. -#define MAX_TIMINGS 300 #define MAX_RX_BUFFER 3 - -typedef enum { - waiting_synchro = 0, - receiving_data = 1, - complete = 2 -} t_status; - -struct somfy_rx_t { - void clear() { - this->status = t_status::waiting_synchro; - this->bit_length = 56; - this->cpt_synchro_hw = 0; - this->cpt_bits = 0; - this->previous_bit = 0; - this->waiting_half_symbol = false; - memset(this->payload, 0, sizeof(this->payload)); - memset(this->pulses, 0, sizeof(this->pulses)); - this->pulseCount = 0; - } - t_status status; - uint8_t bit_length = 56; - uint8_t cpt_synchro_hw = 0; - uint8_t cpt_bits = 0; - uint8_t previous_bit = 0; - bool waiting_half_symbol; - uint8_t payload[10]; - unsigned int pulses[MAX_TIMINGS]; - uint16_t pulseCount = 0; -}; // A simple FIFO queue to hold rx buffers. We are using // a byte index to make it so we don't have to reorganize // the storage each time we push or pop. diff --git a/test/test_pulse/test_pulse.cpp b/test/test_pulse/test_pulse.cpp new file mode 100644 index 0000000..1499f52 --- /dev/null +++ b/test/test_pulse/test_pulse.cpp @@ -0,0 +1,129 @@ +#include +#include +#include "PulseDecoder.h" + +void setUp() {} +void tearDown() {} + +static const uint8_t FRAME[10] = {0xA7, 0x5C, 0x13, 0xF0, 0x0F, 0x81, 0x42, 0x99, 0x3C, 0xE1}; + +// The times between edges a receiver sees for a transmitted waveform. Levels +// are added the way Transceiver::sendFrame() drives the TX pin; a run of the +// same level is one pulse. +struct Wave { + std::vector durations; + int level = 0; + uint32_t run = 100000; // Quiet before the transmission. + void add(int lvl, uint32_t us) { + if(lvl == this->level) { this->run += us; return; } + this->durations.push_back(this->run); + this->level = lvl; + this->run = us; + } +}; +// Mirrors Transceiver::sendFrame(): a wakeup pulse on the first frame (sync 2 +// or 12), sync hardware sync pulses, the software sync and the Manchester bits. +static Wave transmit(const uint8_t *frame, uint8_t sync, uint8_t bitLength) { + Wave w; + if(sync == 2 || sync == 12) { + w.add(1, 10920); + w.add(0, 7357); + } + for(uint8_t i = 0; i < sync; i++) { + w.add(1, 4 * SYMBOL); + w.add(0, 4 * SYMBOL); + } + w.add(1, 4850); + w.add(0, SYMBOL); + for(uint8_t i = 0; i < bitLength; i++) { + const bool one = (frame[i / 8] >> (7 - (i % 8))) & 1; + w.add(one ? 0 : 1, SYMBOL); + w.add(one ? 1 : 0, SYMBOL); + } + w.add(0, 27434); // Inter-frame silence. + w.add(1, 0); + return w; +} +// Feeds durations to a fresh decoder until it reports a frame. +static bool receive(const std::vector &durations, somfy_rx_t &rx) { + rx.clear(); + for(uint32_t d : durations) + if(decodePulse(rx, d) == PulseResult::frame) return true; + return false; +} +static void assertFrame(uint8_t sync, uint8_t bitLength) { + somfy_rx_t rx; + TEST_ASSERT_TRUE(receive(transmit(FRAME, sync, bitLength).durations, rx)); + TEST_ASSERT_EQUAL_UINT8(bitLength, rx.bit_length); + TEST_ASSERT_EQUAL_HEX8_ARRAY(FRAME, rx.payload, bitLength / 8); + TEST_ASSERT_EQUAL_UINT8(sync * 2, rx.cpt_synchro_hw); +} + +/********************************************************************* + * whole frames + ********************************************************************/ +static void test_56_bit_first_frame() { assertFrame(2, 56); } +static void test_56_bit_repeat() { assertFrame(7, 56); } +static void test_80_bit_first_frame() { assertFrame(12, 80); } +static void test_80_bit_repeat() { assertFrame(6, 80); } +static void test_records_the_pulses() { + somfy_rx_t rx; + const Wave w = transmit(FRAME, 7, 56); + TEST_ASSERT_TRUE(receive(w.durations, rx)); + // Every pulse from the first hardware sync on is kept for the frame log. + TEST_ASSERT_TRUE(rx.pulseCount > 14 + 56); + TEST_ASSERT_EQUAL_UINT32(4 * SYMBOL, rx.pulses[1]); +} + +/********************************************************************* + * noise + ********************************************************************/ +static void test_glitch_is_reported_and_skipped() { + somfy_rx_t rx; + rx.clear(); + TEST_ASSERT_EQUAL_UINT8(static_cast(PulseResult::glitch), static_cast(decodePulse(rx, 100))); + // The caller times the next edge from the edge before the glitch, so the + // frame still decodes. + const Wave w = transmit(FRAME, 7, 56); + for(uint32_t d : w.durations) { + if(decodePulse(rx, d) == PulseResult::frame) { + TEST_ASSERT_EQUAL_HEX8_ARRAY(FRAME, rx.payload, 7); + return; + } + } + TEST_FAIL_MESSAGE("no frame"); +} +static void test_bad_timing_mid_frame_starts_over() { + somfy_rx_t rx; + std::vector d = transmit(FRAME, 7, 56).durations; + // Corrupt a data pulse: the decoder gives up on this frame. + d[20] = 3 * SYMBOL; + TEST_ASSERT_FALSE(receive(d, rx)); + TEST_ASSERT_EQUAL_UINT8(static_cast(waiting_synchro), static_cast(rx.status)); + // The next frame is received. + std::vector next = transmit(FRAME, 7, 56).durations; + bool got = false; + for(uint32_t p : next) if(decodePulse(rx, p) == PulseResult::frame) { got = true; break; } + TEST_ASSERT_TRUE(got); + TEST_ASSERT_EQUAL_HEX8_ARRAY(FRAME, rx.payload, 7); +} +static void test_too_few_hardware_syncs_is_not_a_frame() { + somfy_rx_t rx; + // One sync pulse is only 2 edges; at least 4 are needed. + TEST_ASSERT_FALSE(receive(transmit(FRAME, 1, 56).durations, rx)); +} + +int main(int argc, char **argv) { + (void)argc; + (void)argv; + UNITY_BEGIN(); + RUN_TEST(test_56_bit_first_frame); + RUN_TEST(test_56_bit_repeat); + RUN_TEST(test_80_bit_first_frame); + RUN_TEST(test_80_bit_repeat); + RUN_TEST(test_records_the_pulses); + RUN_TEST(test_glitch_is_reported_and_skipped); + RUN_TEST(test_bad_timing_mid_frame_starts_over); + RUN_TEST(test_too_few_hardware_syncs_is_not_a_frame); + return UNITY_END(); +}