From 254e858a7d9fcaf74d8206db546e1d25210e0050 Mon Sep 17 00:00:00 2001 From: Bartosz Kulis Date: Tue, 7 Jul 2026 19:21:34 +0200 Subject: [PATCH] drivers/apator: decode HCA proprietary payload --- drivers/europe/apator/apator.c | 61 ++++++++++++++++++++++++++++- tests/Makefile | 16 +++++++- tests/test_apator_hca.c | 68 +++++++++++++++++++++++++++++++++ tests/test_link | Bin 28824 -> 0 bytes 4 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 tests/test_apator_hca.c delete mode 100755 tests/test_link diff --git a/drivers/europe/apator/apator.c b/drivers/europe/apator/apator.c index 11e7da8..4b5d87a 100644 --- a/drivers/europe/apator/apator.c +++ b/drivers/europe/apator/apator.c @@ -8,6 +8,8 @@ #include "../../engine/driver.h" +#include + /* amiplus — smart electricity (relabelled by NES, DEV, APT). */ static const WmbusMVT k_mvt_amiplus[] = { {"APA", 0x01, 0x02}, @@ -95,6 +97,63 @@ const WmbusDriver wmbus_drv_apator_elf = { * in the wild: wmbusmeters' apatoreitn driver registers (mfr, 0x08, * 0x04) which is version=0x08, medium=0x04, while older firmware uses * the inverted (0x04, 0x08). Cover both. */ +static uint16_t apator_hca_le16(const uint8_t* p) { + return (uint16_t)p[0] | ((uint16_t)p[1] << 8); +} + +static size_t apator_hca_fallback(const uint8_t* a, size_t len, char* o, size_t cap) { + size_t pos = (size_t)snprintf(o, cap, "Apator HCA\n"); + pos += (size_t)snprintf(o + pos, cap - pos, + "Bytes %u\n" + "Status bad length\n", + (unsigned)len); + + size_t off = 0; + int rows = 0; + while(off < len && rows < 3 && pos + 24 < cap) { + size_t n = len - off; + if(n > 6) n = 6; + pos += (size_t)snprintf(o + pos, cap - pos, "Hex%02u ", (unsigned)off); + for(size_t i = 0; i < n && pos + 3 < cap; i++) { + pos += (size_t)snprintf(o + pos, cap - pos, "%02X", a[off + i]); + } + if(pos + 1 < cap) { + o[pos++] = '\n'; + o[pos] = '\0'; + } + off += n; + rows++; + } + + return pos; +} + +static size_t decode_apator_hca(uint16_t m, uint8_t v, uint8_t med, + const uint8_t* a, size_t len, + char* o, size_t cap) { + (void)m; + (void)v; + (void)med; + + if(len != 15) return apator_hca_fallback(a, len, o, cap); + + const uint16_t units = apator_hca_le16(a + 3); + const uint16_t ref_units = apator_hca_le16(a + 7); + + return (size_t)snprintf(o, cap, + "Apator HCA\n" + "Units %u\n" + "RefUnits %u\n" + "TempC %u\n" + "Marker %02X%02X\n" + "Flags %02X%02X%02X%02X%02X\n", + units, + ref_units, + (unsigned)a[12], + a[9], a[10], + a[5], a[6], a[11], a[13], a[14]); +} + static const WmbusMVT k_mvt_apator_hca[] = { {"APA", 0x04, 0x08}, {"APT", 0x04, 0x08}, @@ -104,7 +163,7 @@ static const WmbusMVT k_mvt_apator_hca[] = { }; const WmbusDriver wmbus_drv_apator_hca = { .id = "apator-hca", .title = "Apator HCA", - .mvt = k_mvt_apator_hca, .decode = NULL, + .mvt = k_mvt_apator_hca, .decode = decode_apator_hca, }; /* Ultrimis water (cold) */ diff --git a/tests/Makefile b/tests/Makefile index 51f7e08..b7440e1 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -20,6 +20,17 @@ IZAR_SRC = test_izar.c \ test_izar: $(IZAR_SRC) $(CC) $(CFLAGS) $(IZAR_SRC) -o $@ +APATOR_HCA_SRC = test_apator_hca.c \ + ../drivers/europe/apator/apator.c \ + ../drivers/engine/driver.c \ + ../protocol/wmbus_link.c \ + ../protocol/wmbus_manuf.c \ + ../protocol/wmbus_medium.c \ + ../protocol/wmbus_app_layer.c + +test_apator_hca: $(APATOR_HCA_SRC) + $(CC) $(CFLAGS) $(APATOR_HCA_SRC) -o $@ + PORTS_SRC = test_ports.c \ ../drivers/europe/bmeters/hydrodigit.c \ ../drivers/europe/engelmann/hydroclima.c \ @@ -36,12 +47,13 @@ PORTS_SRC = test_ports.c \ test_ports: $(PORTS_SRC) $(CC) $(CFLAGS) $(PORTS_SRC) -o $@ -check: test_link test_izar test_ports +check: test_link test_izar test_apator_hca test_ports ./test_link ./test_izar + ./test_apator_hca ./test_ports clean: - rm -f test_link test_izar test_ports + rm -f test_link test_izar test_apator_hca test_ports .PHONY: check clean diff --git a/tests/test_apator_hca.c b/tests/test_apator_hca.c new file mode 100644 index 0000000..e3e4b21 --- /dev/null +++ b/tests/test_apator_hca.c @@ -0,0 +1,68 @@ +#include "../drivers/engine/driver.h" + +#include +#include + +extern const WmbusDriver wmbus_drv_apator_hca; + +const WmbusDriver* const wmbus_engine_registry[] = { &wmbus_drv_apator_hca }; +const size_t wmbus_engine_registry_len = 1; + +#define MANUF(a, b, c) \ + ((uint16_t)((((uint16_t)((a) - '@')) << 10) | \ + (((uint16_t)((b) - '@')) << 5) | \ + ((uint16_t)((c) - '@')))) + +static int expect_contains(const char* name, const char* out, const char* expected) { + if(strstr(out, expected)) return 0; + fprintf(stderr, "%s: missing '%s' in:\n%s\n", name, expected, out); + return 1; +} + +static int run_sample(void) { + const uint8_t apdu[] = { + 0x21, 0x01, 0x00, 0x87, 0x19, 0x00, 0x00, 0x04, + 0x20, 0xE7, 0x34, 0x88, 0x16, 0x08, 0x19, + }; + char out[256]; + int fails = 0; + + wmbus_engine_decode(MANUF('A', 'P', 'A'), 0x04, 0x08, + apdu, sizeof(apdu), out, sizeof(out)); + + fails += expect_contains("sample", out, "Apator HCA\n"); + fails += expect_contains("sample", out, "Units 6535\n"); + fails += expect_contains("sample", out, "RefUnits 8196\n"); + fails += expect_contains("sample", out, "TempC 22\n"); + fails += expect_contains("sample", out, "Marker E734\n"); + fails += expect_contains("sample", out, "Flags 0000880819\n"); + + return fails; +} + +static int run_bad_length(void) { + const uint8_t apdu[] = {0x21, 0x01, 0x00}; + char out[256]; + int fails = 0; + + wmbus_engine_decode(MANUF('A', 'P', 'A'), 0x04, 0x08, + apdu, sizeof(apdu), out, sizeof(out)); + + fails += expect_contains("bad_length", out, "Apator HCA\n"); + fails += expect_contains("bad_length", out, "Bytes 3\n"); + fails += expect_contains("bad_length", out, "Status bad length\n"); + fails += expect_contains("bad_length", out, "Hex00 210100\n"); + + return fails; +} + +int main(void) { + int fails = 0; + + fails += run_sample(); + fails += run_bad_length(); + + if(fails) return 1; + printf("All Apator HCA tests passed.\n"); + return 0; +} diff --git a/tests/test_link b/tests/test_link deleted file mode 100755 index 1ab97ff892d6b22b83a6ac588a62c6e642389d1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28824 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 J$TF~~_*cifbjSbz