5.3 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.
Note on PyAware. The project README pointed at PyAware as prior art. PyAware (PyPI, by Ampcontrol Group) is not open source — its PyPI metadata references a private SVN repository — so it could not be ported directly.
selpiimplements the same wire protocol and is fully open, so it was used as the reference instead.
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:
- Query 8 words (16 bytes) at
0x1F0000→ challengeseed. - 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. - Swap each adjacent byte pair of the 16-byte digest (a Selectronic endianness quirk) → 16-byte response.
- Write the 16-byte response back to
0x1F0000. - Query 1 word at
0x1F0010; value1means 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
- neerolyte/selpi — primary reference (MIT-style, see repo).
- angus-g/splink-influx — independent confirmation.