Skip to content

fs20: Improve bitstream decoding, add some FHT support #1783

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 10, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 163 additions & 22 deletions src/devices/fs20.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,53 @@ Command extensions are also not decoded. feel free to improve!

#include "decoder.h"

static int fs20_find_preamble(bitbuffer_t *bitbuffer, int bitpos)
{
// Preamble is 12 x '0' | '1', but we ignore the first preamble bit
// Last bit ('1') is at position (pattern[1] >> 4 & 1)
uint8_t const preamble_pattern[2] = {0x00, 0x10};
uint8_t const min_packet_length = 4 * (8 + 1);

// fast scan for 8 consecutive '0' bits
uint8_t *bits = bitbuffer->bb[0];
while ((bitpos + 12 + min_packet_length < bitbuffer->bits_per_row[0])
&& ((bits[(bitpos / 8) + 1] == 0) || (bits[(bitpos / 8)] != 0))) {
bitpos += 8;
}
if (bitpos) {
bitpos--;
bitpos &= ~0x3;
}

while ((bitpos = bitbuffer_search(bitbuffer, 0, bitpos, preamble_pattern, 12)) < bitbuffer->bits_per_row[0]) {
if (bitpos + min_packet_length >= bitbuffer->bits_per_row[0])
return DECODE_ABORT_LENGTH;

return bitpos + 12;
}

// preamble not found
return DECODE_FAIL_SANITY;
}

struct parity_byte {
uint8_t data;
uint8_t err;
};

static struct parity_byte get_byte(uint8_t* bits, unsigned pos)
{
uint16_t word = (bits[pos / 8] << 8) | bits[(pos / 8) + 1];
struct parity_byte res;

word <<= pos & 7;
res.data = word >> 8;
// parity8 returns odd parity, bit 9 is even parity
res.err = parity8(res.data) != (word >> 7 & 1);

return res;
}

static int fs20_decode(r_device *decoder, bitbuffer_t *bitbuffer)
{
static char const *cmd_tab[] = {
Expand Down Expand Up @@ -65,35 +112,125 @@ static int fs20_decode(r_device *decoder, bitbuffer_t *bitbuffer)
"unused",
"unused",
};
static char const *flags_tab[8] = {
"(none)",
"Extended",
"BiDir",
"Extended | BiDir",
"Response",
"Response | Extended",
"Response | BiDir",
"Response | Extended | BiDir",
};
static char const *fht_cmd_tab[16] = {
"end-of-sync",
"valve open",
"valve close",
"? (0x3)",
"? (0x4)",
"? (0x5)",
"valve open <ext>%",
"? (0x7)",
"offset adjust",
"? (0x9)",
"valve de-scale",
"? (0x11)",
"sync countdown",
"? (0x13)",
"beep",
"pairing?",
};
static char const *fht_flags_tab[8] = {
"(none)",
"Extended",
"BS?",
"Extended | BS?",
"Repeat",
"Repeat | Extended",
"Repeat | BS?",
"Repeat | Extended | BS?",
};

uint8_t *b; // bits of a row
bitbuffer_invert(bitbuffer);

uint8_t *bits = bitbuffer->bb[0];
uint8_t cmd;
uint8_t hc1;
uint8_t hc2;
uint16_t hc;
uint8_t address;
uint8_t ext = 0;
uint8_t sum;

data_t *data;
uint16_t ad_b4 = 0;
uint32_t hc_b4 = 0;

// check length of frame
if (bitbuffer->bits_per_row[0] != 58) {
// check extended length (never tested!)
if (bitbuffer->bits_per_row[0] != 67)
return DECODE_ABORT_LENGTH;
int rc = DECODE_FAIL_MIC;
int bitpos = 0;

while ((bitpos = fs20_find_preamble(bitbuffer, bitpos)) >= 0) {
if (decoder->verbose) {
fprintf(stderr, "Found preamble at %d\n", bitpos);
}

struct parity_byte res;

res = get_byte(bits, bitpos);
if (res.err)
continue;
hc = res.data << 8;

res = get_byte(bits, bitpos + 9);
if (res.err)
continue;
hc |= res.data;

res = get_byte(bits, bitpos + 18);
if (res.err)
continue;
address = res.data;

res = get_byte(bits, bitpos + 27);
if (res.err)
continue;
cmd = res.data;

res = get_byte(bits, bitpos + 36);
if (res.err)
continue;

if (cmd & 0x20) {
ext = res.data;
if (bitpos + 45 + 9 > bitbuffer->bits_per_row[0])
break;

res = get_byte(bits, bitpos + 45);
if (res.err)
continue;
}
sum = res.data;

rc = 1;
break;
}

b = bitbuffer->bb[0];
// check preamble first 13 bits '0000 0000 0000 1'
if ((b[0] != 0x00) || ((b[1] & 0xf8) != 0x08))
return DECODE_FAIL_SANITY;
// propagate MIC
if (rc <= 0)
return rc;

if (bitpos < 0)
return bitpos;

// Sum is (HC1 + HC2 + Addr + Cmd [+ Ext] + Type + Repeater-Hopcount
// Type is either 6 for regular FS20 devices (switches, dimmers, ...)
// or 0xC for FHT (radiator valves)
sum -= hc >> 8;
sum -= hc & 0xff;
sum -= address;
sum -= cmd;
sum -= ext;

// parse values from buffer
hc1 = (b[1] << 5) | (b[2] >> 3);
hc2 = (b[2] << 6) | (b[3] >> 2);
hc = hc1 << 8 | hc2;
address = (b[3] << 7) | (b[4] >> 1);
cmd = b[5] & 0x1f;
if ((sum < 6) || (sum > 0xC + 2))
return DECODE_FAIL_SANITY;

// convert address to fs20 format (base4+1)
for (uint8_t i = 0; i < 4; i++) {
Expand All @@ -109,10 +246,12 @@ static int fs20_decode(r_device *decoder, bitbuffer_t *bitbuffer)

/* clang-format off */
data = data_make(
"model", "", DATA_STRING, "FS20",
"model", "", DATA_STRING, (sum < 0xc) ? "FS20" : "FHT",
"housecode", "", DATA_FORMAT, "%x", DATA_INT, hc_b4,
"address", "", DATA_FORMAT, "%x", DATA_INT, ad_b4,
"command", "", DATA_STRING, cmd_tab[cmd],
"command", "", DATA_STRING, (sum < 0xc) ? cmd_tab[cmd & 0x1f] : fht_cmd_tab[cmd & 0xf],
"flags", "", DATA_STRING, (sum < 0xc) ? flags_tab[cmd >> 5] : fht_flags_tab[cmd >> 5],
"ext", "", DATA_FORMAT, "%x", DATA_INT, ext,
NULL);
/* clang-format on */
decoder_output_data(decoder, data);
Expand All @@ -125,16 +264,18 @@ static char *output_fields[] = {
"housecode",
"address",
"command",
"flags",
"ext",
NULL,
};

r_device fs20 = {
.name = "FS20",
.modulation = OOK_PULSE_PPM,
.modulation = OOK_PULSE_PWM,
.short_width = 400,
.long_width = 600,
.reset_limit = 9000,
.decode_fn = &fs20_decode,
.disabled = 1, // missing MIC and no sample data
.disabled = 0,
.fields = output_fields,
};