Wrapper
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Build output
|
||||
/build/
|
||||
build/
|
||||
*.o
|
||||
*.a
|
||||
*.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
|
||||
* 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.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
|
||||
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.
|
||||
|
||||
+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
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*/
|
||||
#define _DEFAULT_SOURCE /* cfmakeraw, CRTSCTS, sleep() under -std=c99 */
|
||||
|
||||
#include "../src/sppro.h"
|
||||
#include "sppro.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
@@ -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 <stdio.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