From 436d452deba8c5b9672cae81024744ae319cff14 Mon Sep 17 00:00:00 2001 From: Scott Penrose Date: Tue, 16 Jun 2026 04:59:02 +1000 Subject: [PATCH] Wrapper --- .gitignore | 2 +- LICENSE | 32 +++++ README.md | 79 +++++++++---- docs/HARDWARE.md | 2 +- esp32/sppro_esp32.ino | 105 ----------------- .../Esp32SerialMonitor/Esp32SerialMonitor.ino | 76 ++++++++++++ examples/Esp32SerialMonitor/platformio.ini | 16 +++ Makefile => extras/Makefile | 11 +- {host => extras/host}/main.c | 2 +- {tests => extras/tests}/test_sppro.c | 4 +- keywords.txt | 49 ++++++++ library.json | 36 ++++++ library.properties | 11 ++ src/Sppro.h | 109 ++++++++++++++++++ 14 files changed, 399 insertions(+), 135 deletions(-) create mode 100644 LICENSE delete mode 100644 esp32/sppro_esp32.ino create mode 100644 examples/Esp32SerialMonitor/Esp32SerialMonitor.ino create mode 100644 examples/Esp32SerialMonitor/platformio.ini rename Makefile => extras/Makefile (60%) rename {host => extras/host}/main.c (99%) rename {tests => extras/tests}/test_sppro.c (99%) create mode 100644 keywords.txt create mode 100644 library.json create mode 100644 library.properties create mode 100644 src/Sppro.h diff --git a/.gitignore b/.gitignore index ad5eada..58fe5e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Build output -/build/ +build/ *.o *.a *.elf diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9000b66 --- /dev/null +++ b/LICENSE @@ -0,0 +1,32 @@ +MIT License + +Copyright (c) 2026 Scott P + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Third-party / attribution notes: + +* src/md5.c, src/md5.h - MD5 implementation by Alexander Peslyak (Solar + Designer), placed in the public domain. See the notice in those files. + +* The SP PRO serial protocol implementation (CRC table, framing, login flow, + register map and scaling) is ported from the open-source project + neerolyte/selpi (https://github.com/neerolyte/selpi). diff --git a/README.md b/README.md index ba7e3c9..64e7a12 100644 --- a/README.md +++ b/README.md @@ -10,38 +10,69 @@ PyAware seems to already decode Serial Data. Look at porting that to portable C * 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 +This repo is 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), packaged as a **PlatformIO / Arduino library** with an optional thin C++ +wrapper. 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, +src/ sppro.h / sppro.c portable C 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) + Sppro.h optional header-only C++/Arduino wrapper (any Stream) +examples/ Esp32SerialMonitor/ ESP32 example (.ino + platformio.ini) docs/ PROTOCOL.md wire protocol, CRC, login, register map + scaling HARDWARE.md RS-232 pinout and MAX3232 -> ESP32 wiring +extras/ host/main.c Linux serial-console dashboard (termios) + tests/test_sppro.c host known-answer tests (no hardware) + Makefile host build (test / console) +library.json, library.properties, keywords.txt PlatformIO + Arduino manifests ``` -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. +The core (`src/sppro.*`) 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, an MCU, +or anything with a serial port. Only `src/` and `examples/` are part of the published +library; `extras/` is development tooling (Arduino ignores it). -## Build & test +## Use on Arduino / ESP32 (PlatformIO or Arduino IDE) + +Add the library (`lib_deps = SPPro` in PlatformIO once published, or *Sketch → Include +Library* in the Arduino IDE), then use the `Sppro` wrapper, which works over **any** +Arduino `Stream` (HardwareSerial, SoftwareSerial, WiFiClient, …): + +```cpp +#include +Sppro sp(Serial2); // wraps any Stream + +void setup() { + Serial2.begin(57600, SERIAL_8N1, 16, 17); // via a MAX3232 level shifter + sp.login(""); // "" = no serial password + sp.readScales(); +} +void loop() { + Serial.println(sp.value("BatteryVolts")); // scaled value, or NAN on error + delay(5000); +} +``` + +A complete ESP32 example is in +[`examples/Esp32SerialMonitor/`](examples/Esp32SerialMonitor); build it standalone with +`cd examples/Esp32SerialMonitor && pio run` (it links the parent library via +`lib_deps = symlink://../..`). See [docs/HARDWARE.md](docs/HARDWARE.md) — the SP PRO port +is RS-232 (±12 V) and **needs a MAX3232 level shifter**; never wire it straight to a GPIO. + +## Build & test on a host (no hardware) ```sh -make test # build and run the host unit tests (no hardware needed) +cd extras +make test # build and run the host unit tests 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: +Then, with a real SP PRO + USB-RS232 adapter: ```sh ./build/sppro-console /dev/ttyUSB0 [serial_password] [interval_seconds] @@ -52,7 +83,7 @@ voltage, state of charge, load/solar/generator power, and energy totals. The ser 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 +## Use the pure C core (non-Arduino: ESP-IDF, STM32, Linux, Zephyr …) ```c #include "sppro.h" @@ -68,15 +99,21 @@ 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`, +`extras/host/main.c` is a full worked example of the two callbacks (termios). 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. +- Verified by host unit tests (`cd extras && make test`): CRC against selpi's documented + `0xa000` frame, MD5 known-answers, frame round-trips, and the scaling formulas. The + ESP32 example + C++ wrapper are verified to compile/link via PlatformIO. **Not yet + verified against physical hardware** — confirm decoded values against the inverter's own + display. +- On 8-bit AVR, `double` is 32-bit float, so very large `Wh` accumulators lose some + precision (fine for display). Use the 32-bit-int register types directly if you need + exact counts. - 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. diff --git a/docs/HARDWARE.md b/docs/HARDWARE.md index 018df08..e4fbc86 100644 --- a/docs/HARDWARE.md +++ b/docs/HARDWARE.md @@ -59,7 +59,7 @@ Serial2.begin(57600, SERIAL_8N1, /*RX=*/16, /*TX=*/17); GPIO16/17 are safe general-purpose pins on most ESP32 dev boards. (On a few modules with PSRAM these pins are reserved — pick two other free UART-capable GPIOs and update the -sketch if so.) See `esp32/sppro_esp32.ino`. +sketch if so.) See `examples/Esp32SerialMonitor/`. ## Quick bring-up checks diff --git a/esp32/sppro_esp32.ino b/esp32/sppro_esp32.ino deleted file mode 100644 index 0fed8bf..0000000 --- a/esp32/sppro_esp32.ino +++ /dev/null @@ -1,105 +0,0 @@ -/* - * sppro_esp32.ino - read a Selectronic SP PRO from an ESP32 over UART2. - * - * Reuses the portable core unchanged. Copy src/sppro.c, src/sppro.h, src/md5.c - * and src/md5.h next to this .ino (Arduino compiles all source in the sketch - * folder), or add src/ as a library. - * - * Wiring (see docs/HARDWARE.md) - the SP PRO port is RS-232 (+/-12V), so a - * level shifter is required; do NOT wire it straight to the ESP32: - * - * SP PRO RJ45 pin 3 (TXD) --> MAX3232 R1IN ; R1OUT --> ESP32 GPIO16 (RX2) - * ESP32 GPIO17 (TX2) --> MAX3232 T1IN ; T1OUT --> SP PRO RJ45 pin 6 (RXD) - * SP PRO RJ45 pin 4/5 (GND)--> MAX3232 GND --> ESP32 GND (common ground!) - * ESP32 3V3 --> MAX3232 VCC (+ four 0.1uF charge-pump caps) - * - * Set SPPRO_PASSWORD to the inverter's serial-port password ("" if none). - */ -extern "C" { -#include "sppro.h" -} - -static const int PIN_RX = 16; /* ESP32 RX2 <- MAX3232 R1OUT */ -static const int PIN_TX = 17; /* ESP32 TX2 -> MAX3232 T1IN */ -static const char *SPPRO_PASSWORD = ""; - -static sppro_scales_t g_scales; -static bool g_ready = false; - -/* ----- transport bound to Serial2 ---------------------------------------- */ - -static int esp_read(void *ctx, uint8_t *buf, size_t len) { - (void)ctx; - size_t got = 0; - /* brief wait so a frame that is mid-flight is not reported as "empty" */ - unsigned long start = millis(); - while (got < len && (millis() - start) < 200) { - while (Serial2.available() && got < len) { - buf[got++] = (uint8_t)Serial2.read(); - start = millis(); - } - if (got == 0) delay(1); - else break; - } - return (int)got; /* 0 on timeout */ -} - -static int esp_write(void *ctx, const uint8_t *buf, size_t len) { - (void)ctx; - return (int)Serial2.write(buf, len); -} - -static sppro_transport_t g_transport = { esp_read, esp_write, nullptr }; - -/* ----- sketch ------------------------------------------------------------- */ - -static bool connect_sppro() { - if (sppro_session_login(&g_transport, SPPRO_PASSWORD) != SPPRO_OK) { - Serial.println("login failed - check wiring/password"); - return false; - } - if (sppro_session_read_scales(&g_transport, &g_scales) != SPPRO_OK) { - Serial.println("could not read scale factors"); - return false; - } - Serial.println("connected to SP PRO"); - return true; -} - -static void show(const char *name) { - const sppro_reg_t *reg = sppro_reg_by_name(name); - double value; - if (!reg) return; - if (sppro_session_read(&g_transport, reg, &g_scales, &value) != SPPRO_OK) { - Serial.printf("%-26s (read error)\n", reg->description); - return; - } - if (reg->conv == SPPRO_C_SHUNT_NAME) - Serial.printf("%-26s %12s\n", reg->description, sppro_shunt_name((int)value)); - else - Serial.printf("%-26s %12.2f %s\n", reg->description, value, reg->units); -} - -void setup() { - Serial.begin(115200); - Serial2.begin(SPPRO_BAUD, SERIAL_8N1, PIN_RX, PIN_TX); - delay(200); - g_ready = connect_sppro(); -} - -void loop() { - if (!g_ready) { - delay(2000); - g_ready = connect_sppro(); - return; - } - Serial.println("\n== Selectronic SP PRO =="); - show("BatteryVolts"); - show("BattSocPercent"); - show("BatteryTemperature"); - show("DCBatteryPower"); - show("LoadAcPower"); - show("CombinedKacoAcPowerHiRes"); - show("ACGeneratorPower"); - delay(5000); -} diff --git a/examples/Esp32SerialMonitor/Esp32SerialMonitor.ino b/examples/Esp32SerialMonitor/Esp32SerialMonitor.ino new file mode 100644 index 0000000..489a310 --- /dev/null +++ b/examples/Esp32SerialMonitor/Esp32SerialMonitor.ino @@ -0,0 +1,76 @@ +/* + * Esp32SerialMonitor - read a Selectronic SP PRO from an ESP32 and print a + * dashboard to the USB serial monitor, using the thin Sppro C++ wrapper. + * + * The SP PRO serial port is RS-232 (+/-12V) - you MUST use a level shifter + * (MAX3232); do not wire it straight to a GPIO. See docs/HARDWARE.md: + * + * SP PRO RJ45 pin 3 (TXD) --> MAX3232 R1IN ; R1OUT --> ESP32 GPIO16 (RX2) + * ESP32 GPIO17 (TX2) --> MAX3232 T1IN ; T1OUT --> SP PRO RJ45 pin 6 (RXD) + * SP PRO RJ45 pin 4/5 (GND)--> MAX3232 GND --> ESP32 GND (common ground!) + * ESP32 3V3 --> MAX3232 VCC (+ four 0.1uF charge-pump caps) + * + * Build with PlatformIO (this folder has its own platformio.ini): + * cd examples/Esp32SerialMonitor && pio run + * or open the .ino in the Arduino IDE after installing this library. + */ +#include + +static const int PIN_RX = 16; /* ESP32 RX2 <- MAX3232 R1OUT */ +static const int PIN_TX = 17; /* ESP32 TX2 -> MAX3232 T1IN */ +static const char *PASSWORD = ""; /* inverter serial-port password ("" if none) */ + +/* Sppro wraps any Arduino Stream; here it is UART2. */ +Sppro sp(Serial2); +bool ready = false; + +static bool connectSppro() { + if (sp.login(PASSWORD) != SPPRO_OK) { + Serial.println("login failed - check wiring/password"); + return false; + } + if (sp.readScales() != SPPRO_OK) { + Serial.println("could not read scale factors"); + return false; + } + Serial.println("connected to SP PRO"); + return true; +} + +static void show(const char *name) { + const sppro_reg_t *reg = sppro_reg_by_name(name); + if (!reg) return; + if (reg->conv == SPPRO_C_SHUNT_NAME) { + Serial.printf("%-26s %12s\n", reg->description, sp.name(name)); + } else { + double v; + if (sp.read(name, v) != SPPRO_OK) + Serial.printf("%-26s (read error)\n", reg->description); + else + Serial.printf("%-26s %12.2f %s\n", reg->description, v, reg->units); + } +} + +void setup() { + Serial.begin(115200); + Serial2.begin(SPPRO_BAUD, SERIAL_8N1, PIN_RX, PIN_TX); + delay(200); + ready = connectSppro(); +} + +void loop() { + if (!ready) { + delay(2000); + ready = connectSppro(); + return; + } + Serial.println("\n== Selectronic SP PRO =="); + show("BatteryVolts"); + show("BattSocPercent"); + show("BatteryTemperature"); + show("DCBatteryPower"); + show("LoadAcPower"); + show("CombinedKacoAcPowerHiRes"); + show("ACGeneratorPower"); + delay(5000); +} diff --git a/examples/Esp32SerialMonitor/platformio.ini b/examples/Esp32SerialMonitor/platformio.ini new file mode 100644 index 0000000..0e2f3bc --- /dev/null +++ b/examples/Esp32SerialMonitor/platformio.ini @@ -0,0 +1,16 @@ +; Build this example standalone: cd here && pio run +; It pulls in the parent SPPro library via a symlink to the repo root +; (which holds library.json). symlink:// is the modern replacement for the +; deprecated lib_extra_dirs. +; +; src_dir = . keeps the .ino at the example root (so the Arduino IDE sees it as +; a normal example) while still letting `pio run` build it from this folder. +[platformio] +src_dir = . + +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 115200 +lib_deps = symlink://../.. diff --git a/Makefile b/extras/Makefile similarity index 60% rename from Makefile rename to extras/Makefile index 96a4116..cdccd9d 100644 --- a/Makefile +++ b/extras/Makefile @@ -1,11 +1,14 @@ -# SPPro - portable C parser for Selectronic SP PRO serial data. +# SPPro - host build for the portable C parser (dev tooling, lives in extras/). +# The library itself is ../src; these targets are not part of the published +# Arduino/PlatformIO library. CC ?= cc -CFLAGS ?= -std=c99 -Wall -Wextra -Wpedantic -O2 +CFLAGS ?= -std=c99 -Wall -Wextra -Wpedantic -O2 -I../src LDFLAGS ?= -SRC := src/sppro.c src/md5.c -HDR := src/sppro.h src/md5.h +SRCDIR := ../src +SRC := $(SRCDIR)/sppro.c $(SRCDIR)/md5.c +HDR := $(SRCDIR)/sppro.h $(SRCDIR)/md5.h BUILD := build diff --git a/host/main.c b/extras/host/main.c similarity index 99% rename from host/main.c rename to extras/host/main.c index fdd9b66..07f95fb 100644 --- a/host/main.c +++ b/extras/host/main.c @@ -13,7 +13,7 @@ */ #define _DEFAULT_SOURCE /* cfmakeraw, CRTSCTS, sleep() under -std=c99 */ -#include "../src/sppro.h" +#include "sppro.h" #include #include diff --git a/tests/test_sppro.c b/extras/tests/test_sppro.c similarity index 99% rename from tests/test_sppro.c rename to extras/tests/test_sppro.c index 015866d..2dddeec 100644 --- a/tests/test_sppro.c +++ b/extras/tests/test_sppro.c @@ -2,8 +2,8 @@ * test_sppro.c - host known-answer tests for the SP PRO parser. No hardware. * make test */ -#include "../src/sppro.h" -#include "../src/md5.h" +#include "sppro.h" +#include "md5.h" #include #include diff --git a/keywords.txt b/keywords.txt new file mode 100644 index 0000000..60bf2d5 --- /dev/null +++ b/keywords.txt @@ -0,0 +1,49 @@ +####################################### +# Syntax coloring for the SPPro library +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +Sppro KEYWORD1 +sppro_transport_t KEYWORD1 +sppro_scales_t KEYWORD1 +sppro_reg_t KEYWORD1 +sppro_status_t KEYWORD1 +sppro_type_t KEYWORD1 +sppro_conv_t KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +login KEYWORD2 +readScales KEYWORD2 +scales KEYWORD2 +read KEYWORD2 +value KEYWORD2 +name KEYWORD2 +sppro_crc16 KEYWORD2 +sppro_build_query KEYWORD2 +sppro_build_write KEYWORD2 +sppro_parse_query_response KEYWORD2 +sppro_decode KEYWORD2 +sppro_convert KEYWORD2 +sppro_reg_by_name KEYWORD2 +sppro_shunt_name KEYWORD2 +sppro_login_response KEYWORD2 +sppro_session_login KEYWORD2 +sppro_session_query KEYWORD2 +sppro_session_write KEYWORD2 +sppro_session_read KEYWORD2 +sppro_session_read_scales KEYWORD2 + +####################################### +# Constants (LITERAL1) +####################################### + +SPPRO_BAUD LITERAL1 +SPPRO_OK LITERAL1 +SPPRO_ERR_UNKNOWN LITERAL1 +SPPRO_C_SHUNT_NAME LITERAL1 diff --git a/library.json b/library.json new file mode 100644 index 0000000..8cf7512 --- /dev/null +++ b/library.json @@ -0,0 +1,36 @@ +{ + "name": "SPPro", + "version": "0.1.0", + "description": "Portable C parser for the Selectronic SP PRO inverter serial protocol (CRC, frame build/parse, MD5 login, register decoding + scaling), with an optional thin Arduino C++ wrapper that works over any Stream (HardwareSerial, SoftwareSerial, WiFiClient).", + "keywords": "selectronic, sp pro, inverter, solar, battery, rs232, serial, energy", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/scottp/SPPro.git" + }, + "authors": [ + { + "name": "Scott P", + "maintainer": true + } + ], + "frameworks": "*", + "platforms": "*", + "headers": ["sppro.h", "Sppro.h"], + "examples": [ + { + "name": "Esp32SerialMonitor", + "base": "examples/Esp32SerialMonitor", + "files": ["Esp32SerialMonitor.ino", "platformio.ini"] + } + ], + "export": { + "exclude": [ + "extras", + "build", + ".pio", + "**/build", + "**/.pio" + ] + } +} diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..c528342 --- /dev/null +++ b/library.properties @@ -0,0 +1,11 @@ +name=SPPro +version=0.1.0 +author=Scott P +maintainer=Scott P +sentence=Read and decode Selectronic SP PRO inverter data over its serial port. +paragraph=Portable C parser for the SP PRO serial protocol (Kermit CRC, Q/W frame build/parse, MD5 challenge/response login, register decoding and scaling). Includes an optional thin C++ wrapper around any Arduino Stream (HardwareSerial, SoftwareSerial, WiFiClient/EthernetClient). Ported from the open-source selpi project; needs an RS-232 level shifter (e.g. MAX3232). +category=Communication +url=https://github.com/scottp/SPPro +architectures=* +license=MIT +includes=Sppro.h diff --git a/src/Sppro.h b/src/Sppro.h new file mode 100644 index 0000000..427eccf --- /dev/null +++ b/src/Sppro.h @@ -0,0 +1,109 @@ +/* + * Sppro.h - optional thin C++/Arduino wrapper around the portable C core. + * + * Header-only and guarded by ARDUINO, so it compiles to nothing on non-Arduino + * platforms (where you use the C API in sppro.h directly with your own + * sppro_transport_t callbacks). The class wraps any Arduino Stream, so it works + * over HardwareSerial (Serial1/Serial2), SoftwareSerial, or a networked client + * (WiFiClient/EthernetClient) without any per-transport code. + * + * #include + * Sppro sp(Serial2); + * void setup() { Serial2.begin(57600, SERIAL_8N1, 16, 17); + * sp.login(""); sp.readScales(); } + * void loop() { Serial.println(sp.value("BatteryVolts")); } + * + * Note: on 8-bit AVR `double` is 32-bit float, so very large Wh accumulators + * lose some precision (fine for display). + */ +#ifndef SPPRO_CPP_H +#define SPPRO_CPP_H + +#ifdef ARDUINO + +#include +#include + +extern "C" { +#include "sppro.h" +} + +class Sppro { +public: + /* Wrap any Arduino Stream (HardwareSerial, SoftwareSerial, Client, ...). + * Configure the stream's baud/pins yourself before calling login(). */ + explicit Sppro(Stream &io, unsigned long readTimeoutMs = 200) + : _io(io), _timeout(readTimeoutMs) + { + _t.read = &Sppro::readCb; + _t.write = &Sppro::writeCb; + _t.ctx = this; + } + + /* MD5 challenge/response login. pw is the inverter serial-port password + * ("" if none). Returns SPPRO_OK or a negative sppro_status_t. */ + int login(const char *pw = "") { return sppro_session_login(&_t, pw); } + + /* Read the device scale factors once after login; cached for read()/value(). */ + int readScales() { return sppro_session_read_scales(&_t, &_scales); } + + const sppro_scales_t &scales() const { return _scales; } + const sppro_transport_t &transport() const { return _t; } + + /* Read a register by its selpi name into out. Returns SPPRO_OK, + * SPPRO_ERR_UNKNOWN for a bad name, or a negative I/O error. */ + int read(const char *name, double &out) + { + const sppro_reg_t *reg = sppro_reg_by_name(name); + if (!reg) return SPPRO_ERR_UNKNOWN; + return sppro_session_read(&_t, reg, &_scales, &out); + } + + /* Convenience: returns the scaled value, or NAN on any error. */ + double value(const char *name) + { + double v; + return read(name, v) == SPPRO_OK ? v : NAN; + } + + /* For SPPRO_C_SHUNT_NAME registers: returns the label, or "Error". */ + const char *name(const char *reg_name) + { + double v; + if (read(reg_name, v) != SPPRO_OK) return "Error"; + return sppro_shunt_name((int)v); + } + +private: + static int readCb(void *ctx, uint8_t *buf, size_t len) + { + Sppro *self = static_cast(ctx); + Stream &io = self->_io; + size_t n = 0; + unsigned long start = millis(); + while (n < len && (millis() - start) < self->_timeout) { + while (io.available() && n < len) { + int c = io.read(); + if (c < 0) break; + buf[n++] = (uint8_t)c; + start = millis(); + } + if (n == 0) delay(1); + else break; + } + return (int)n; /* 0 on timeout */ + } + + static int writeCb(void *ctx, const uint8_t *buf, size_t len) + { + return (int)static_cast(ctx)->_io.write(buf, len); + } + + Stream &_io; + unsigned long _timeout; + sppro_transport_t _t; + sppro_scales_t _scales; +}; + +#endif /* ARDUINO */ +#endif /* SPPRO_CPP_H */