refactor: move the RX pulse decoder into PulseDecoder

The decoding of the time between received edges into frame bits moves out
of Transceiver::handleReceive unchanged into decodePulse(), with the RTS
timings and the receive buffer type. It stays in IRAM on the ESP32 and is
now covered by native tests that feed it the edges sendFrame() produces
for 56 and 80-bit first frames and repeats, a glitch and a broken pulse.

handleReceive keeps the noise guard, the edge timing and the RX queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-13 21:21:05 +02:00
parent c75b715b3b
commit a176017960
6 changed files with 306 additions and 138 deletions

119
src/PulseDecoder.cpp Normal file
View file

@ -0,0 +1,119 @@
#include "PulseDecoder.h"
#if defined(ESP32)
#include <esp_attr.h>
#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;
}

53
src/PulseDecoder.h Normal file
View file

@ -0,0 +1,53 @@
#ifndef PULSE_DECODER_H
#define PULSE_DECODER_H
#include <stdint.h>
#include <string.h>
// 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

View file

@ -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

View file

@ -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.