timeRequest built both 112-byte descriptions (with a String copy of the URL)
on async_tcp for every request, and logElapsed threw them away for anything
under 100ms. Check worthLogging() first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The log lock is a binary semaphore, so when the radio task preempted loop()
mid-log every log from async_tcp waited the full 20ms for a holder that
couldn't let go until the radio job ended. Waiting now stops as soon as
the holder is preempted on another core. The rule lives in
LogLockPolicy.h with unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The emit to a newly connected client also stored the RSSI and time, which
restarted the 10s hold-off and hid a real change from the clients already
connected. The baseline moves into RssiBroadcast, which ignores
per-client emits, with unit tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review flagged the outer My/Favorite losing to the nested Up/Down it queues.
Every such nested send is from a simMy shade, where the Up/Down is what
starts the motor, so the behaviour stays. The hold moves into TxHold in
ShadeMotion with a comment explaining the order and unit tests pinning it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The frame was popped before queueTx(), so whenever the radio queue was busy
it was lost. Peek it instead, only remove it once it is queued (or TX is
suspended), and don't try while the queue has no room. The repeater buffer
moves to TxBuffer.h/.cpp so it can be unit tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
app_elf_sha256 has no terminator (IDF 4.4 fills all 17 bytes), so strcmp
never matched the running build and %s printed whatever followed it. Copy
it into a terminated buffer first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every append found the end of the buffer with strlen(), up to four
times per append and four appends per field, so building a response
cost the square of its size. /discovery fills a 4 KB buffer and spent
about 195 ms of its ~210 ms there (shades 126 ms, groups 69 ms), all on
the async_tcp task that serves every socket and web request.
Keep the length in the formatter and write every append at the end
through one helper. The web response and socket event subclasses and
ControllerChunker use the tracked length too. Output is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At -70 dBm the RSSI moves a dBm or two constantly, so the >1 dBm check
every 1.5s sent wifiStrength to every client about 270 times an hour.
That is extra traffic on an already lossy link and a state write in
Home Assistant for each one.
Broadcast on a channel change, or for a change of at least 3 dBm no
more than every 10s. A newly connected client still gets the current
value straight away.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests travelPosition (part-way positions both ways, separate up and
down times, stopping at the ends, zero travel time, steady progress
and staying within 0-100), displayPosition with and without flipping,
and flipCommand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When something didn't fit, the formatters skipped it silently, closing
brackets included, and clients got malformed JSON. Two writes could
also go past the buffer:
- AsyncJsonResp flushed when full and then wrote the value without
checking again, so a value bigger than the whole buffer overflowed.
It is now streamed straight out instead.
- JsonSockEvent::closeEvent added the closing ] when only one byte was
left, putting the terminator one byte past the buffer.
The formatters now set an overflowed() flag and log an error, and a
socket event that overflowed is logged and not sent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests BufferedJsonFormatter output: nesting and commas, number and bool
values, skipped null strings and escaping. A canary after the buffer
catches writes past its end.
Three fail: control characters other than \b\f\n\r\t aren't escaped,
calcEscapedLength skips the first character, and so a value starting
with an escaped character can be written one byte past the buffer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds SomfyFrame.cpp in the native env, with a minimal String and
no-op ESP_LOGx in the stubs, and tests:
- a fixed 56-bit frame, byte for byte, so what goes on air is pinned
- encode/decode round trips for 56-bit, 80-bit (repeats, Favorite,
Stop, steps, Toggle), RTW and RTV frames
- rejection of bad checksums, addresses, rolling codes, keys and
80-bit parity
- repeat/synonym tracking in copy()
- parsing and naming of commands
The command-name round trip fails: names the firmware reports, such as
"Step Up" and "My+Up", parse back to the wrong command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds an [env:native] for host-side unit tests (pio test -e native) with
a minimal Arduino.h stub, and covers the Utils helpers: _trim,
toBoolean, SETCHARPROP, sortArray and the Timestamp conversions.
The firmware envs now extend a shared [esp32] section instead of [env]
so the native env doesn't inherit the Arduino framework, the ESP32
libraries or the build scripts.
The half-hour calcTZOffset cases fail: +05:30 comes back as 2300.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>