Files
SPPro/docs/PROTOCOL.md
T
2026-06-16 05:02:54 +10:00

4.9 KiB

SP PRO serial protocol

This documents the Selectronic SP PRO serial "memory" protocol as implemented in src/sppro.c. It is a port of the open-source Python project neerolyte/selpi (memory/*.py), cross-checked against the Go project angus-g/splink-influx.

Physical layer

Setting Value
Interface RS-232 (not RS-485), DB9 or RJ45 jack on the SP PRO
Baud 57600 (Port 1; Port 2 defaults to 9600)
Framing 8 data bits, no parity, 1 stop bit (8N1)
Flow control none

See HARDWARE.md for pinout and ESP32 wiring.

Frames

All multi-byte fields are little-endian. A frame is built from an 8-byte header:

byte 0      message type: 'Q' (0x51) query/read, or 'W' (0x57) write
byte 1      word count minus one (0x00 = 1 word, 0xff = 256 words)
bytes 2..5  memory address (uint32, little-endian)
bytes 6..7  CRC-16 over bytes 0..5  (little-endian)

Query (read)

Request is just the 8-byte header. The response echoes those 8 bytes, appends the requested data (words * 2 bytes), then a final CRC-16 over the whole response:

request :  Q | len-1 | addr[4] | crc[2]
response:  Q | len-1 | addr[4] | crc[2] | data[words*2] | crc[2]

Worked example (documented in selpi) — read 1 word at 0xa000:

request :  51 00 00 a0 00 00 9d 4b
response:  51 00 00 a0 00 00 9d 4b 01 00 d8 19   -> data word = 0x0001

Write

Header (with its own CRC over bytes 0..5), then the data words, then a CRC-16 over the whole message. The device echoes the entire request back to confirm:

request :  W | len-1 | addr[4] | crc[2] | data[words*2] | crc[2]
response:  <exact echo of the request>

CRC-16

Reflected CRC-CCITT ("Kermit"): 256-entry table, init 0x0000, crc = (crc >> 8) ^ table[(crc ^ byte) & 0xff], emitted little-endian. A received frame is valid when the CRC computed over the entire message (data + trailing CRC) equals 0. Implemented as sppro_crc16().

Login (MD5 challenge / response)

Reading data registers requires authenticating first:

  1. Query 8 words (16 bytes) at 0x1F0000 → challenge seed.
  2. Build md5( seed[16] + password_padded_to_32_bytes ). The password is the SP PRO's serial-port password (blank if none); it is padded/truncated to 32 bytes with spaces.
  3. Swap each adjacent byte pair of the 16-byte digest (a Selectronic endianness quirk) → 16-byte response.
  4. Write the 16-byte response back to 0x1F0000.
  5. Query 1 word at 0x1F0010; value 1 means authenticated.

Pure helper: sppro_login_response(). Full handshake: sppro_session_login().

Scaling

Raw register words are integers; engineering units come from per-device scale factors read once at startup from six consecutive words at 41000:

Address Name
41000 CommonScaleForAcVolts
41001 CommonScaleForAcCurrent
41002 CommonScaleForDcVolts
41003 CommonScaleForDcCurrent
41004 CommonScaleForTemperature
41005 CommonScaleForInternalVoltages

With MAGIC = 32768.0 (from selpi memory/converter.py):

Conversion Formula
ac_w raw * AcVolts * AcCurrent / (MAGIC * 800)
ac_w_signed raw * AcVolts * AcCurrent / (MAGIC * 100)
ac_wh raw * 24 * AcVolts * AcCurrent / (MAGIC * 100)
dc_w raw * DcVolts * DcCurrent / (MAGIC * 100)
dc_wh raw * 24 * DcVolts * DcCurrent / (MAGIC * 100)
dc_v raw * DcVolts / (MAGIC * 10)
temperature raw * Temperature / MAGIC
percent raw / 256
shunt_name enum → string (sppro_shunt_name)

Register map

The curated subset shipped in SPPRO_REGISTERS (transcribed from selpi memory/variable.py):

Name Address Type Conversion Units
BatteryVolts 0xa05c u16 dc_v V
BatteryTemperature 0xa03c u16 temperature C
DCBatteryPower 0xa02f s32 dc_w W
BattSocPercent 41089 u16 percent %
LoadAcPower 41107 u32 ac_w W
CombinedKacoAcPowerHiRes 0xa3a8 u32 ac_w W (AC solar)
ACGeneratorPower 41098 s16 ac_w_signed W
Shunt1Power / Shunt2Power 0xa088 / 0xa089 s16 dc_w W
Shunt1Name / Shunt2Name 0xc109 / 0xc10a s16 shunt_name
DCkWhInToday / DCkWhOutToday 41135 / 41137 u32 dc_wh Wh
BattInkWhTotalAcc / BattOutkWhTotalAcc 41354 / 41381 u32 dc_wh Wh
ACLoadkWhTotalAcc 41438 u32 ac_wh Wh
TotalKacokWhTotalAcc 41519 u32 ac_wh Wh

selpi's memory/variable.py lists many more accumulators (7/30/365-day, yearly, resettable); add rows to SPPRO_REGISTERS as needed — the table is the single source of truth and decoding is data-driven.

Credits