Protocol

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 decode
Part 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 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 = 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 requests
Part 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

Decoded
Part 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 requests
Part 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

Decoded
Part 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
};

Continue reading to learn about Deploying the device in the field.

Leave a Reply

Your email address will not be published. Required fields are marked *

 

This site uses Akismet to reduce spam. Learn how your comment data is processed.