Describes the protocol that the pool controller uses on the RS-485 bus to communicate with its attached devices.
Key characteristics of this bus are:
- A pool controller initiates a query addressed to a specific device. The device will respond by performing an action and then respond back to the controller.
- It is an half-duplex connection, so only one system can transmit at the time. The controller will send a query and wait for a response, or times out when the device does not respond within a predefined period.
The OPNpool device sniffs this RS-485 bus to gather state information, and presents it to the user. The user can also change select parameters such as the thermostats or start e.g. the pool circuit.
The pool controller communicates with the pump and chlorinator over the RS-485 bus using a proprietary protocol. This protocol has been reverse engineered and documented in bits and pieces found on-line by people such as shown in the table below
Pioneers | Contributors |
---|---|
Joshua Bloch | Russ Goldin (prot; controller). |
Michael Russe | Richard Sears (monitor) |
George Saw |
The controller uses two different protocols to communicate with its devices:
-
“A5” messages are used to communicate with devices such as pumps. The first bytes are always
0x00, 0xFF, 0xA5
-
“IC” messages are used to communicate with the chlorinator. The first bytes are always
0x10, 0x02
A5 messages
The messages consist of a header, data and a 16-bit checksum. The length of the data block is specified in the header. The checksum is calculated starting at the last byte of the header (0xA5) up to the last data byte. Checksum is transmitted most significant byte first.
#define PACK8 __attribute__((aligned( __align__( uint8_t ) ), packed )) typedef struct { uint8_t pro; // protocol version uint8_t dst; // destination uint8_t src; // source uint8_t typ; // message type uint8_t len; // # of data bytes following } PACK8 mHdr_a5_t;
The most significant 4 bits of source and destination address form the address group. The least significant 4 bits are the device number.
typedef enum { ADDRGROUP_all = 0x00, ADDRGROUP_ctrl = 0x01, ADDRGROUP_remote = 0x02, ADDRGROUP_chlor = 0x05, ADDRGROUP_pump = 0x06 } addrGroup_t;
We further distinguish between messages to/from a pump and control messages. Examples of control messages are time, state, head and scheduling requests.
Periodic state message from controller
Message on the RS-485 bus
Decoded message
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x01, .dst = 0x0F, // DATALINK_ADDRGROUP_ALL .src = 0x10, // DATALINK_ADDRGROUP_CTRL .typ = 0x02, // NETWORK_TYP_CTRL_STATE_BCAST .len = 0x1D }; |
Data |
network_msg_ctrl_state_bcast = { .hour = 0x12, // "18:26" .minute = 0x1A, // .activeLo = 0x20, // NETWORK_CIRCUIT_POOL | .. .activeHi = 0x00 // .U04to06[3] = {0x00, 0x00, 0x00} // opt. more `active` circuits .U07to08[2] = {0x00, 0x00}; .mode = 0x00, .heatStatus = 0x00, // false, false .U11 = 0x00, .delay = 0x00, // 0 .U13 = 0x01, .poolTemp = 0x34, // 52 F .spaTemp = 0x34, // 52 F .U16 = 0x02, .solarTemp = 0x50, // .airTemp = 0x37, // 55 F .solarTemp2 = 0xFF, // no-sensor .U20to21[2] = {0x02, 0xD0}, .heatSrc = 0x00, // NETWORK_HEAT_SRC_OFF, NETWORK_HEAT_SRC_OFF .U23to28[6] = {0x01, 0x00, 0x00, 0x00, 0x29 0x0B} } |
Tail |
datalink_tail_a5 = { .crc[2] = {0x04, 0x28} // 0x428 }; |
Controller requests status from pump
Message on the RS-485 bus
Decoded message
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x00, .dst = 0x60, // DATALINK_ADDRGROUP_PUMP .src = 0x10, // DATALINK_ADDRGROUP_CTRL .typ = 0x07, // NETWORK_TYP_PUMP_STATUS .len = 0x00 }; |
Data |
network_msg_pump_status_req = {}; |
Tail |
datalink_tail_a5 = { .crc[2] = {0x01, 0x1C}; // 0x11C }; |
Pump replies with the message
Decoded message
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x00, .dst = 0x10, // DATALINK_ADDRGROUP_CTRL .src = 0x60, // DATALINK_ADDRGROUP_PUMP .typ = 0x07, // NETWORK_TYP_PUMP_STATUS .len = 0x0F }; |
Data |
network_msg_pump_status_resp = { .running = 0x0A, // true .mode = 0x00, // NETWORK_PUMP_MODE_FILTER .status = 0x00, // ok .powerHi = 0x02, // 748 W .powerLo = 0xEC, // .rpmHi = 0x09, // 2500 rpm .rpmLo = 0xC4, // .gpm = 0x00, // unknown G/m .pct = 0x00, // 0 % .U09 = 0x00, .err = 0x00, // no-error .remHr = 0x00, .remMin = 0x01, // 1 .clockHr = 0x12, // 18:26 .clockMin = 0x1A // } |
Tail |
datalink_tail_a5 = { .crc[2] = {0x03, 0x1D} // 0x31D }; |
Controller requests status from chemical
Message on the RS-485 bus
The interface parses it, but doesn’t decodePart | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0x00, 0xFF 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x00, .dst = 0x90, // DATALINK_ADDRGROUP_X09 .src = 0x10, // DATALINK_ADDRGROUP_CTRL .typ = 0xD2, // NETWORK_TYP_CTRL_?? .len = 0x01 }; |
Data |
unknown = { .U00 = 0xD2 } |
Tail |
datalink_tail_a5 = { .crc[2] = {0x02, 0xEA} // 0x2EA }; |
IC messages
The IC messages consist of a header, data and a tail containing a 8-bit checksum and a 2 byte post-amble. The length of the data block depends on the message type specified in the header. The checksum is calculated starting at the beginning of the header up to the last data byte.
#define PACK8 __attribute__((aligned( __alignof__( uint8_t ) ), packed )) typedef struct { uint8_t dst; // destination uint8_t typ; // message type } PACK8 mHdr_ic_t;
Controller asks chlorinator to set percentage
Message on the RS-485 bus
Decoded message
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0x10, 0x02}; |
Header |
datalink_hdr_ic = { .dst = 0x50, // DATALINK_ADDRGROUP_CHLOR .typ = 0x11 // NETWORK_TYP_CHLOR_LEVEL_SET }; |
Data |
network_msg_chlor_level_set { .pct = 0x50 // 80% } |
Tail |
datalink_tail_ic = { .crc[1] = {0xC3}, .post[2] = {0x10, 0x03} } |
The chlorinator replies with
Decoded message
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0x10, 0x02}; |
Header |
datalink_hdr_ic = { .dst = 0x00, // DATALINK_ADDRGROUP_ALL .typ = 0x12 // NETWORK_TYP_CHLOR_LEVEL_RESP }; |
Data |
network_msg_chlor_level_resp { .salt = 0x00, // 0 ppm .err = 0xC0 // POOLSTATE_CHLOR_STATUS_OK } |
Tail |
datalink_tail_ic = { .crc[1] = {0xE4}, .post[2] = {0x10, 0x03} } |
Controller asks chlorinator for model number
Message on the RS-485 bus
Decoded message
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0x10, 0x02}; |
Header |
datalink_hdr_ic = { .dst = 0x50, // DATALINK_ADDRGROUP_CHLOR .typ = 0x14 // NETWORK_TYP_CHLOR_X14 }; |
Data |
unknown = { .U00 = 0x00 } |
Tail |
datalink_tail_ic = { .crc[1] = {}, .post[2] = {0x10, 0x03} } |
The chlorinator replies
Decoded message
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0x10, 0x02}; |
Header |
datalink_hdr_ic = { .dst = 0x00, // DATALINK_ADDRGROUP_ALL .typ = 0x03 // NETWORK_TYP_CHLOR_NAME }; |
Data |
network_msg_chlor_name = { .U00 = 0x00, .name[16] = {0x49, 0x6E, 0x74, 0x65, // Intellichlor--40 0x6C, 0x6C, 0x69, 0x63, 0x68, 0x6C, 0x6F, 0x72, 0x2D, 0x2D, 0x34, 0x30} } |
Tail |
datalink_tail_ic = { .crc[1] = {0xBC}, .post[2] = {0x10, 0x03} } |
Controller pings the chlorinator
Message on the RS-485 bus
Decoded message
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0x10, 0x02}; |
Header |
datalink_hdr_ic = { .dst = 0x50, // DATALINK_ADDRGROUP_CHLOR .typ = 0x00 // NETWORK_TYP_CHLOR_PING_REQ }; |
Data |
network_msg_chlor_ping_req = { .U00 = 0x00 } |
Tail |
datalink_tail_ic = { .crc[1] = {0x62}, .post[2] = {0x10, 0x03} } |
A5 information requests
Request heat settings
Messages on the RS-485 bus
Remote requestsPart | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x01, .dst = 0x10, // DATALINK_ADDRGROUP_CTRL .src = 0x20, // DATALINK_ADDRGROUP_REMOTE .typ = 0xC8, // NETWORK_TYP_CTRL_HEAT_REQ .len = 0x00 }; |
Data |
network_msg_ctrl_heat_req = {} |
Tail |
datalink_tail_a5 = { .crc[2] = {0x01, 0x9E} // 0x19E }; |
Controller replies with
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x01, .dst = 0x0F, // DATALINK_ADDRGROUP_ALL .src = 0x10, // DATALINK_ADDRGROUP_CTRL .typ = 0x08, // NETWORK_TYP_CTRL_HEAT .len = 0x0D }; |
Data |
network_msg_ctrl_heat = { .poolTemp = 0x34, // 52 F .spaTemp = 0x34, // 52 F .airTemp = 0x37, // 55 F .poolTempSetpoint = 0x04, // 4 F .spaTempSetpoint = 0x00, // 0 F .heatSrc = 0x00, // NETWORK_HEAT_SRC_OFF .U06to12[7] = {0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00 } |
Tail |
datalink_tail_a5 = { .crc[2] = {0x02, 0x7C} // 0x27C }; |
Request schedule
Messages on the RS-485 bus
Remote requests
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x01, .dst = 0x10, // DATALINK_ADDRGROUP_CTRL .src = 0x20, // DATALINK_ADDRGROUP_REMOTE .typ = 0xDE, // NETWORK_TYP_CTRL_SCHED_REQ .len = 0x00 }; |
Data |
network_msg_ctrl_sched_req = {} |
Tail |
datalink_tail_a5 = { .crc[2] = {0x01, 0xB4} // 0x1B4 }; |
Controller replies with
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x10, .dst = 0x0F, // DATALINK_ADDRGROUP_ALL .src = 0x10, // DATALINK_ADDRGROUP_CTRL .typ = 0x1E, // NETWORK_TYP_CTRL_SCHED .len = 0x10 }; |
Data |
network_msg_ctrl_sched_resp { .U00to03[4] = {0x01, 0x00, 0x00, 0x0F}, .sched[2] = {{ .circuit = 0x00, // NETWORK_CIRCUIT_SPA .U05 = 0x00, .prgStartHi = 0x00, .prgStartLo = 0x00, .prgStopHi = 0x00, .prgStopLo = 0x00, },{ .circuit = 0x06, // NETWORK_CIRCUIT_POOL .U05 = 0x01, .prgStartHi = 0x01, // 08:00 .prgStartLo = 0xE0, .prgStopHi = 0x02, // 10:00 .prgStopLo = 0x58, } } |
Tail |
datalink_tail_a5 = { .crc[2] = {0x02, 0x45} // 0x245 }; |
Request current time
Messages on the RS-485 bus
Remote requests
Part | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x01, .dst = 0x10, // DATALINK_ADDRGROUP_CTRL .src = 0x20, // DATALINK_ADDRGROUP_REMOTE .typ = 0xC5, // NETWORK_TYP_CTRL_TIME_REQ .len = 0x00 }; |
Data |
network_msg_ctrl_time_req = {} |
Tail |
datalink_tail_a5 = { .crc[2] = {0x01, 0x9B} // 0x19B }; |
Controller replies with
Part | Decoded |
---|---|
Preamble |
datalink_pre = {}; |
Header |
datalink_hdr_a5 = { .ver = 0x01, .dst = 0x0F, // DATALINK_ADDRGROUP_ALL .src = 0x10, // DATALINK_ADDRGROUP_CTRL .typ = 0x05, // NETWORK_TYP_CTRL_TIME .len = 0x08 }; |
Data |
network_msg_ctrl_time { .hour = 0x12, // 19:26 .minute = 0x1A, .U02 = 0x10, .day = 0x18, // 2022-02-24 .month = 0x02, .year = 0x16, .clkSpeed = 0x00, .daylightSavings = 0x00 } |
Tail |
datalink_tail_a5 = { .crc[2] = {0x01, 0x3E} // 0x13E }; |
A5 set requests
Set circuit
Message on the RS-485 bus
Remote set requestsPart | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x01, .dst = 0x10, // DATALINK_ADDRGROUP_CTRL .src = 0x22, // DATALINK_ADDRGROUP_REMOTE (wireless_remote) .typ = 0x86, // NETWORK_TYP_CTRL_CIRCUIT_SET .len = 0x02 }; |
Data |
network_msg_ctrl_circuit_set = { .circuit = 0x06, // 1-based circuit .value = 0x01 }; |
Tail |
datalink_tail_a5 = { .crc[2] = {0x01, 0x67} // 0x167 }; |
Controller replies with
DecodedPart | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x01, .dst = 0x22, // DATALINK_ADDRGROUP_REMOTE (wireless_remote) .src = 0x10, // DATALINK_ADDRGROUP_CTRL .typ = 0x01, // NETWORK_TYP_CTRL_SET_ACK .len = 0x01 }; |
Data |
network_msg_ctrl_set_ack = { .typ = 0x86; // NETWORK_TYP_CTRL_CIRCUIT_SET }; |
Tail |
datalink_tail_a5 = { .crc[2] = {0x01, 0x60} // 0x160 }; |
Set heat
Message on the RS-485 bus
Remote set requestsPart | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x01, .dst = 0x10, // DATALINK_ADDRGROUP_CTRL .src = 0x22, // DATALINK_ADDRGROUP_REMOTE (wireless_remote) .typ = 0x88, // NETWORK_TYP_CTRL_HEAT_SET .len = 0x04 }; |
Data |
struct network_msg_ctrl_heat_set_t { .poolSetpoint = 0x48, // 0 .spaSetpoint = 0x16, // 1 .heatSrc = 0x01, // 2 .UNKNOWN_3 = 0x00 // 3 }; |
Tail |
datalink_tail_a5 = { .crc[2] = {0x01, 0xC3} // 0x1C3 }; |
Controller replies with
DecodedPart | Decoded |
---|---|
Preamble |
datalink_pre = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFF, 0xA5}; |
Header |
datalink_hdr_a5 = { .ver = 0x01, .dst = 0x22, // DATALINK_ADDRGROUP_REMOTE (wireless_remote) .src = 0x10, // DATALINK_ADDRGROUP_CTRL .typ = 0x01, // NETWORK_TYP_CTRL_SET_ACK .len = 0x01 }; |
Data |
network_msg_ctrl_set_ack = { .typ = 0x88; // NETWORK_TYP_CTRL_HEAT_SET } |
Tail |
datalink_tail_a5 = { .crc[2] = {0x01, 0x62} // 0x162 }; |