# SPPro Access Build a system that can connect to an SPPro Serial Port to get sensible data out of it to display it on a serial console and be used in other projects. PyAware seems to already decode Serial Data. Look at porting that to portable C for decoding. * Hardware necessary to connect to SPPro * https://www.selectronic.com.au/documents/TechNotes/TN0050_02%20SP%20PRO%20Serial%20Port%20Pin-out.pdf * How to connect this to an ESP32 * Parse Serial Data * A simple C parser, portable to allow parsing This repo now contains a **portable C parser** for the SP PRO serial protocol, ported from [neerolyte/selpi](https://github.com/neerolyte/selpi) (PyAware itself is not open source). Full protocol notes are in [docs/PROTOCOL.md](docs/PROTOCOL.md) and the ESP32 wiring in [docs/HARDWARE.md](docs/HARDWARE.md). ## Layout ``` src/ sppro.h / sppro.c portable core: CRC, frame build/parse, md5.h / md5.c register map + scaling, MD5 login, session layer host/ main.c Linux serial-console dashboard (termios) esp32/ sppro_esp32.ino ESP32 (Arduino) example, reuses the core unchanged tests/ test_sppro.c host known-answer tests (no hardware) docs/ PROTOCOL.md wire protocol, CRC, login, register map + scaling HARDWARE.md RS-232 pinout and MAX3232 -> ESP32 wiring ``` The core (`src/`) is C99, **no dynamic allocation and no I/O** — it builds/parses byte buffers and converts raw words to units. I/O is supplied by the caller through a small `sppro_transport_t` read/write callback, so the same code runs on a host or an MCU. ## Build & test ```sh make test # build and run the host unit tests (no hardware needed) make host # build ./build/sppro-console ``` ## Run against a real SP PRO Wire it up per [docs/HARDWARE.md](docs/HARDWARE.md) (RS-232, **needs a MAX3232 level shifter**, 57600 8N1), then: ```sh ./build/sppro-console /dev/ttyUSB0 [serial_password] [interval_seconds] ``` It logs in, reads the device scale factors, and prints a refreshing dashboard of battery voltage, state of charge, load/solar/generator power, and energy totals. The serial-port password is set on the inverter (front panel → Settings → Communications); pass `""` if none is configured, or set `SPPRO_PASSWORD`. ## Using the core in another project ```c #include "sppro.h" sppro_transport_t t = { my_read, my_write, my_ctx }; /* your serial callbacks */ sppro_session_login(&t, ""); /* MD5 challenge/response */ sppro_scales_t scales; sppro_session_read_scales(&t, &scales); double soc, vbat; sppro_session_read(&t, sppro_reg_by_name("BattSocPercent"), &scales, &soc); sppro_session_read(&t, sppro_reg_by_name("BatteryVolts"), &scales, &vbat); ``` For environments with their own protocol loop, the pure helpers (`sppro_build_query`, `sppro_parse_query_response`, `sppro_decode`, `sppro_crc16`, `sppro_login_response`) can be used without the session/transport layer. ## Status & caveats - Verified by host unit tests: CRC against selpi's documented `0xa000` frame, MD5 known-answers, frame round-trips, and the scaling formulas. **Not yet verified against physical hardware** — confirm decoded values against the inverter's own display. - Read-only monitoring is the focus. Write framing (`sppro_build_write` / `sppro_session_write`) exists because login needs it, but no inverter-setting writes are wired into the demos. - The register table is a useful subset; add rows from selpi `memory/variable.py` as needed (decoding is table-driven). ## Not-yet-mapped registers (future work) `SPPRO_REGISTERS` covers all the live measurements plus the most useful energy totals. The following entries exist in selpi `memory/variable.py` but are **not** in the table yet. All are decodable by the existing converters — adding them is pure data (new rows), no new code. Scale factor (already used internally via `sppro_scales_t`, just not exposed as a row): | Name | Addr | Type | Conv | |---|---|---|---| | CommonScaleForInternalVoltages | 41005 | u16 | raw | Battery "out" energy accumulators (all `u32`, `dc_wh`): | Name | Addr | |---|---| | BattOutkWhAcc | 41143 | | QuickView_BattOutkWhAcc | 41178 | | BattOutkWhPreviousAcc | 41356 | | BattOutkWh7DayAcc / …AccAvg | 41358 / 41360 | | BattOutkWh30DayAcc / …AccAvg | 41362 / 41364 | | BattOutkWh365DayAcc / …AccAvg | 41366 / 41368 | | BattOutkWhYearAcc / …AccAvg | 41370 / 41372 | | BattOutkWhResetableAcc / …AccAvg | 41374 / 41376 | Other DC energy: | Name | Addr | Type | Conv | |---|---|---|---| | DCkWhOut | 41257 | u32 | dc_wh | | Shunt1WhTotalAcc | 41305 | s32 | dc_wh | | Shunt1WhTodayAcc | 41146 | s16 | dc_wh | AC energy (input / export / load / solar): | Name | Addr | Type | Conv | |---|---|---|---| | ACInputWhTotalAcc | 41459 | u32 | ac_wh | | ACInputWhTodayAcc | 41151 | u16 | ac_wh | | ACExportWhTotalAcc | 41499 | u32 | ac_wh | | ACExportWhTodayAcc | 41154 | u16 | ac_wh | | ACLoadWhAcc | 41150 | u16 | ac_wh | | ACSolarWhTotalAcc | 41519 | u32 | ac_wh | | ACSolarWhTodayAcc | 41157 | s16 | ac_wh | Two quirks when transcribing these: - `ACSolarWhTotalAcc` (41519) is the **same address** as the already-included `TotalKacokWhTotalAcc` (41519) — selpi just has two names for it. - `ACGeneratorPower` appears twice in selpi's MAP; the second definition wins (`s16`, `ac_w_signed`) — that is the one already in the table. The first (`u32`, `ac_w`) is dead. ## See Also * pyAWARE - not open source * selpi - a Python set for seletronics * https://github.com/neerolyte/selpi * splink-influx * https://github.com/angus-g/splink-influx