This document describes the Jandy RS485 serial communication protocol used by Aqualink control systems to communicate with various pool and spa equipment on the RS485 network. This protocol is reverse-engineered from the AqualinkD project's source code analysis.
The RS485 protocol is used by the following Jandy/Pentair systems:
- Aqualink RS8, RS12, RS16 (and variants)
- Aquapure (SWG - Salt Water Chlorinator) systems
- Jandy ePumps (variable speed pumps)
- JXi and LX Heaters
- Chemical feeder and analyzer systems
- Heat pumps
- Color control lights
- iAqualinkTouch panels
- OneTouch keypads
- AllButton simulators
- Remote control adapters
- Baud Rate: 9600 bps
- Data Bits: 8
- Stop Bits: 1
- Parity: None
- Flow Control: None (hardware or software)
- Serial Standard: RS485
- Bus Topology: Multi-drop (up to multiple devices on single RS485 bus)
From aq_serial.c:
// Open with non-blocking mode and no controlling terminal
_RS485_fds = open(port, O_RDWR | O_NOCTTY | O_NONBLOCK | O_CLOEXEC);
// Configure for 8N1, 9600 baud
cfsetispeed(&tty, B9600);
cfsetospeed(&tty, B9600);
tty.c_cflag &= ~PARENB; // No parity
tty.c_cflag &= ~CSTOPB; // 1 stop bit
tty.c_cflag |= CS8; // 8 data bits
tty.c_cflag &= ~CRTSCTS; // No hardware flow control- Non-blocking I/O: Used to prevent blocking on Data Carrier Detect (DCD)
- Raw Mode: No canonical processing, no echo
- Read Timeout: Configurable via
VMIN=0andVTIME=10(1 second timeout) - Low Latency Mode: Can be set via
ASYNC_LOW_LATENCYioctl for FTDI adapters
The AqualinkD system supports two main protocols:
- Jandy Protocol (DLE/STX/ETX based)
- Pentair Protocol (PP1/PP2/PP3/PP4 header based)
This document focuses on the Jandy Protocol.
The Jandy protocol uses a simple framing schema with delimiters and checksums:
[DLE] [STX] [DEST] [CMD] [DATA...] [CHECKSUM] [DLE] [ETX]
1 2 3 4 5-N N+1 N+2 N+3
Where:
- DLE: Data Link Escape (0x10) - frame start marker
- STX: Start of Text (0x02) - indicates start of frame
- DEST: Destination device ID (0x00 = master/controller)
- CMD: Command byte indicating packet type
- DATA: Variable length payload (0 to many bytes)
- CHECKSUM: Single byte checksum (sum of DEST through last DATA byte, masked to 8 bits)
- DLE: Data Link Escape (0x10) - frame end marker
- ETX: End of Text (0x03) - indicates end of frame
The Pentair protocol uses a different frame structure:
[PP1] [PP2] [PP3] [PP4] [FROM] [DEST] [CMD] [LENGTH] [DATA...] [CHKSUM_HI] [CHKSUM_LO]
1 2 3 4 5 6 7 8 9-N N+1 N+2
Where:
- NUL: Null byte (0x00) - padding
- PP1: Protocol Marker 1 (0xFF) - Pentair frame start
- PP2: Protocol Marker 2 (0x00) - part of header
- PP3: Protocol Marker 3 (0xFF) - part of header
- PP4: Protocol Marker 4 (0xA5) - completes 4-byte header
- FROM: Source device ID
- DEST: Destination device ID (0x10 = master)
- CMD: Command byte (0x01=Speed, 0x04=RemoteCtl, 0x06=Power, 0x07=Status)
- LENGTH: Data length (number of data bytes following)
- DATA: Variable length payload
- CHKSUM_HI: 16-bit checksum high byte (sum of CMD through last DATA byte)
- CHKSUM_LO: 16-bit checksum low byte
The Pentair protocol is used primarily by Pentair IntelliFlo and variable speed pumps (device IDs 0x60-0x6F).
If the value 0x10 (DLE) appears in the data portion of the packet (after STX and before the final DLE), it must be escaped by inserting a NUL byte (0x00) after it. The parser must skip these escape sequences.
| Constant | Value | Name |
|---|---|---|
| DLE | 0x10 | Data Link Escape |
| STX | 0x02 | Start of Text |
| ETX | 0x03 | End of Text |
Within the data portion (after STX):
| Offset | Field | Description |
|---|---|---|
| 0-1 | Header | DLE (0x10) + STX (0x02) when counting from packet start |
| 2 | PKT_DEST | Destination device ID (0x00 = master) |
| 3 | PKT_CMD | Command type |
| 4+ | PKT_DATA | Command-specific data |
| Device | ID Range | Hex | Notes |
|---|---|---|---|
| Master/Controller | 0x00 | 0x00 | Destination for all ACKs and responses |
| Device | ID Range | Decimal | Hex | Count | Notes |
|---|---|---|---|---|---|
| AllButton | 0x08-0x0B | 8-11 | 0x08-0x0B | 4 | Emulated keypad |
| OneTouch | 0x40-0x43 | 64-67 | 0x40-0x43 | 4 | Physical keypad |
| AqualinkTouch (iAqlnk Touch) | 0x30-0x33 | 48-51 | 0x30-0x33 | 4 | Touch panel |
| iAqualink2 (Jandy Link) | 0xA0-0xA3 | 160-163 | 0xA0-0xA3 | 4 | Cloud interface |
| PDA (AquaPalm) | 0x60-0x63 | 96-99 | 0x60-0x63 | 4 | Handheld remote |
| RS Serial Adapter | 0x48-0x49 | 72-73 | 0x48-0x49 | 2 | Serial gateway |
| Device | ID Range | Decimal | Hex | Count | Notes |
|---|---|---|---|---|---|
| Aquapure SWG | 0x50-0x53 | 80-83 | 0x50-0x53 | 4 | Salt chlorinator |
| LX Heater | 0x38-0x3B | 56-59 | 0x38-0x3B | 4 | Heating system |
| JXi Heater | 0x68-0x6B | 104-107 | 0x68-0x6B | 4 | Jandy JXi heater |
| ePump Standard | 0x78-0x7B | 120-123 | 0x78-0x7B | 4 | Variable speed pump (standard range) |
| ePump Extended | 0xE0-0xE3 | 224-227 | 0xE0-0xE3 | 4 | Variable speed pump (extended for panel rev W+) |
| Heat Pump | 0x70-0x73 | 112-115 | 0x70-0x73 | 4 | Heat pump device |
| Chemistry Feeder | 0x80-0x83 | 128-131 | 0x80-0x83 | 4 | Chemical feeder (ChemLink) |
| Chemistry Analyzer | 0x84-0x87 | 132-135 | 0x84-0x87 | 4 | Chemical analyzer (TrueSense, guess) |
| Jandy Lights | 0xF0-0xF4 | 240-244 | 0xF0-0xF4 | 5 | Colored light control |
| Spa Remote | 0x20-0x23 | 32-35 | 0x20-0x23 | 4 | Remote control for spa |
| Remote Power Center | 0x28-0x2B | 40-43 | 0x28-0x2B | 4 | Remote power management |
| PC Dock | 0x58-0x5B | 88-91 | 0x58-0x5B | 4 | PC docking station |
| Command | Value | Name | Direction | Description |
|---|---|---|---|---|
| CMD_PROBE | 0x00 | Probe | To Device | Polling/probe message |
| CMD_ACK | 0x01 | Acknowledge | From Device | Acknowledgment response |
| CMD_STATUS | 0x02 | Status | Bidirectional | Status information (display panels) |
| CMD_MSG | 0x03 | Message | To Device | Display message (16 bytes) |
| CMD_MSG_LONG | 0x04 | Long Message | To Device | Display message (128 bytes) |
| CMD_MSG_LOOP_ST | 0x08 | Message Loop Start | From Device | Start message loop cycle |
| Constant | Value | Description |
|---|---|---|
| ACK_NORMAL | 0x80 | Normal ACK response |
| ACK_SCREEN_BUSY_SCROLL | 0x81 | Screen busy, cache next message |
| ACK_SCREEN_BUSY_BLOCK | 0x83 | Screen busy, don't send more |
Panel compatibility notes:
- Some keypads use 0x00, others 0x80 (version/implementation dependent)
- Using 0x80 for ACK may trigger CMD_MSG_LOOP_ST cycle
| Command | Value | Name | Direction | Description |
|---|---|---|---|---|
| CMD_PERCENT | 0x11 | Set SWG % | To SWG | Set chlorine generation percentage (0-100, >100 = boost) |
| CMD_PPM | 0x16 | PPM/Status | From SWG | Return PPM and device status |
Example: Set SWG to 75%:
[0x10, 0x02, 0x50, 0x11, 0x4B, checksum, 0x10, 0x03]
↑ ↑ ↑
SWG %SET 75 decimal
| Command | Value | Name | Direction | Description |
|---|---|---|---|---|
| CMD_EPUMP_STATUS | 0x1F | Status Request | Bidirectional | Get/Set pump status (RPM, Watts, GPM) |
| CMD_EPUMP_RPM | 0x44 | Set RPM | To Pump | Set pump speed (RPM mode) |
| CMD_EPUMP_WATTS | 0x45 | Set Watts | To Pump | Set pump power (Watts mode) |
Response format from ePump (0x1F):
CMD=0x1F | Next CMD | Unused | Unused | Hi_Watts | Lo_Watts | Hi_RPM | Lo_RPM | ...
Bytes: 4 5 6 7 8 9 10 11
Calculation examples:
- Watts = (Byte8 × 256) + Byte7
- RPM = (Byte10 × 256) + Byte11
| Command | Value | Name | Description |
|---|---|---|---|
| CMD_JXI_PING | 0x0C | Ping | Poll heater status |
| CMD_JXI_STATUS | 0x0D | Status | Return heater status |
Similar to JXi but with different device ID range (0x38-0x3B).
| Command | Value | Name | Description |
|---|---|---|---|
| CMD_IAQ_PAGE_START | 0x23 | Page Start | Begin new menu page |
| CMD_IAQ_PAGE_BUTTON | 0x24 | Button | Button definition for current page |
| CMD_IAQ_PAGE_MSG | 0x25 | Page Message | Message content for page |
| CMD_IAQ_TABLE_MSG | 0x26 | Table Message | Table data populate command |
| CMD_IAQ_PAGE_END | 0x28 | Page End | End of page definition |
| CMD_IAQ_STARTUP | 0x29 | Startup | Startup message |
| CMD_IAQ_POLL | 0x30 | Poll | Ready to receive commands |
| CMD_IAQ_CTRL_READY | 0x31 | Control Ready | Ready for big control command |
| CMD_IAQ_PAGE_CONTINUE | 0x40 | Page Continue | Multiple pages continue cycle |
| CMD_IAQ_MSG_LONG | 0x2C | Long Message | Display popup message |
| CMD_IAQ_MAIN_STATUS | 0x70 | Main Status | Main screen status (large, 120+ bytes) |
| CMD_IAQ_1TOUCH_STATUS | 0x71 | OneTouch Status | OneTouch emulation status |
| CMD_IAQ_AUX_STATUS | 0x72 | Auxiliary Status | Auxiliary equipment status (large) |
| CMD_IAQ_CMD_READY | 0x73 | Command Ready | Ready to receive command |
| CMD_IAQ_TITLE_MESSAGE | 0x2D | Title Message | Product name/title message |
These define different screen pages in the iAqualinkTouch interface:
| Page | ID | Purpose |
|---|---|---|
| Home | 0x01 | Home screen |
| Status | 0x5B | Status screen (or 0x2A alternate) |
| Devices | 0x36 | Device control (or 0x35, 0x51 alternates) |
| Set Temperature | 0x39 | Temperature setting |
| Menu | 0x0F | Main menu |
| Set VSP | 0x1E | Variable speed pump setup |
| Set Time | 0x4B | Time setting |
| Set Date | 0x4E | Date setting |
| Set SWG | 0x30 | SWG setup |
| Set Boost | 0x1D | Chlorine boost setting |
| Set Quick Boost | 0x3F | Quick boost setting |
| OneTouch | 0x4D | OneTouch page |
| Color Light | 0x48 | Color light control |
| System Setup | 0x14 | System setup (or 0x49, 0x4A alternates) |
| VSP Setup | 0x2D | VSP detailed setup |
| Freeze Protect | 0x11 | Freeze protection |
| Label Aux | 0x32 | Auxiliary labeling |
| Help | 0x0C | Help screen |
| Service Mode | 0x5E | Service/timeout mode |
| Command | Value | Name |
|---|---|---|
| CMD_PDA_0x04 | 0x04 | Unknown (menu building?) |
| CMD_PDA_0x05 | 0x05 | Unknown |
| CMD_PDA_0x1B | 0x1B | Unknown |
| CMD_PDA_HIGHLIGHT | 0x08 | Highlight line |
| CMD_PDA_CLEAR | 0x09 | Clear display |
| CMD_PDA_SHIFTLINES | 0x0F | Shift display lines |
| CMD_PDA_HIGHLIGHTCHARS | 0x10 | Highlight characters |
| Command | Value | Name | Description |
|---|---|---|---|
| RSSA_DEV_STATUS | 0x13 | Device Status | Status or error response |
| RSSA_DEV_READY | 0x07 | Device Ready | Ready to receive command |
The checksum is calculated by summing all bytes from DEST through the last DATA byte, then masking to 8 bits.
int generate_checksum(unsigned char* packet, int length)
{
int i, sum, n;
n = length - 3; // Exclude DLE, ETX, and trailing NUL
sum = 0;
for (i = 0; i < n; i++)
sum += (int) packet[i];
return (sum & 0x0FF);
}The checksum is placed at packet[length-3] (before the final DLE).
For packet: [DLE, STX, 0x50, 0x11, 0x4B, CHKSUM, DLE, ETX]
sum = 0x50 + 0x11 + 0x4B = 0xAC
checksum = 0xAC & 0xFF = 0xAC
There's a known bug in the Jandy OneTouch protocol where long messages (0x0a command) to OneTouch devices have incorrect checksums. The code detects this pattern:
- packet[3] == 0x04
- packet[4] == 0x03
- packet[length-3] == 0x0a
In this case, the checksum is ignored (forced valid) with a warning log.
bool check_jandy_checksum(unsigned char* packet, int length)
{
if (generate_checksum(packet, length) == packet[length-3])
return true;
// Known bug workaround
if (packet[3] == 0x04 && packet[4] == 0x03 && packet[length-3] == 0x0a)
return true; // Forced valid with LOG warning
return false;
}The Aquapure Salt Water Chlorinator communicates via the Jandy protocol to report its status and receive percentage/power commands.
Up to 4 Aquapure units can be independently controlled (though AqualinkD only supports one in v1.0).
When the master sends a query to the SWG, the SWG responds with its operational status.
| Status | Value | Name | Description |
|---|---|---|---|
| SWG_STATUS_ON | 0x00 | Normal Operation | Generating chlorine |
| SWG_STATUS_NO_FLOW | 0x01 | No Flow | Water not flowing through cell |
| SWG_STATUS_LOW_SALT | 0x02 | Low Salt | Salt level too low |
| SWG_STATUS_HI_SALT | 0x04 | High Salt | Salt level too high |
| SWG_STATUS_CLEAN_CELL | 0x08 | Clean Cell | Cell cleaning cycle active |
| SWG_STATUS_TURNING_OFF | 0x09 | Turning Off | Shutdown in progress |
| SWG_STATUS_HIGH_CURRENT | 0x10 | High Current | Excessive current draw |
| SWG_STATUS_LOW_VOLTS | 0x20 | Low Voltage | Insufficient voltage |
| SWG_STATUS_LOW_TEMP | 0x40 | Low Temperature | Water temperature too low |
| SWG_STATUS_CHECK_PCB | 0x80 | Check PCB | PCB/control board error |
| SWG_STATUS_GENFAULT | 0xFD | General Fault | General fault state |
| SWG_STATUS_UNKNOWN | 0xFE | Unknown | No status received |
| SWG_STATUS_OFF | 0xFF | Off | Device off (AqualinkD internal state) |
Query SWG Status:
Master → SWG: [DLE, STX, 0x50, 0x02, CHKSUM, DLE, ETX]
↑ ↑
SWG CMD_STATUS
SWG Response:
SWG → Master: [DLE, STX, 0x00, 0x16, PPM_VAL, STATUS, CHKSUM, DLE, ETX]
↑ ↑ ↑ ↑
Master CMD_PPM PPM*100 Status byte
Where:
- PPM_VAL: Division factor for PPM (actual PPM = PPM_VAL × 100)
- STATUS: One of the status values above
- Example:
[DLE, STX, 0x00, 0x16, 0x0C, 0x00, CHKSUM, DLE, ETX]means 1200 PPM, On
Set SWG Percentage:
Master → SWG: [DLE, STX, 0x50, 0x11, PERCENT, CHKSUM, DLE, ETX]
↑ ↑ ↑
SWG %SET 0-100 (>100 = boost mode)
Example: Set to 75%:
[DLE, STX, 0x50, 0x11, 0x4B, CHKSUM, DLE, ETX] (0x4B = 75 decimal)
Boost Mode:
Master → SWG: [DLE, STX, 0x50, 0x11, 0x65, CHKSUM, DLE, ETX] (0x65 = 101 decimal)
From actual systems (from protocol notes):
AR %% | HEX: 0x10|0x02|0x50|0x11|0xff|0x72|0x10|0x03|
In service/timeout: Set to 0xFF (all on in special mode)
SWG response: 0x10|0x02|0x00|0x16|0x0C|0x00|0x1E|0x10|0x03|
Status = 0x00 (on), PPM = 0x0C (1200 PPM)
Jandy ePumps are variable-speed DC or AC motors that can operate in RPM or Watts mode. They communicate via the RS485 bus to report their current operational status and receive speed/power commands.
- Standard Range: 0x78-0x7B (120-123 decimal)
- Extended Range: 0xE0-0xE3 (224-227 decimal) - Panel revision W and later
Command: CMD_EPUMP_WATTS (0x45)
Master → Pump: [DLE, STX, 0x78, 0x45, 0x00, HI_WATTS, LO_WATTS, CHKSUM, DLE, ETX]
↑ ↑ ↑ ↑
Pump WATTS_CMD Reserved Watts value (16-bit)
Pump → Master: [DLE, STX, 0x00, 0x1F, 0x45, 0x00, HI_WATTS, LO_WATTS, ..., CHKSUM, DLE, ETX]
↑ ↑ ↑
Master STATUS (0x1F) Original CMD echoed
Watts Calculation: watts = (packet[8] × 256) + packet[7]
Example: Set to 1309 Watts
1309 = 0x51D
Hi_byte = 0x05 (1309 >> 8)
Lo_byte = 0x1D (1309 & 0xFF)
Packet: [DLE, STX, 0x78, 0x45, 0x00, 0x05, 0x1D, CHKSUM, DLE, ETX]
Command: CMD_EPUMP_RPM (0x44)
Similar format to Watts but for RPM control:
Master → Pump: [DLE, STX, 0x78, 0x44, 0x00, HI_RPM, LO_RPM, CHKSUM, DLE, ETX]
Pump → Master: [DLE, STX, 0x00, 0x1F, 0x44, 0x00, HI_RPM, LO_RPM, ..., CHKSUM, DLE, ETX]
RPM Calculation: rpm = (packet[6] × 256) + packet[7]
When pump responds with status (0x1F), the full packet contains:
| Offset | Field | Description |
|---|---|---|
| 0-1 | Header | DLE + STX |
| 2 | DEST | 0x00 (Master) |
| 3 | CMD | 0x1F (Status) |
| 4 | Orig_CMD | Echo of original command (0x44, 0x45, etc.) |
| 5 | Reserved | 0x00 |
| 6-7 | WATTS_HI/LO | Current watts (16-bit) |
| 8-9 | RPM_HI/LO | Current RPM (16-bit) |
| 10+ | Other fields | Pressure, temperature, etc. (device dependent) |
Device IDs: 0x68-0x6B (104-107 decimal)
| Command | Value | Description |
|---|---|---|
| CMD_JXI_PING | 0x0C | Ping/poll heater |
| CMD_JXI_STATUS | 0x0D | Status response |
From status packets (CMD_JXI_STATUS):
- Byte 6 == 0x10: Error condition
- Other values: Normal operational status
// From protocol notes in devices_jandy.c
"LXi status | HEX: 0x10|0x02|0x00|0x0d|0x00|0x00|0x00|0x1f|0x10|0x03|"
"LXi status | HEX: 0x10|0x02|0x00|0x0a|0x00|0x00|0x00|0x1f|0x10|0x03|"
↑ ERROR if 0x10Device IDs: 0x38-0x3B (56-59 decimal)
Similar protocol to JXi but with different device ID range.
Device IDs: 0x80-0x83 (128-131 decimal)
Chemistry feeders communicate to dispense specific chemicals or adjust chemical dosing levels.
Device IDs: 0x84-0x87 (132-135 decimal) [GUESS - not fully documented in code]
These devices report chemical levels (pH, ORP, salt levels, etc.) back to the control system.
Device IDs: 0x70-0x73 (112-115 decimal)
Heat pumps can operate in heating or cooling mode and report their operational state (on/off, heating/cooling, error state, etc.) via the RS485 protocol.
Device IDs: 0xF0-0xF4 (240-244 decimal)
Color-changing lights can be controlled to display different colors and brightness levels via RS485 commands.
The iAqualinkTouch is a more advanced control panel with a graphical display and touch interface, communicating via the Jandy RS485 protocol but with more complex message structures for GUI rendering.
- Paged Menus: Screens are defined with multiple commands
- Large Status Messages: Status packets can be 120+ bytes (up to full packet buffer)
- Button Definitions: Dynamic button layout per page
- Table Messages: Structured data (equipment lists, parameters, etc.)
Master → Touch: PAGE_START (0x23) - Begin new page
Master → Touch: PAGE_BUTTON (0x24) - Define button
Master → Touch: PAGE_MSG (0x25) - Add text/content
Master → Touch: PAGE_CONTINUE (0x40) - More pages follow
Master → Touch: PAGE_END (0x28) - Page complete
Large packet (typically 100+ bytes) containing complete system status suitable for main equipment view.
Status suitable for OneTouch emulation mode.
Status for auxiliary equipment (pumps, heaters, etc.).
Navigation Keys:
| Key | Value | Function |
|---|---|---|
| HOME | 0x01 | Home page |
| MENU | 0x02 | Menu |
| ONETOUCH | 0x03 | OneTouch |
| HELP | 0x04 | Help |
| BACK | 0x05 | Back |
| STATUS | 0x06 | Status |
| PREV_PAGE | 0x20 | Previous page |
| NEXT_PAGE | 0x21 | Next page |
Grid Keys (Numbered 0x11-0x1F):
The screen has a 3×5 grid of buttons:
Button Layout:
Column 1 Column 2 Column 3
Row 1: 0x11 Row 1: 0x16 Row 1: 0x1B (KEY01-KEY05 = Column 1)
Row 2: 0x12 Row 2: 0x17 Row 2: 0x1C (KEY06-KEY10 = Column 2)
Row 3: 0x13 Row 3: 0x18 Row 3: 0x1D (KEY11-KEY15 = Column 3)
Row 4: 0x14 Row 4: 0x19 Row 4: 0x1E
Row 5: 0x15 Row 5: 0x1A Row 5: 0x1F
After receiving a command, devices must respond with an ACK to indicate successful receipt.
| Type | Value | Purpose |
|---|---|---|
| ACK_NORMAL | 0x80 | Normal acknowledgment |
| ACK_SCREEN_BUSY_SCROLL | 0x81 | Screen busy but can cache next message |
| ACK_SCREEN_BUSY_BLOCK | 0x83 | Screen busy, stop sending |
// From aq_serial.c - _send_ack()
unsigned char ackPacket[] = { DLE, STX, DEV_MASTER, CMD_ACK, ack_type, command, checksum, DLE, ETX };Example - Send normal ACK:
[DLE, STX, 0x00, CMD_ACK, 0x80, 0x00, CHKSUM, DLE, ETX]
↑ ↑
ACK_NORMAL No command
If the command being acknowledged is DLE (0x10), the packet must be escaped:
Original: [DLE, STX, 0x00, CMD_ACK, 0x80, 0x10, CHKSUM, DLE, ETX]
Escaped : [DLE, STX, 0x00, CMD_ACK, 0x80, 0x10, 0x00, 0x10, DLE, ETX]
↑ ↑ escape NUL + extra DLE
OneTouch keypads communicate keypress events via specific key codes:
| Key | Value | Function |
|---|---|---|
| UP | 0x06 | Up arrow |
| DOWN | 0x05 | Down arrow |
| SELECT | 0x04 | Select/OK |
| PAGE_UP/SELECT_1 | 0x03 | Top button |
| BACK/SELECT_2 | 0x02 | Middle button |
| PAGE_DOWN/SELECT_3 | 0x01 | Bottom button |
From display lines, OneTouch can control equipment on/off or navigate menu:
Keypress → Master: [DLE, STX, 0x00, key_code, details, CHKSUM, DLE, ETX]
From get_packet() in aq_serial.c:
- Wait for DLE: First byte must be 0x10
- Expect STX: Next byte must be 0x02
- Collect Payload: Read bytes into buffer, tracking index
- Handle Escaping: If byte is DLE, check if followed by NUL (escape) or 0x03 (end)
- Validate Checksum: Verify calculated checksum matches packet
- Return Success/Error
| Code | Symbol | Meaning |
|---|---|---|
| 0 | Success | Packet received and valid |
| -1 | AQSERR_READ | Serial read error |
| -2 | AQSERR_TIMEOUT | Read timeout |
| -3 | AQSERR_CHKSUM | Checksum validation failed |
| -4 | AQSERR_2LARGE | Packet exceeds max length |
| -5 | AQSERR_2SMALL | Packet too short (<5 bytes) |
The parser automatically detects protocol type:
protocolType getProtocolType(const unsigned char* packet)
{
if (packet[0] == DLE) // 0x10
return JANDY; // This is Jandy protocol
else if (packet[0] == PP1) // 0xFF
return PENTAIR; // This is Pentair protocol
return P_UNKNOWN;
}The system supports configurable delay between packet transmission and reception to prevent bus collisions:
_aqconfig_.frame_delay // Configurable delay in millisecondsWhen frame_delay > 0:
- Track time of last serial read
- Before sending packet, wait until minimum elapsed time since last read
- Calculate minimum wait =
frame_delayms - Use
nanosleep()for precise timing
Last Read: T=0ms
Send Delay: frame_delay = 50ms
Current Time: T=30ms
Must wait: 20ms more before sending
Description: Long messages (0x0A command) to OneTouch devices sometimes have invalid checksums.
Detection Pattern:
if (packet[3] == 0x04 && packet[4] == 0x03 && packet[length-3] == 0x0a)
// This is the known bug - force checksum validWorkaround: AqualinkD logs a debug message and accepts the packet anyway.
Issue: The panel sometimes shows SWG status differently than the actual device status due to timing of when messages are received.
Examples:
- "AQUAPURE" prefix is 8 characters in display (MSG_SWG_PCT_LEN)
- "SALT" prefix is 4 characters (MSG_SWG_PPM_LEN)
- Timeouts or missing ACKs can mark SWG as offline
Current Limitation: AqualinkD only supports one SWG device despite the protocol allowing up to 4 (IDs 0x50-0x53).
Code Comment:
// Capture the SWG ID. We could have more than one, but for the moment
// AqualinkD only supports one so we'll pick the first one.Note: Newer panel revisions (Rev W and later) support additional ePump IDs in range 0xE0-0xE3 in addition to the standard 0x78-0x7B.
Issue: iAqualinkTouch status packets (0x70, 0x71, 0x72) can exceed 128 bytes, requiring larger buffer.
Solution: Max packet length increased to 512 bytes, with warning log for packets over 128 bytes (except for those specific status commands).
For reference, the Pentair protocol is also supported:
[0xFF] [0x00] [0xFF] [0xA5] [FROM] [DEST] [CMD] [LENGTH] [DATA...] [CHKSUM_HI] [CHKSUM_LO]
- Master: 0x10
- Pumps: 0x60-0x6F (96-111 decimal)
| Command | Value | Description |
|---|---|---|
| PEN_CMD_SPEED | 0x01 | Set pump speed (RPM or GPM) |
| PEN_CMD_REMOTECTL | 0x04 | Remote control |
| PEN_CMD_POWER | 0x06 | Set pump power (ON/OFF) |
| PEN_CMD_STATUS | 0x07 | Status request/response |
16-bit checksum calculated over data portion, stored as two bytes (high, low).
The system can log all RS485 packets in multiple formats:
- Raw Byte Logging: Individual bytes as received
- Formatted Logging: Hex display with protocol interpretation
- Pretty Printing: Human-readable descriptions
_aqconfig_.ftdi_low_latency // Enable low latency mode for FTDI adapters
_aqconfig_.frame_delay // Delay between packets (milliseconds)
_aqconfig_.log_protocol_packets // Log all packets to file
_aqconfig_.log_raw_bytes // Log individual bytes receivedWhen combining two bytes into a 16-bit value:
uint16_t value_16bit = (high_byte << 8) | low_byte
= (high_byte * 256) + low_byteExample: RPM from bytes [0x0B, 0x82]:
RPM = (0x0B * 256) + 0x82 = 2816 + 130 = 2946 RPM
Real packets from the decoding directory (from repository):
AR %% Set to 75%:
HEX: 0x10|0x02|0x50|0x11|0x4B|0x72|0x10|0x03|
SWG Response PPM=1200, Status=ON:
HEX: 0x10|0x02|0x00|0x16|0x0C|0x00|0x1E|0x10|0x03|
ePump Watts Response:
HEX: 0x10|0x02|0x00|0x1f|0x45|0x00|0x05|0x1d|0x10|0x03|
(Watts = 0x051D = 1309)
LXi Heater Status:
HEX: 0x10|0x02|0x00|0x0d|0x00|0x00|0x00|0x1f|0x10|0x03|
Key files in the AqualinkD project that implement this protocol:
source/aq_serial.c- Serial I/O and low-level protocol handlingsource/aq_serial.h- Protocol definitions and constantssource/devices_jandy.c- Jandy device handling and packet processingsource/devices_jandy.h- Jandy device declarationssource/devices_pentair.c- Pentair protocol implementationsource/rs_devices.h- Device ID ranges and helper functionssource/packetLogger.c- Packet logging utilities
See aq_serial.h for:
- Frame delimiters (NUL, DLE, STX, ETX)
- Command byte definitions (CMD_*)
- ACK response types
- Device ID ranges
- Packet length limits
- Error codes
The Jandy RS485 protocol is a mature, well-established protocol used across Jandy Aqualink pool control systems. While it has some quirks and undocumented messages, the AqualinkD project provides a solid open-source reference implementation.
The protocol supports:
- Multiple device types (pumps, heaters, chemical control, lighting)
- Multiple panel types (OneTouch, AqualinkTouch, PDA, AllButton emulation)
- Reliable communication via checksums
- Extensibility for new device types and panels
Future versions may add support for:
- Multiple SWG devices simultaneously
- Additional undocumented device types or commands
- Protocol optimizations and performance improvements