* Add support for multi-byte characters like emojis (💡) in WebButton labels for light device toggle buttons
- Remove the 3-character buffer truncation of light device toggle button labels allowing multi-byte characters like emojis
- Rely on CSS to handle truncation and add `max-width:0` to the td so it respects the provided percentage width instead of expanding to fit content
- Wrap button content in a span with overflow handling to prevent layout breakage
* Use localized `D_BUTTON_TOGGLE` for light device button default text
* Improve UTF-8 support in light device WebButton text
- Add `Utf8Truncate` function in `support.ino` to truncate UTF-8 strings to a max chars limit or max visual width limit
- Truncate light device button text to max 4 chars or max width of 4 before passing to button HTML template
- cache calls to GetWebButton so only called once and update bool var from `set_button` to `has_web_button`.
* Small tweaks to improve clarity in Utf8Truncate on codepoint limits and visual width handling
* Correctness fixes
---------
Co-authored-by: s-hadinger <49731213+s-hadinger@users.noreply.github.com>
- Replace broken "http" and "://" in file condition with file.startswith(("http://", "https://")) — the original always evaluated "http" as truthy and only tested for "://".
- Strip whitespace and skip blank lines from custom_files_upload entries, matching the pattern in solidify-from-url.py.
- Resolve relative local paths against $PROJECT_DIR rather than the build-time CWD.
- Replace 8-bit per-channel math with 10-bit intermediates to preserve color ratios at low brightness.
- When SetOption105 (white blend mode) is enabled on a 4-byte RGBW strip, extract the minimum RGB component as a white channel (scaled by rgbwwTable[3]) rather than zeroing it, mirroring the logic in xdrv_04_light.ino.
- Gamma correction is applied via ledGamma10_10 throughout, including the extracted white channel.
- solidify_all_python.be imports tasmota_defines during solidification, but the actual file is generated post-compilation by gen-berry-defines.py. If it doesn't exist yet, solidification fails. Create an empty stub before calling prepareBerryFiles so the import succeeds regardless of build order.
* Add local file support to solidify-from-url.py and fix URL rename alias bug
- Add support for local .be file paths (absolute or PROJECT_DIR-relative) in prepareBerryFiles used by custom_berry_solidify project option in solidify-from-url.py
- Fix URL path to use "/" as separator instead of os.path.sep, and pass the rename alias to addHeaderFile so the solidified header name matches the actual target filename
* solidify-from-url: replace compiled berry binary with berry_port Python interpreter
- Remove ensureBerry() and the shell invocation of the compiled `berry` binary.
- Instead run solidify_all_python.be via `python3 -m berry_port`, matching how gen-berry-structures.py drives solidification for all other berry modules.
- Use Popen to filter routine per-file output and call env.Exit on failure.
* wrap printf with stubs
* Remove flash savings message from wrap_printf
Removed print statement indicating flash savings for ESP8266.
* safe more by using rom function
- Route single-specifier hostnames through Format() so patterns like %NX (last N MAC hex chars) work correctly, while preserving the existing two-specifier %s-%04d behavior via snprintf
- Update comment in my_user_config.h to accurately describe chip ID rather than MAC
- Position tracking (abs_position, rel_position) was previously skipped when SEESAW_ENCODER_LIKE_ROTARY light control mode was active due to an early return. Moved position updates before the light control block so Show() and Telemetry always reflect current position.
- Also adds Pos (global relative position) alongside Pos1/Pos2 in JSON telemetry responses, rule triggers, and the web UI display.
# 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)