Skip to content
Merged
Show file tree
Hide file tree
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
27 changes: 16 additions & 11 deletions PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-)
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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).


---

Expand Down
51 changes: 51 additions & 0 deletions main/message_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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);
Expand Down
20 changes: 20 additions & 0 deletions test/samples/chlorinator.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
75 changes: 75 additions & 0 deletions test/test_message_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down