mirror of
https://github.com/arendst/Tasmota.git
synced 2026-07-27 20:05:46 +00:00
development
1
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f290dd99c9 |
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) |