From 40d1dad30d146b7da43f240dc286b88fc46a7650 Mon Sep 17 00:00:00 2001 From: lawther Date: Fri, 26 Jun 2026 16:03:53 +1000 Subject: [PATCH] Add decoder for CMD 0x0F chlorinator pump mode unicast (issue #23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handles the Viron Chlorinator (0x0084) → Touchscreen (0x0050) unicast that sets current pump mode. Log-only; no pool_state update since the pump announces its own speed independently. --- PROTOCOL.md | 27 +++++++------ main/message_decoder.c | 51 ++++++++++++++++++++++++ test/samples/chlorinator.txt | 20 ++++++++++ test/test_message_decoder.c | 75 ++++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 11 deletions(-) diff --git a/PROTOCOL.md b/PROTOCOL.md index 4545b67..f27d5cf 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -15,7 +15,7 @@ This document describes the proprietary serial protocol used by the Connect 10 p - [0x0A — Firmware Version ✅](#0x0a--firmware-version-) - [0x0B — Channel Status ✅](#0x0b--channel-status-) - [0x0D — Active Channels Bitmask ✅](#0x0d--active-channels-bitmask-) - - [0x0F — Chlorinator Mode → Touchscreen ⚠️](#0x0f--chlorinator-mode--touchscreen-️) + - [0x0F — Chlorinator Set Pump Speed ✅](#0x0f--chlorinator-set-pump-speed-) - [0x10 — Channel Toggle Command ⚠️](#0x10--channel-toggle-command-️) - [0x12 — Device Status ⚠️](#0x12--device-status-️) - [0x14 — Mode (Spa/Pool) ✅](#0x14--mode-spapool-) @@ -124,7 +124,7 @@ Click any CMD in the first column to jump to the full section in [Commands](#com | [`0x0A`](#0x0a--firmware-version-) | Firmware Version | `0x0050`, `0x0062`, `0x0070`, `0x0081`, `0x0084`, `0x00A0`, `0x00F0` → Broadcast | Same `{major, minor}` payload across all sources; dispatched on CMD byte alone | Yes (unified handler) | | [`0x0B`](#0x0b--channel-status-) | Channel Status | `0x0050` → Broadcast | | Yes | | [`0x0D`](#0x0d--active-channels-bitmask-) | Active Channels Bitmask | `0x0050` → `0x006F` Internal Channels | Unicast | Yes | -| [`0x0F`](#0x0f--chlorinator-mode--touchscreen-️) | Chlorinator Mode → Touchscreen | `0x0084` → `0x0050` | 2-byte `[01, mode]`; mirrors [0x18](#0x18--chlorinator-cell-mode-️) cell mode | **No (doc only)** | +| [`0x0F`](#0x0f--chlorinator-set-pump-speed-) | Chlorinator Set Pump Speed | `0x0084` → `0x0050` | The Chlorinator requests the Touch Screen to set pump speed (Off/Auto/Manual/Low/Medium/High) | Yes | | [`0x10`](#0x10--channel-toggle-command-️) | Channel Toggle Command | `0x00F0` Gateway → Broadcast | | Yes | | [`0x12`](#0x12--device-status-️) | Device Status | `0x0050`, `0x0062`, `0x0074`, `0x0081`, `0x0084`, `0x0090`, `0x00F0` → Broadcast | Payload layout differs per source | Yes (per-source) | | [`0x14`](#0x14--mode-spapool-) | Mode (Spa/Pool) | `0x0050` → Broadcast | | Yes | @@ -342,22 +342,27 @@ Reports which channels are currently active. Unicast from the Touchscreen (`0x00 --- -### 0x0F — Chlorinator Mode → Touchscreen ⚠️ +### 0x0F — Chlorinator Set Pump Speed ✅ -Inter-device unicast from the Viron Chlorinator (`0x0084`) to the Touchscreen (`0x0050`) reporting the chlorinator's current mode. Counterpart to the `0x18` cell-mode unicast that the chlorinator sends to the Viron XT Pump (`0x00A0`) — the two messages carry the same mode value and may briefly disagree during transitions. +Inter-device unicast from the Viron Chlorinator (`0x0084`) to the Touchscreen (`0x0050`) requesting setting current pump mode. -**Pattern (provisional):** `02 00 84 00 50 80 00 0F ?? ??` (LENGTH and HDR_CHK to be confirmed from a capture) +**Pattern:** `02 00 84 00 50 80 00 0F 0E 73` -**Data Fields (provisional):** +**Data Fields:** -- Byte 10: Fixed `0x01` in observed captures (purpose unknown) -- Byte 11: Mode value — same encoding as the `0x18` cell-mode broadcast (`0x00`=Off, `0x01`=Auto, `0x02`=On — tentative) +- Byte 10: Always `0x01` (purpose unknown) +- Byte 11: Pump mode/speed value: + - `0x00` = Off + - `0x01` = Auto + - `0x02` = Manual / On + - `0x03` = Low Speed + - `0x04` = Medium Speed + - `0x05` = High Speed **Notes:** -- ⚠️ Documented only — no handler in `message_decoder.c` yet. The layout came from contemporaneous capture analysis but a definitive sample pair has not been pinned down. -- Mode encoding follows the protocol-wide channel-state convention (see [0x0B](#0x0b--channel-status-)). -- The companion `0x18` cell-mode unicast is documented at [0x18](#0x18--chlorinator-cell-mode-️). +- Handled in `message_decoder.c` (`handle_chlor_set_pump_mode`) as a log-only message (no state updates or MQTT publishing since the pump announces its own speed). + --- diff --git a/main/message_decoder.c b/main/message_decoder.c index 3b9c22b..fe46b77 100644 --- a/main/message_decoder.c +++ b/main/message_decoder.c @@ -108,6 +108,10 @@ static const char *MSG_TYPE_ICI_HEATER_TEMP_SETTING = "02 00 74 FF FF 80 00 17 0 // Chlorinator status broadcast (CMD 0x12) — same payload shape from either variant static const char *MSG_TYPE_CHLOR_STATUS_A = "02 00 90 FF FF 80 00 12 0D 2F"; static const char *MSG_TYPE_CHLOR_STATUS_B = "02 00 84 FF FF 80 00 12 0D 23"; + +// Chlorinator unicast to Touch screen (CMD 0x0F) — sets pump speed +static const char *MSG_TYPE_CHLOR_SET_PUMP_MODE = "02 00 84 00 50 80 00 0F 0E 73"; + // VX 11S v3 Chlorinator CMD 0x12 status broadcast (meaning unknown; payload always 0x00 in captures) static const char *MSG_TYPE_VX11S_STATUS = "02 00 81 FF FF 80 00 12 0D 20"; @@ -214,6 +218,7 @@ static const cmd_name_entry_t CMD_NAME_TABLE[] = { {0x0A, "Firmware Version"}, {0x0B, "Channel Status"}, {0x0D, "Active Channels Bitmask"}, + {0x0F, "Chlorinator Set Pump Mode"}, {0x10, "Channel Toggle Cmd"}, {0x12, "Status/Other"}, {0x14, "Mode"}, @@ -2080,6 +2085,47 @@ static bool handle_vx11s_status( return true; } +/** + * Handler: Chlorinator set pump mode unicast + * Pattern: "02 00 84 00 50 80 00 0F 0E 73" + * Log-only — no pool_state update since the touch screen will talk to the pump, + * and the pump will announce its own speed. + * + * 2-byte payload: first byte is fixed 0x01 (purpose unknown), + * second byte is pump mode/speed: 0x00=Off, 0x01=Auto, 0x02=Manual, + * 0x03=Low, 0x04=Medium, 0x05=High. + */ +static bool handle_chlor_set_pump_mode( + const uint8_t *data, int len, + const uint8_t *payload, int payload_len, + const char *addr_info, + message_decoder_context_t *ctx) +{ + if (payload_len != 2) return false; + + // Payload 0 is always 0x01, meaning unknown + // Payload 1 is always <=5, meaning speed + if (payload[0] != 0x01 || payload[1] > 5) { + ESP_LOGW(TAG, "%s Chlorinator set pump mode - UNEXPECTED VALUE: payload=0x%02X 0x%02X (expected 0x01 [0x00-0x05])", addr_info, payload[0], payload[1]); + return true; + } + + uint8_t mode = payload[1]; + const char *mode_name; + switch (mode) { + case 0x00: mode_name = "Off"; break; + case 0x01: mode_name = "Auto"; break; + case 0x02: mode_name = "Manual"; break; + case 0x03: mode_name = "Low"; break; + case 0x04: mode_name = "Medium"; break; + case 0x05: mode_name = "High"; break; + } + + ESP_LOGI(TAG, "%s Chlorinator pump mode - %s (0x%02X)", addr_info, mode_name, mode); + + return true; +} + /** * Handler: Light configuration message * Pattern: "02 00 50 FF FF 80 00 06 0E E4" @@ -3167,6 +3213,11 @@ static bool dispatch_message( return handle_vx11s_status(data, len, payload, payload_len, addr_info, ctx); } + // Chlorinator pump mode unicast (§345, issue #23) + if (match_pattern(data, len, MSG_TYPE_CHLOR_SET_PUMP_MODE)) { + return handle_chlor_set_pump_mode(data, len, payload, payload_len, addr_info, ctx); + } + // Gateway messages if (match_pattern(data, len, MSG_TYPE_SERIAL_NUMBER)) { return handle_serial_number(data, len, payload, payload_len, addr_info, ctx); diff --git a/test/samples/chlorinator.txt b/test/samples/chlorinator.txt index 1700484..ce5f96b 100644 --- a/test/samples/chlorinator.txt +++ b/test/samples/chlorinator.txt @@ -6,3 +6,23 @@ I (23:22:12.179) MSG_DECODER: [VX 11S v3 Salt Chlorinator -> Broadcast] VX 11S v I (23:22:13.177) MSG_DECODER: RX MSG: 02 00 81 FF FF 00 00 13 0B 9F 03 W (23:22:13.178) MSG_DECODER: Unhandled [VX 11S v3 Salt Chlorinator -> Broadcast] CMD=0x13 (Unknown CMD 0x13) LEN=11 payload=[] 02 00 81 FF FF 00 00 13 0B 9F 03 + +# zagnuts +# Off: +I (23:30:18.981) MSG_DECODER: RX MSG: 02 00 84 00 50 80 00 0F 0E 73 01 00 01 03 +I (23:30:18.983) MSG_DECODER: [Viron Chlorinator -> Touch Screen] Chlorinator pump mode - Off (0x00) +# Auto: +I (23:29:20.781) MSG_DECODER: RX MSG: 02 00 84 00 50 80 00 0F 0E 73 01 01 02 03 +I (23:29:20.783) MSG_DECODER: [Viron Chlorinator -> Touch Screen] Chlorinator pump mode - Auto (0x01) +# Manual: +I (23:29:29.661) MSG_DECODER: RX MSG: 02 00 84 00 50 80 00 0F 0E 73 01 02 03 03 +I (23:29:29.664) MSG_DECODER: [Viron Chlorinator -> Touch Screen] Chlorinator pump mode - Manual (0x02) +# Low: +I (23:29:53.041) MSG_DECODER: RX MSG: 02 00 84 00 50 80 00 0F 0E 73 01 03 04 03 +I (23:29:53.043) MSG_DECODER: [Viron Chlorinator -> Touch Screen] Chlorinator pump mode - Low (0x03) +# Medium +I (23:29:43.881) MSG_DECODER: RX MSG: 02 00 84 00 50 80 00 0F 0E 73 01 04 05 03 +I (23:29:43.884) MSG_DECODER: [Viron Chlorinator -> Touch Screen] Chlorinator pump mode - Medium (0x04) +# High +I (23:30:02.621) MSG_DECODER: RX MSG: 02 00 84 00 50 80 00 0F 0E 73 01 05 06 03 +I (23:30:02.623) MSG_DECODER: [Viron Chlorinator -> Touch Screen] Chlorinator pump mode - High (0x05) \ No newline at end of file diff --git a/test/test_message_decoder.c b/test/test_message_decoder.c index 2a0474e..2d19ebd 100644 --- a/test/test_message_decoder.c +++ b/test/test_message_decoder.c @@ -583,6 +583,78 @@ void test_decode_chlor_ph_setpoint(void) TEST_ASSERT(test_pool_state.ph_setpoint == 78, "pH setpoint should be 78 (7.8 pH)"); } +/** + * Test: Chlorinator pump mode unicast + * Real message: 02 00 84 00 50 80 00 0F 0E 73 01 03 04 03 (Low) + * Pump mode = payload[1] = 0x03 + */ +void test_decode_chlor_set_pump_mode(void) +{ + init_test_context(); + + uint8_t msg[] = { + 0x02, 0x00, 0x84, 0x00, 0x50, 0x80, 0x00, + 0x0F, 0x0E, // command 0x0F, length (14) + 0x73, // Header checksum + 0x01, // fixed byte 10 + 0x03, // pump speed: 0x03 = Low + 0x04, // Data checksum (0x01+0x03 = 0x04) + 0x03 + }; + + bool decoded = decode_message(msg, sizeof(msg), &test_ctx); + + TEST_ASSERT(decoded, "Chlorinator pump mode should be decoded"); +} + +/** + * Test: Chlorinator pump mode unicast — unexpected payload[0] + * Real message pattern with payload[0]=0x00 instead of 0x01; should still be + * recognised (decoded=true) and logged at WARN rather than treated as unknown. + */ +void test_decode_chlor_set_pump_mode_bad_byte0(void) +{ + init_test_context(); + + uint8_t msg[] = { + 0x02, 0x00, 0x84, 0x00, 0x50, 0x80, 0x00, + 0x0F, 0x0E, + 0x73, + 0x00, // unexpected: should be 0x01 + 0x03, // Low + 0x03, // Data checksum (0x00+0x03 = 0x03) + 0x03 + }; + + bool decoded = decode_message(msg, sizeof(msg), &test_ctx); + + TEST_ASSERT(decoded, "Chlorinator pump mode with unexpected payload[0] should still be decoded"); +} + +/** + * Test: Chlorinator pump mode unicast — out-of-range mode value + * payload[1]=0x06 is above the known range (0x00–0x05); should still be + * recognised and not counted as an unknown message. + */ +void test_decode_chlor_set_pump_mode_bad_mode(void) +{ + init_test_context(); + + uint8_t msg[] = { + 0x02, 0x00, 0x84, 0x00, 0x50, 0x80, 0x00, + 0x0F, 0x0E, + 0x73, + 0x01, + 0x06, // out-of-range mode + 0x07, // Data checksum (0x01+0x06 = 0x07) + 0x03 + }; + + bool decoded = decode_message(msg, sizeof(msg), &test_ctx); + + TEST_ASSERT(decoded, "Chlorinator pump mode with out-of-range mode should still be decoded"); +} + /** * Test: Chlorinator ORP setpoint * Real message: 02 00 90 FF FF 80 00 1D 0F 3C 02 8A 02 8E 03 @@ -935,6 +1007,9 @@ int main(void) printf("\n--- Chlorinator Tests ---\n"); test_decode_chlor_ph_setpoint(); test_decode_chlor_orp_setpoint(); + test_decode_chlor_set_pump_mode(); + test_decode_chlor_set_pump_mode_bad_byte0(); + test_decode_chlor_set_pump_mode_bad_mode(); // Pump tests printf("\n--- Pump Tests ---\n");