Wrapper
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# Build output
|
# Build output
|
||||||
/build/
|
build/
|
||||||
*.o
|
*.o
|
||||||
*.a
|
*.a
|
||||||
*.elf
|
*.elf
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -10,38 +10,69 @@ PyAware seems to already decode Serial Data. Look at porting that to portable C
|
|||||||
* Parse Serial Data
|
* Parse Serial Data
|
||||||
* A simple C parser, portable to allow parsing
|
* A simple C parser, portable to allow parsing
|
||||||
|
|
||||||
This repo now contains a **portable C parser** for the SP PRO serial protocol, ported
|
This repo is a **portable C parser** for the SP PRO serial protocol (ported from
|
||||||
from [neerolyte/selpi](https://github.com/neerolyte/selpi) (PyAware itself is not open
|
[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
|
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).
|
wiring in [docs/HARDWARE.md](docs/HARDWARE.md).
|
||||||
|
|
||||||
## Layout
|
## 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
|
md5.h / md5.c register map + scaling, MD5 login, session layer
|
||||||
host/ main.c Linux serial-console dashboard (termios)
|
Sppro.h optional header-only C++/Arduino wrapper (any Stream)
|
||||||
esp32/ sppro_esp32.ino ESP32 (Arduino) example, reuses the core unchanged
|
examples/ Esp32SerialMonitor/ ESP32 example (.ino + platformio.ini)
|
||||||
tests/ test_sppro.c host known-answer tests (no hardware)
|
|
||||||
docs/ PROTOCOL.md wire protocol, CRC, login, register map + scaling
|
docs/ PROTOCOL.md wire protocol, CRC, login, register map + scaling
|
||||||
HARDWARE.md RS-232 pinout and MAX3232 -> ESP32 wiring
|
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
|
The core (`src/sppro.*`) is C99, **no dynamic allocation and no I/O** — it builds/parses
|
||||||
buffers and converts raw words to units. I/O is supplied by the caller through a small
|
byte buffers and converts raw words to units. I/O is supplied by the caller through a
|
||||||
`sppro_transport_t` read/write callback, so the same code runs on a host or an MCU.
|
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.h>
|
||||||
|
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
|
```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
|
make host # build ./build/sppro-console
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run against a real SP PRO
|
Then, with a real SP PRO + USB-RS232 adapter:
|
||||||
|
|
||||||
Wire it up per [docs/HARDWARE.md](docs/HARDWARE.md) (RS-232, **needs a MAX3232 level
|
|
||||||
shifter**, 57600 8N1), then:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./build/sppro-console /dev/ttyUSB0 [serial_password] [interval_seconds]
|
./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
|
password is set on the inverter (front panel → Settings → Communications); pass `""` if
|
||||||
none is configured, or set `SPPRO_PASSWORD`.
|
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
|
```c
|
||||||
#include "sppro.h"
|
#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);
|
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
|
`sppro_parse_query_response`, `sppro_decode`, `sppro_crc16`, `sppro_login_response`) can
|
||||||
be used without the session/transport layer.
|
be used without the session/transport layer.
|
||||||
|
|
||||||
## Status & caveats
|
## Status & caveats
|
||||||
|
|
||||||
- Verified by host unit tests: CRC against selpi's documented `0xa000` frame, MD5
|
- Verified by host unit tests (`cd extras && make test`): CRC against selpi's documented
|
||||||
known-answers, frame round-trips, and the scaling formulas. **Not yet verified against
|
`0xa000` frame, MD5 known-answers, frame round-trips, and the scaling formulas. The
|
||||||
physical hardware** — confirm decoded values against the inverter's own display.
|
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` /
|
- Read-only monitoring is the focus. Write framing (`sppro_build_write` /
|
||||||
`sppro_session_write`) exists because login needs it, but no inverter-setting writes
|
`sppro_session_write`) exists because login needs it, but no inverter-setting writes
|
||||||
are wired into the demos.
|
are wired into the demos.
|
||||||
|
|||||||
+1
-1
@@ -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
|
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
|
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
|
## Quick bring-up checks
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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 <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 *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);
|
||||||
|
}
|
||||||
@@ -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://../..
|
||||||
@@ -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
|
CC ?= cc
|
||||||
CFLAGS ?= -std=c99 -Wall -Wextra -Wpedantic -O2
|
CFLAGS ?= -std=c99 -Wall -Wextra -Wpedantic -O2 -I../src
|
||||||
LDFLAGS ?=
|
LDFLAGS ?=
|
||||||
|
|
||||||
SRC := src/sppro.c src/md5.c
|
SRCDIR := ../src
|
||||||
HDR := src/sppro.h src/md5.h
|
SRC := $(SRCDIR)/sppro.c $(SRCDIR)/md5.c
|
||||||
|
HDR := $(SRCDIR)/sppro.h $(SRCDIR)/md5.h
|
||||||
|
|
||||||
BUILD := build
|
BUILD := build
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
*/
|
*/
|
||||||
#define _DEFAULT_SOURCE /* cfmakeraw, CRTSCTS, sleep() under -std=c99 */
|
#define _DEFAULT_SOURCE /* cfmakeraw, CRTSCTS, sleep() under -std=c99 */
|
||||||
|
|
||||||
#include "../src/sppro.h"
|
#include "sppro.h"
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
* test_sppro.c - host known-answer tests for the SP PRO parser. No hardware.
|
* test_sppro.c - host known-answer tests for the SP PRO parser. No hardware.
|
||||||
* make test
|
* make test
|
||||||
*/
|
*/
|
||||||
#include "../src/sppro.h"
|
#include "sppro.h"
|
||||||
#include "../src/md5.h"
|
#include "md5.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
+109
@@ -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.h>
|
||||||
|
* 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 <Arduino.h>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
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<Sppro *>(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<Sppro *>(ctx)->_io.write(buf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream &_io;
|
||||||
|
unsigned long _timeout;
|
||||||
|
sppro_transport_t _t;
|
||||||
|
sppro_scales_t _scales;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* ARDUINO */
|
||||||
|
#endif /* SPPRO_CPP_H */
|
||||||
Reference in New Issue
Block a user