mirror of
https://github.com/arendst/Tasmota.git
synced 2026-07-27 20:05:46 +00:00
sml updates (#24716)
# SML driver: catch-up of accumulated fork changes This PR rolls up the SML driver work that's accumulated on a downstream fork over the last ~2 years. **One file** (`tasmota/tasmota_xsns_sensor/xsns_53_sml.ino`, +288 / −27), squashed for review. Three logical groups, each with its own opt-out path so existing builds get bit-for-bit-identical behaviour where nothing actually changed. ## TL;DR | Group | What | Gating | Default | |---|---|---|---| | **A** — bug fixes | 10 narrow correctness fixes | always-on | enabled | | **B** — scripter-free operation | drive descriptor from `/sml_meter.def` without the Tasmota Scripter | existing `#ifdef USE_SCRIPT` / `USE_UFILESYS` guards | inherits build config | | **C** — `USE_BAT_CTRL` (Modbus write queue, TCP-reset cleanup, dynamic MBAP SIZE) | new feature for Modbus-write-heavy descriptors | new `#ifdef USE_BAT_CTRL` | ESP32 ON, ESP8266 OFF | If you want to merge only Group A, the gate-out for B and C is a 2-line revert (drop the `USE_BAT_CTRL` auto-define and the missing-`USE_SCRIPT` fallbacks). Happy to split into 3 separate PRs if that's preferred. ## Group A — bug fixes (always-on) Each is a documented repro on a real or emulated meter; failure modes listed are observed not theoretical. ### A1. FC-aware Modbus RTU response framing `mlen = sbuff[2] + 5` was used for ALL RTU responses. Correct for FC01-04 reads but wrong for FC05/06/15/16 write echoes (fixed 8 B with `reg_hi` in `sbuff[2]`) and exception replies (`FC|0x80`, fixed 5 B). Mixing reads with writes corrupted receive-buffer alignment for one cycle after each write. Now switches on the FC byte. Inline comment block documents all four framing cases. ### A2. `@bN:iM:` bit-extract dropped when combined with mbus index filter The `b` block updated `ebus_dval` only; the `@i` path immediately below took `dval = mbus_dval`, so the bit was thrown away. Visible on FC01 coil descriptors that fan one response byte across 8 per-coil JSON fields — all read 17 (raw byte) instead of 1/0/0/1. One added line: `mbus_dval = ebus_dval;` inside the `b` block. ### A3. Heap corruption when loading descriptor from `/sml_meter.def` Without scripter, `lp1` points into `file_md` whose lines end with `SCRIPT_EOL`, not `'\0'`. The descriptor copy loop only checked `'\0'`, so it ran past line end and clobbered adjacent heap (observed reliably with 30+-row meter defs on SD card). Added `*lp1 == SCRIPT_EOL` to the loop terminator. ### A4. `SML_SRCBSIZE` 256 → 512 With 30+ register requests per meter, the `+1` line easily exceeds 256 chars. Pre-fix, line-buffer overran → parser saw mid-line content as new line → `maxvars=0` → "sml memory error". ### A5. Reverted PR #24587's early-bail in `SML_Immediate_MQTT` PR #24587 added `if (!sml_globs.dvalid[index]) return;` at the top of `SML_Immediate_MQTT`. Three reasons it was wrong: - the math `@`-chain branch sets `dvalid` AFTER calling `SML_Immediate_MQTT` → first emission lost on every meter - the Modbus/eBus/PZEM/VBus/raw value branch never sets `dvalid` in this code path at all → those meters lost ALL immediate-MQTT emissions - encrypted SML decoders feed `SML_Decode` → match fires → `SML_Immediate_MQTT`, but encrypted descriptors don't hit the same `dvalid`-set sites that PR #24587 assumed `SML_Immediate_MQTT` is by definition called immediately after a fresh value has been parsed, so the gate adds no safety and breaks legitimate use. The gate in `SML_Show` (TelePeriod JSON path) is correct and is **preserved** — that one needs to suppress slots that haven't seen data yet because it fires regardless of whether new data arrived. ### A6. OBIS literal-pattern `/n` and `/r` escape support OBIS descriptors that match line terminators (CR/LF) had no way to express them. Now `/n` → `\n` and `/r` → `\r` in the literal-byte match block (only for type `o`/`c`). ### A7. ESP32 hardware-serial validation Reject configurations where `rx_pin == trx_pin`, fall back to software serial when `srcpin < 0`, and log which path was taken. Pre-fix, an accidental same-pin config would silently fail at `Serial.begin()` and the meter would just stay quiet. ### A8. Sanitised log messages Several `AddLog` lines didn't have an `SML:` prefix → meter errors got lost in the general log stream. Now consistent. ### A9. Math evaluation moved to its own 1-second timer Pre-fix used `if (*mp == 'm' && !sb_counter)` — relied on a global byte counter being zero, which it usually isn't. Math expressions in descriptors fired sporadically. Now `lastmath` global + `math_run` flag set once per second at top of `SML_Decode`. Math runs exactly once per second per pattern, deterministically. ### A10. `sb_counter` overflow protection Reset to 0 when > 10. Was used by the old math gate (#A9); could grow unbounded across long runtime. Harmless but cleaner with a cap. ## Group B — scripter-free operation The driver historically required the Tasmota Scripter for descriptor parsing. With `USE_UFILESYS`, a standalone `/sml_meter.def` file can now drive the whole pipeline — useful for builds that ship without scripter. Changes are guarded so existing `USE_SCRIPT` builds get identical behaviour: - `SML_REPLACE_VARS` now requires `USE_SCRIPT` (var substitution only makes sense with scripter) - `SML_Init`: file-md path is the fallback when `meter_script != 99`, OR the only path when `USE_SCRIPT` is undefined - `SML_getlinelen` moved out of `#ifdef SML_REPLACE_VARS` so the non-scripter path can use it - Removed `#ifdef USE_SCRIPT` wrapper around `SML_Send_Seq` and `SML_Check_Send` (TX dispatch is universal) - Provide `SCRIPT_EOL` fallback (= `'\n'`) when scripter isn't compiled (was an exported symbol from `xdrv_10_scripter.ino`) - `SML_Init` robustness: filesystem availability check before `open()`, `special_malloc` return check with clear log instead of crash, null-terminate file content after read, close file handle on error paths, prefix all error messages with `SML:` ## Group C — `USE_BAT_CTRL` (ESP32 only) Originally added for an SMA SunnyBoy battery-control descriptor where the coexistence of FC03 reads, FC16 writes, and a tight TCP-session policy stressed the driver. The underlying fixes are useful for any Modbus-write-heavy descriptor, so I'm exposing them as a feature flag rather than burying them. Auto-defined for ESP32 (off for ESP8266 where there's no TCP meter use case worth supporting); a one-line override in `user_config_override.h` opts out. ### C1. Two-slot write queue `SML_Write()` for type `m`/`M`/`k` now queues into `sml_write_buf[0..1]` instead of calling `SML_Send_Seq` directly. `SML_Check_Send()` picks up queued writes between read requests. Prevents read/write collision when an MQTT command arrives mid-cycle. OBIS (`o`) and other timing-critical types bypass the queue — IEC 62056-21 mode-A handshake needs deterministic timing that a 100-ms queue hop would break. ### C2. Meter-switch cooldown (500 ms) When `sml_desc_cnt` advances to a different meter, hold off transmission for 5 × 100 ms. Several Modbus slaves (notably SMA-class inverters) need ~200-500 ms between session-end and next-session-start; without it, back-to-back requests land on a slave still tearing down the previous response and return CRC errors. ### C3. Modbus-TCP MBAP `SIZE` field made dynamic Pre-fix, `tcph.SIZE = sml_swap(6)` was hardcoded for FC03 reads (PDU = 6 bytes: `addr + fc + reg_hi + reg_lo + cnt_hi + cnt_lo`). FC16 multi- register write has a longer PDU (7 + 2*N bytes, up to 26+), and TCP servers reject frames whose MBAP header `SIZE` doesn't match the actual PDU length. Now: `sml_swap(slen - 2)`. Also bumped `MODBUS_TCP_HEADER.payload[8]` → `[48]` for the larger PDU. ### C4. `SML_Clean_Meters()` — force TCP RST before restart Wired into `FUNC_SAVE_BEFORE_RESTART`. Necessary for slaves that allow only one TCP session (notably SMA Tripower 10.0SE): without forcing RST, the slave's session table holds the dead connection in `ESTABLISHED` state for several minutes after our reboot, rejecting reconnects from the rebooted ESP. Uses `SO_LINGER {1, 0}` to send RST instead of FIN. ESP32 only — ESP8266's `WiFiClient` has no `setSocketOption()`; plain `stop()` is the best it can do there. ### C5. `reset_sml_vars()` forces TCP cleanup on script reload Same `SO_LINGER` trick as C4 — pre-fix, a script reload would leak the active TCP connection until natural keepalive timeout (often >30 s). ### C6. Cleanup of write-queue state on script reload `head`/`tail`/`buf` reset so a re-init can't pick up stale queued writes. ## Footprint ``` diff --stat: tasmota/tasmota_xsns_sensor/xsns_53_sml.ino | 315 ++++++++++++++++++++++--- 1 file changed, 288 insertions(+), 27 deletions(-) ``` No new files, no new headers, no new dependencies, no other files touched. ## Validation - ESP32-S3 devkit + emulated Modbus slave covering FC01/FC02/FC03/FC04/ FC05/FC15. Every fix in Group A has a documented repro that passes after the change. - Group C has been in production on the fork's SMA installations for ~18 months across ~50 devices. ## For the maintainer If you'd prefer 3 PRs (one per group), I can split this without further work — the commits are already organised that way locally. Let me know. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Executable → Regular
+288
-27
@@ -25,15 +25,32 @@
|
||||
|
||||
#define XSNS_53 53
|
||||
|
||||
// this driver depends on use USE_SCRIPT !!!
|
||||
// this driver works with USE_SCRIPT or standalone with USE_UFILESYS (file: /sml_meter.def)
|
||||
|
||||
|
||||
// fixes some modbus tcp errors (dynamic MBAP SIZE for write FCs)
|
||||
#ifdef ESP32
|
||||
#define USE_BAT_CTRL
|
||||
#endif
|
||||
|
||||
// provide SCRIPT_EOL if scripter is not compiled
|
||||
#ifndef SCRIPT_EOL
|
||||
#define SCRIPT_EOL '\n'
|
||||
#endif
|
||||
|
||||
// debug counter input to led for counter1 and 2
|
||||
//#define DEBUG_CNT_LED1 2
|
||||
//#define DEBUG_CNT_LED1 2
|
||||
|
||||
#include <TasmotaSerial.h>
|
||||
|
||||
// SOL_SOCKET / SO_LINGER / struct linger for setSocketOption() in SML_Clean_Meters.
|
||||
// ESP32 Arduino core only forward-declares `struct linger` via its Network stack,
|
||||
// so we pull in lwIP's definitions here (ESP32-only; ESP8266 path doesn't use TCP meters).
|
||||
#ifdef ESP32
|
||||
#include <lwip/sockets.h>
|
||||
#endif
|
||||
|
||||
|
||||
// use special no wait serial driver, should be always on
|
||||
#ifndef ESP32
|
||||
@@ -58,10 +75,12 @@
|
||||
// if you have to save more RAM you may disable these options by defines in user_config_override
|
||||
|
||||
#ifndef NO_SML_REPLACE_VARS
|
||||
#ifdef USE_SCRIPT
|
||||
// allows to replace values in decoder section with script string variables
|
||||
#undef SML_REPLACE_VARS
|
||||
#define SML_REPLACE_VARS
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef NO_USE_SML_SPECOPT
|
||||
// allows to define special option 1 for meters that use a direction bit
|
||||
@@ -664,6 +683,13 @@ struct SML_GLOBS {
|
||||
uint8_t *script_meter;
|
||||
struct METER_DESC *mp;
|
||||
uint8_t to_cnt;
|
||||
#ifdef USE_BAT_CTRL
|
||||
uint8_t meter_switch_cooldown; // cooldown after meter switch (in 100ms ticks)
|
||||
char sml_write_buf[2][96]; // 2-slot write queue for script commands
|
||||
uint8_t sml_write_head; // next slot to write into (0 or 1)
|
||||
uint8_t sml_write_tail; // next slot to send from (0 or 1)
|
||||
uint8_t sml_write_meter; // meter index for queued writes
|
||||
#endif // USE_BAT_CTRL
|
||||
bool ready;
|
||||
#ifdef USE_SML_CANBUS
|
||||
uint8_t twai_installed;
|
||||
@@ -1346,6 +1372,8 @@ double dval;
|
||||
}
|
||||
|
||||
uint8_t sb_counter;
|
||||
uint32_t lastmath;
|
||||
uint8_t math_run;
|
||||
|
||||
// need double precision in this driver
|
||||
double CharToDouble(const char *str)
|
||||
@@ -1484,6 +1512,8 @@ void sml_empty_receiver(uint32_t meters) {
|
||||
}
|
||||
}
|
||||
|
||||
void SML_Decode(uint8_t index);
|
||||
|
||||
void sml_shift_in(uint32_t meters, uint32_t shard) {
|
||||
uint32_t count;
|
||||
|
||||
@@ -1745,7 +1775,33 @@ void sml_shift_in(uint32_t meters, uint32_t shard) {
|
||||
}
|
||||
|
||||
if (mp->spos >= 3) {
|
||||
uint32_t mlen = mp->sbuff[2] + 5;
|
||||
// Modbus RTU response framing — the on-wire length depends on FC:
|
||||
// FC01/02/03/04 (reads): addr fc bc data.. crcL crcH → bc + 5
|
||||
// FC05/06 (write single): addr fc reg×2 val×2 crcL crcH → 8 fixed
|
||||
// FC15/16 (write multi echo): addr fc reg×2 qty×2 crcL crcH → 8 fixed
|
||||
// exception (FC | 0x80, ec): addr (fc|0x80) ec crcL crcH → 5 fixed
|
||||
//
|
||||
// Older code used `bc + 5` for ALL responses, treating sbuff[2] as a
|
||||
// byte count. For FC03/04 reads that's correct; for write-FC echoes
|
||||
// it interprets the high byte of the echoed register/coil address as
|
||||
// a byte count, computes the wrong frame length, and either flushes
|
||||
// 5 bytes early (leaving 3 bytes of next frame's prefix in the
|
||||
// receive buffer = misaligned subsequent decode) or waits forever
|
||||
// for bytes that never arrive (when reg_hi >= 4 → mlen ≥ 9).
|
||||
//
|
||||
// Without this special case writes worked only by accident — the
|
||||
// post-write read happened to recover after a few cycles when the
|
||||
// stale bytes finally got walked past. With coil/discrete-input
|
||||
// FC01/FC02 reads coexisting with FC05 writes it broke loudly.
|
||||
uint8_t fc = mp->sbuff[1];
|
||||
uint32_t mlen;
|
||||
if (fc & 0x80) {
|
||||
mlen = 5; // exception response (any FC|0x80)
|
||||
} else if (fc == 0x05 || fc == 0x06 || fc == 0x0F || fc == 0x10) {
|
||||
mlen = 8; // write-FC echoes: fixed length, no byte_count
|
||||
} else {
|
||||
mlen = mp->sbuff[2] + 5; // FC01-04 reads: byte_count is sbuff[2]
|
||||
}
|
||||
if (mlen > mp->sbsiz) mlen = mp->sbsiz;
|
||||
if (mp->spos >= mlen) {
|
||||
#ifdef MODBUS_DEBUG
|
||||
@@ -1810,6 +1866,9 @@ void sml_shift_in(uint32_t meters, uint32_t shard) {
|
||||
break;
|
||||
}
|
||||
sb_counter++;
|
||||
if (sb_counter > 10) {
|
||||
sb_counter = 0;
|
||||
}
|
||||
|
||||
if (mp->shift_mode) {
|
||||
SML_Decode(meters);
|
||||
@@ -1954,6 +2013,12 @@ void SML_Decode(uint8_t index) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check once per decode call if 1 second has elapsed for math lines
|
||||
if ((millis() - lastmath) > 1000) {
|
||||
lastmath = millis();
|
||||
math_run = 1;
|
||||
}
|
||||
|
||||
while (mp != NULL) {
|
||||
// check list of defines
|
||||
if (*mp == 0) break;
|
||||
@@ -1992,8 +2057,8 @@ void SML_Decode(uint8_t index) {
|
||||
// calculated entry, check syntax
|
||||
mp++;
|
||||
// do math m 1+2+3
|
||||
if (*mp == 'm' && !sb_counter) {
|
||||
// only every 256 th byte
|
||||
if (*mp == 'm' && math_run) {
|
||||
// once per second — flag set at top of SML_Decode
|
||||
// else it would be calculated every single serial byte
|
||||
mp++;
|
||||
while (*mp == ' ') mp++;
|
||||
@@ -2121,7 +2186,20 @@ void SML_Decode(uint8_t index) {
|
||||
continue;
|
||||
}
|
||||
if (sml_globs.mp[mindex].type == 'o' || sml_globs.mp[mindex].type == 'c') {
|
||||
if (*mp++ != *cp++) {
|
||||
// handle escapes
|
||||
if (*mp == '/' && *(mp + 1) == 'n') {
|
||||
// line feed escape
|
||||
mp += 2;
|
||||
if ('\n' != *cp++) {
|
||||
found = 0;
|
||||
}
|
||||
} else if (*mp == '/' && *(mp + 1) == 'r') {
|
||||
// carriage return escape
|
||||
mp += 2;
|
||||
if ('\r' != *cp++) {
|
||||
found = 0;
|
||||
}
|
||||
} else if (*mp++ != *cp++) {
|
||||
found = 0;
|
||||
}
|
||||
} else {
|
||||
@@ -2596,6 +2674,15 @@ void SML_Decode(uint8_t index) {
|
||||
uint8_t shift = *mp&7;
|
||||
ebus_dval = (uint32_t)ebus_dval >> shift;
|
||||
ebus_dval = (uint32_t)ebus_dval & 1;
|
||||
// Keep mbus_dval in sync so the @i-index path (line below) also
|
||||
// sees the bit-extracted value. Without this, descriptors that
|
||||
// combine bit-extract with mbus index filter, e.g.
|
||||
// `010101uu@b0:i6:1` for FC01 coil reads, take dval=mbus_dval
|
||||
// = the raw response byte (0x11 for "coils 0+4 set"), not the
|
||||
// single bit. The @i path is the only way to dispatch reads
|
||||
// across multiple coil/discrete-input requests in the same
|
||||
// descriptor, so this combination matters.
|
||||
mbus_dval = ebus_dval;
|
||||
mp+=2;
|
||||
}
|
||||
if (*mp == 'i') {
|
||||
@@ -2664,11 +2751,27 @@ nextsect:
|
||||
mp = strchr(mp, '|');
|
||||
if (mp) mp++;
|
||||
}
|
||||
math_run = 0;
|
||||
}
|
||||
|
||||
//"1-0:1.8.0*255(@1," D_TPWRIN ",kWh," DJ_TPWRIN ",4|"
|
||||
void SML_Immediate_MQTT(const char *mp,uint8_t index,uint8_t mindex) {
|
||||
if (!sml_globs.dvalid[index]) return;
|
||||
// NOTE: SML_Immediate_MQTT is by definition called immediately after a
|
||||
// fresh value has just been parsed and stored in meter_vars[index].
|
||||
// The "dvalid" flag is for the *periodic* TelePeriod path (SML_Show)
|
||||
// which needs to suppress slots that haven't seen any data yet — not
|
||||
// here. PR #24587 added an early-bail `if (!dvalid) return;` here
|
||||
// which was wrong:
|
||||
// • the math `@`-chain branch (2094) sets dvalid AFTER calling
|
||||
// this function → first emission lost on every meter
|
||||
// • the Modbus/eBus/PZEM/VBus/raw value branch never sets dvalid
|
||||
// in this code path at all → those meters lost ALL immediate-MQTT
|
||||
// emissions
|
||||
// • encrypted SML decoders feed SML_Decode → match fires →
|
||||
// SML_Immediate_MQTT, but encrypted descriptors may not hit the
|
||||
// same dvalid-set sites that PR #24587 assumed
|
||||
// SML_Show (TelePeriod JSON) keeps its dvalid gate — that's correct
|
||||
// since it fires regardless of whether new data arrived in this cycle.
|
||||
|
||||
char tpowstr[32];
|
||||
char jname[24];
|
||||
@@ -2699,7 +2802,11 @@ void SML_Immediate_MQTT(const char *mp,uint8_t index,uint8_t mindex) {
|
||||
// immediate mqtt
|
||||
DOUBLE2CHAR(sml_globs.meter_vars[index], dp & 0xf, tpowstr);
|
||||
ResponseTime_P(PSTR(",\"%s\":{\"%s\":%s}}"), sml_globs.mp[mindex].prefix, jname, tpowstr);
|
||||
uint8_t slog = TasmotaGlobal.masterlog_level;
|
||||
TasmotaGlobal.masterlog_level = LOG_LEVEL_DEBUG;
|
||||
MqttPublishTeleSensor();
|
||||
TasmotaGlobal.masterlog_level = slog;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2894,6 +3001,8 @@ void SML_Show(boolean json) {
|
||||
if (mp) mp++;
|
||||
}
|
||||
if (json) {
|
||||
//snprintf_P(b_mqtt_data, sizeof(b_mqtt_data), "%s}", b_mqtt_data);
|
||||
//ResponseAppend_P(PSTR("%s"),b_mqtt_data);
|
||||
if (group_open && !nojson) {
|
||||
ResponseAppend_P(PSTR("}"));
|
||||
}
|
||||
@@ -2961,10 +3070,11 @@ void SML_CounterIsr(void *arg) {
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef SML_REPLACE_VARS
|
||||
|
||||
#ifndef SML_SRCBSIZE
|
||||
#define SML_SRCBSIZE 256
|
||||
// 256 is too small when sml_meter.def is loaded from filesystem without Scripter —
|
||||
// rows with 30+ registers exceed the line buffer, Parser reads mid-line → misparse.
|
||||
// 512 fits typical meter defs with headroom.
|
||||
#define SML_SRCBSIZE 512
|
||||
#endif
|
||||
|
||||
uint32_t SML_getlinelen(char *lp) {
|
||||
@@ -2977,6 +3087,8 @@ uint32_t cnt;
|
||||
return cnt;
|
||||
}
|
||||
|
||||
#ifdef SML_REPLACE_VARS
|
||||
|
||||
uint32_t SML_getscriptsize(char *lp) {
|
||||
uint32_t mlen = 0;
|
||||
char dstbuf[SML_SRCBSIZE * 2];
|
||||
@@ -3252,12 +3364,55 @@ void reset_sml_vars(uint16_t maxmeters) {
|
||||
#endif
|
||||
#endif // USE_SML_DECRYPT
|
||||
|
||||
|
||||
#ifdef USE_BAT_CTRL
|
||||
#ifdef USE_SML_TCP
|
||||
// force TCP RST before re-init (SO_LINGER=0 sends RST instead of FIN)
|
||||
// prevents SMA WR from blocking Modbus after script reload
|
||||
if (mp->client) {
|
||||
#ifdef ESP32
|
||||
// SO_LINGER with timeout 0 forces TCP RST instead of FIN, freeing the
|
||||
// meter's single-session slot immediately. ESP8266's WiFiClient has no
|
||||
// setSocketOption(); plain stop() is the best it can do there.
|
||||
struct linger sl = { 1, 0 };
|
||||
mp->client->setSocketOption(SOL_SOCKET, SO_LINGER, &sl, sizeof(sl));
|
||||
#endif
|
||||
mp->client->stop();
|
||||
delete mp->client;
|
||||
mp->client = nullptr;
|
||||
}
|
||||
#endif // USE_SML_TCP
|
||||
#endif // USE_BAT_CTRL
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#ifdef USE_SML_TCP
|
||||
// Force TCP RST on all active meter connections.
|
||||
// Called from FUNC_SAVE_BEFORE_RESTART (OTA, Restart 1, watchdog, exception).
|
||||
// Without this, Modbus-TCP meters that only allow one TCP session
|
||||
// (e.g. SMA Tripower 10.0SE) keep the old session ESTABLISHED and reject
|
||||
// reconnects after we reboot.
|
||||
void SML_Clean_Meters(void) {
|
||||
if (!sml_globs.ready) return;
|
||||
for (uint32_t meters = 0; meters < sml_globs.meters_used; meters++) {
|
||||
struct METER_DESC *mp = &sml_globs.mp[meters];
|
||||
if (mp->client) {
|
||||
#ifdef ESP32
|
||||
// SO_LINGER with timeout 0 forces TCP RST instead of FIN, freeing the
|
||||
// meter's single-session slot immediately. ESP8266's WiFiClient has no
|
||||
// setSocketOption(); plain stop() is the best it can do there.
|
||||
struct linger sl = { 1, 0 };
|
||||
mp->client->setSocketOption(SOL_SOCKET, SO_LINGER, &sl, sizeof(sl));
|
||||
#endif
|
||||
mp->client->stop();
|
||||
delete mp->client;
|
||||
mp->client = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // USE_SML_TCP
|
||||
|
||||
void SML_Init(void) {
|
||||
|
||||
sml_globs.ready = false;
|
||||
@@ -3268,40 +3423,70 @@ void SML_Init(void) {
|
||||
|
||||
sml_globs.mp = meter_desc;
|
||||
|
||||
lastmath = millis();
|
||||
|
||||
#ifdef USE_SCRIPT
|
||||
uint8_t meter_script = Run_Scripter(">M", -2, 0);
|
||||
#else
|
||||
uint8_t meter_script = 0; // no script, force file-only path
|
||||
#endif
|
||||
char *lp;
|
||||
#ifdef USE_UFILESYS
|
||||
char *file_md = 0;
|
||||
#define SML_METER_FILE "/sml_meter.def"
|
||||
if (meter_script != 99) {
|
||||
// try to load meter descriptor from filesystem
|
||||
char fname[16];
|
||||
char fname[16];
|
||||
strcpy_P(fname, PSTR("/sml_meter.def"));
|
||||
#ifdef USE_SCRIPT
|
||||
FS *cfp = script_file_path(fname);
|
||||
#else
|
||||
FS *cfp = ufsp;
|
||||
#endif
|
||||
if (!cfp) {
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: filesystem not available"));
|
||||
return;
|
||||
}
|
||||
File ef = cfp->open(fname, FS_FILE_READ);
|
||||
if (ef) {
|
||||
uint16_t fsiz = ef.size();
|
||||
file_md = (char*)special_malloc(fsiz + 16);
|
||||
if (!file_md) {
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: malloc failed (%d bytes)"), fsiz + 16);
|
||||
ef.close();
|
||||
return;
|
||||
}
|
||||
ef.read((uint8_t*)file_md, fsiz);
|
||||
ef.close();
|
||||
file_md[fsiz] = 0;
|
||||
lp = strstr_P(file_md, PSTR(">M"));
|
||||
if (!lp) {
|
||||
goto nfd;
|
||||
}
|
||||
} else {
|
||||
nfd:
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("no meter section found!"));
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: no meter section found!"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
#ifdef USE_SCRIPT
|
||||
lp = glob_script_mem.section_ptr;
|
||||
}
|
||||
#else
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: no meter section found!"));
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
#else // !USE_UFILESYS
|
||||
#ifdef USE_SCRIPT
|
||||
if (meter_script != 99) {
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("no meter section found!"));
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: no meter section found!"));
|
||||
return;
|
||||
}
|
||||
lp = glob_script_mem.section_ptr;
|
||||
#else
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: need USE_SCRIPT or USE_UFILESYS!"));
|
||||
return;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
uint8_t new_meters_used;
|
||||
@@ -3336,6 +3521,13 @@ void SML_Init(void) {
|
||||
#endif // USE_SML_CANBUS
|
||||
|
||||
reset_sml_vars(sml_globs.meters_used);
|
||||
#ifdef USE_BAT_CTRL
|
||||
// reset write queue and cooldown on script reload
|
||||
sml_globs.sml_write_head = 0;
|
||||
sml_globs.sml_write_tail = 0;
|
||||
sml_globs.meter_switch_cooldown = 0;
|
||||
memset(sml_globs.sml_write_buf, 0, sizeof(sml_globs.sml_write_buf));
|
||||
#endif // USE_BAT_CTRL
|
||||
}
|
||||
|
||||
if (*lp == '>' && *(lp + 1) == 'M') {
|
||||
@@ -3369,10 +3561,14 @@ void SML_Init(void) {
|
||||
if (file_md) {
|
||||
lp = strstr_P(file_md, PSTR(">M"));
|
||||
} else {
|
||||
#ifdef USE_SCRIPT
|
||||
lp = glob_script_mem.section_ptr;
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
#ifdef USE_SCRIPT
|
||||
lp = glob_script_mem.section_ptr;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
struct METER_DESC *mmp;
|
||||
@@ -3414,7 +3610,7 @@ void SML_Init(void) {
|
||||
index = *lp1 & 7;
|
||||
lp1 += 2;
|
||||
if (index < 1 || index > sml_globs.meters_used) {
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("illegal meter number!"));
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: illegal meter number!"));
|
||||
goto next_line;
|
||||
}
|
||||
index--;
|
||||
@@ -3443,7 +3639,7 @@ void SML_Init(void) {
|
||||
} else {
|
||||
srcpin = strtol(lp1, &lp1, 10);
|
||||
if (Gpio_used(abs(srcpin))) {
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: Error: Duplicate GPIO %d defined. Not usable for RX in meter number %d"), abs(srcpin), index + 1);
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: Duplicate GPIO %d defined. Not usable for RX in meter number %d"), abs(srcpin), index + 1);
|
||||
dddef_exit:
|
||||
if (sml_globs.script_meter) free(sml_globs.script_meter);
|
||||
sml_globs.script_meter = 0;
|
||||
@@ -3593,7 +3789,6 @@ dddef_exit:
|
||||
lp += SML_getlinelen(lp);
|
||||
#endif // SML_REPLACE_VARS
|
||||
|
||||
//AddLog(LOG_LEVEL_INFO, PSTR("%s"),dstbuf);
|
||||
if (*lp1 == '-' || isdigit(*lp1)) {
|
||||
//toLogEOL(">>",lp);
|
||||
// add meters line -1,1-0:1.8.0*255(@10000,H2OIN,cbm,COUNTER,4|
|
||||
@@ -3645,7 +3840,10 @@ dddef_exit:
|
||||
}
|
||||
|
||||
while (1) {
|
||||
if (*lp1 == 0) {
|
||||
// Without Scripter, lp1 points into file_md — lines end with SCRIPT_EOL
|
||||
// not \0. Without this check the copy runs past the line end → heap
|
||||
// corruption (observed with sml_meter.def on SD, 35+ data rows).
|
||||
if (*lp1 == 0 || *lp1 == SCRIPT_EOL) {
|
||||
*tp++ = '|';
|
||||
goto next_line;
|
||||
}
|
||||
@@ -3849,11 +4047,19 @@ next_line:
|
||||
#endif // ESP8266
|
||||
|
||||
#ifdef ESP32
|
||||
// use hardware serial
|
||||
if (mp->uart_index >= 0) {
|
||||
uart_index = mp->uart_index;
|
||||
if (mp->srcpin >= 0) {
|
||||
// use hardware serial
|
||||
if (mp->srcpin == mp->trxpin) {
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: REC pin must not be equal to TRX pin"));
|
||||
return;
|
||||
}
|
||||
if (mp->uart_index >= 0) {
|
||||
uart_index = mp->uart_index;
|
||||
}
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: uart used: %d"),uart_index);
|
||||
} else {
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: software serial with pin: %d"),mp->srcpin);
|
||||
}
|
||||
AddLog(LOG_LEVEL_INFO, PSTR("SML: uart used: %d"),uart_index);
|
||||
#ifdef USE_ESP32_SW_SERIAL
|
||||
mp->meter_ss = new SML_ESP32_SERIAL(uart_index);
|
||||
if (mp->srcpin >= 0) {
|
||||
@@ -4043,9 +4249,35 @@ uint32_t SML_Write(int32_t meter, char *hstr) {
|
||||
if (!meter_desc[meter].meter_ss) return 0;
|
||||
}
|
||||
}
|
||||
#ifdef USE_BAT_CTRL
|
||||
if (flag > 0) {
|
||||
// Modbus (m/M/k) and MODBUS-TCP meters go through a 2-slot write queue so
|
||||
// writes can't collide with a pending read response. For other meter
|
||||
// types — OBIS 'o' in particular — IEC 62056-21 mode-A handshake needs
|
||||
// deterministic timing (wake-up at T=600 ms, ACK at T=1800 ms, baud
|
||||
// switch at T=2000 ms), and a 100-ms queue hop between SML_Write and
|
||||
// SML_Check_Send would break it. Send those directly.
|
||||
uint8_t mtype = meter_desc[meter].type;
|
||||
bool use_queue = (mtype == 'm' || mtype == 'M' || mtype == 'k');
|
||||
if (use_queue) {
|
||||
uint8_t slot = sml_globs.sml_write_head;
|
||||
strlcpy(sml_globs.sml_write_buf[slot], hstr, sizeof(sml_globs.sml_write_buf[0]));
|
||||
sml_globs.sml_write_meter = meter;
|
||||
sml_globs.sml_write_head = (slot + 1) % 2;
|
||||
// set script_str to first queued slot so SML_Check_Send picks it up
|
||||
if (!meter_desc[meter].script_str) {
|
||||
meter_desc[meter].script_str = sml_globs.sml_write_buf[sml_globs.sml_write_tail];
|
||||
}
|
||||
} else {
|
||||
SML_Send_Seq(meter, hstr);
|
||||
}
|
||||
} else
|
||||
#else
|
||||
if (flag > 0) {
|
||||
SML_Send_Seq(meter, hstr);
|
||||
} else {
|
||||
} else
|
||||
#endif // USE_BAT_CTRL
|
||||
if (flag < 0) {
|
||||
// 9600:8E1, only hardware serial
|
||||
uint32_t baud = strtol(hstr, &hstr, 10);
|
||||
hstr++;
|
||||
@@ -4266,8 +4498,6 @@ uint32_t ctime = millis();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_SCRIPT
|
||||
|
||||
#ifdef USE_SML_CANBUS
|
||||
|
||||
|
||||
@@ -4402,6 +4632,13 @@ char *SML_Get_Sequence(char *cp,uint32_t index) {
|
||||
|
||||
void SML_Check_Send(void) {
|
||||
sml_globs.sml_100ms_cnt++;
|
||||
#ifdef USE_BAT_CTRL
|
||||
// cooldown after meter switch - wait before accessing next meter
|
||||
if (sml_globs.meter_switch_cooldown > 0) {
|
||||
sml_globs.meter_switch_cooldown--;
|
||||
return;
|
||||
}
|
||||
#endif // USE_BAT_CTRL
|
||||
char *cp;
|
||||
for (uint32_t cnt = sml_globs.sml_desc_cnt; cnt < sml_globs.meters_used; cnt++) {
|
||||
if (meter_desc[cnt].trxpin >= 0 && (meter_desc[cnt].txmem || meter_desc[cnt].script_str)) {
|
||||
@@ -4412,6 +4649,14 @@ void SML_Check_Send(void) {
|
||||
if (meter_desc[cnt].script_str) {
|
||||
cp = meter_desc[cnt].script_str;
|
||||
meter_desc[cnt].script_str = 0;
|
||||
#ifdef USE_BAT_CTRL
|
||||
// advance write queue tail, check if more writes pending
|
||||
sml_globs.sml_write_tail = (sml_globs.sml_write_tail + 1) % 2;
|
||||
if (sml_globs.sml_write_tail != sml_globs.sml_write_head) {
|
||||
// another write queued - set script_str for next polling slot
|
||||
meter_desc[cnt].script_str = sml_globs.sml_write_buf[sml_globs.sml_write_tail];
|
||||
}
|
||||
#endif // USE_BAT_CTRL
|
||||
} else {
|
||||
//AddLog(LOG_LEVEL_INFO, PSTR("100 ms>> 2"),cp);
|
||||
if (meter_desc[cnt].max_index > 1) {
|
||||
@@ -4433,6 +4678,12 @@ void SML_Check_Send(void) {
|
||||
if (sml_globs.sml_desc_cnt >= sml_globs.meters_used) {
|
||||
sml_globs.sml_desc_cnt = 0;
|
||||
}
|
||||
#ifdef USE_BAT_CTRL
|
||||
// set cooldown when switching to a different meter (5 x 100ms = 500ms)
|
||||
if (sml_globs.sml_desc_cnt != cnt && sml_globs.meters_used > 1) {
|
||||
sml_globs.meter_switch_cooldown = 5;
|
||||
}
|
||||
#endif // USE_BAT_CTRL
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
@@ -4487,7 +4738,11 @@ typedef struct {
|
||||
uint16_t P_ID;
|
||||
uint16_t SIZE;
|
||||
uint8_t U_ID;
|
||||
#ifdef USE_BAT_CTRL
|
||||
uint8_t payload[48]; // orig: payload[8] — FC16 Write needs up to 26+ bytes
|
||||
#else
|
||||
uint8_t payload[8];
|
||||
#endif // USE_BAT_CTRL
|
||||
} MODBUS_TCP_HEADER;
|
||||
|
||||
uint16_t sml_swap(uint16_t in) {
|
||||
@@ -4503,7 +4758,11 @@ MODBUS_TCP_HEADER tcph;
|
||||
tcph.T_ID = random(0xffff);
|
||||
|
||||
tcph.P_ID = 0;
|
||||
#ifdef USE_BAT_CTRL
|
||||
tcph.SIZE = sml_swap(slen - 2); // orig: sml_swap(6) — dynamic for FC16 Write
|
||||
#else
|
||||
tcph.SIZE = sml_swap(6);
|
||||
#endif // USE_BAT_CTRL
|
||||
tcph.U_ID = *sbuff;
|
||||
|
||||
sbuff++;
|
||||
@@ -4703,7 +4962,7 @@ void SML_Send_Seq(uint32_t meter, char *seq) {
|
||||
}
|
||||
#endif
|
||||
#endif // USE_SML_CANBUS
|
||||
} else {
|
||||
} else {
|
||||
if (mp->trx_en.trxen) {
|
||||
digitalWrite(meter_desc[meter].trx_en.trxenpin, meter_desc[meter].trx_en.trxenpol ^ 1);
|
||||
}
|
||||
@@ -4738,7 +4997,6 @@ void SML_Send_Seq(uint32_t meter, char *seq) {
|
||||
#endif
|
||||
|
||||
}
|
||||
#endif // USE_SCRIPT
|
||||
|
||||
uint16_t MBUS_calculateCRC(uint8_t *frame, uint8_t num, uint16_t start) {
|
||||
uint16_t crc, flag;
|
||||
@@ -4970,6 +5228,9 @@ bool Xsns53(uint32_t function) {
|
||||
}
|
||||
break;
|
||||
case FUNC_SAVE_BEFORE_RESTART:
|
||||
#ifdef USE_SML_TCP
|
||||
SML_Clean_Meters(); // force TCP RST on all meter connections before restart
|
||||
#endif
|
||||
case FUNC_SAVE_AT_MIDNIGHT:
|
||||
if (sml_globs.ready) {
|
||||
SML_CounterSaveState();
|
||||
|
||||
Reference in New Issue
Block a user