mqttStart() called MQTTAsync_create()/MQTTClient_create() on every
invocation, including reconnects from mqttEvery5Seconds(). This leaked
the old Paho client handle without destroying it. The old client's
internal threads would eventually reconnect using the same MQTT
clientID, causing the broker to disconnect the new client ("session
taken over"), creating a permanent connect/disconnect loop.
Fix: create the Paho client once (guarded by mqttClientCreated flag)
and reuse it on reconnect, matching the ESP32 implementation pattern.
Properly destroy the client in mqttStop().
Affects: hasp_mqtt_paho_async.cpp (Linux, macOS, Windows async builds)
hasp_mqtt_paho_single.cpp (Windows synchronous builds)
Does not affect: ESP32/ESP8266/STM32 (different MQTT implementations)
FixesHASwitchPlate/openHASP#1005
Made-with: Cursor
- hasp_dispatch.cpp: remove extra #endif so #else correctly pairs with #if HASP_TARGET_PC
- linux_headless.ini, linux_fbdev.ini: add -std=c++14 for ArduinoJson build
- platformio.ini: add user_setups/darwin/*.ini and user_setups/linux/*.ini to extra_configs so envs are available without override (CI/Docker)
Made-with: Cursor
On PC build the MQTT callback runs on Paho's async thread. dispatch_parse_jsonl
and dispatch_parse_json call hasp_new_object() which uses LVGL APIs; LVGL is
not thread-safe. Result: segfault when a retained page-0 label is delivered on
connect (e.g. 'MSGR: Jsonl fully parsed' then crash).
Fix: when topic is command/jsonl or command/json, queue (topic, payload) and
return from the callback. Main loop drains the queue and calls dispatch_topic_
payload on the LVGL thread. Queue capped at 4 items to bound memory.
Ref: home-automation#188 (connect loop / throttle workaround removal)
Made-with: Cursor
When the backwards-compatibility path in my_obj_set_action() converts
simple action strings (e.g. "next", "prev", "back") into JSON objects,
it builds the JSON in a stack-local char json[64] inside an if-block,
then calls deserializeJson(doc, json). Because json is a mutable char*,
ArduinoJson performs zero-copy in-place parsing — storing pointers into
the buffer rather than copying the string data.
When the if-block ends, json goes out of scope and its stack memory is
freed. The subsequent serializeJson(doc, str, size) call (outside the
block) reads from those dangling pointers, producing corrupted action
strings like {"up":"page \"\u0000\u0000\u0000"} instead of the
expected {"up":"page next"}.
On 32-bit ESP32 the stack frame often gets reused in a way that
preserves the data, masking the bug. On 64-bit platforms (macOS/Linux
SDL2 builds) the different stack layout exposes the corruption, causing
"Invalid page 0" errors and broken button navigation.
The fix is to cast json to const char* before passing it to
deserializeJson, forcing ArduinoJson to copy the strings into the
document's memory pool. This is the same pattern already used throughout
hasp_dispatch.cpp, which has the comment:
"Note: Deserialization needs to be (const char *) so the objects
WILL be copied — this uses more memory but otherwise the mqtt
receive buffer can get overwritten by the send buffer !!"
Made-with: Cursor
Mirrors linux_headless but uses clang/libc++ via osx_build_extra.py.
No SDL2 dependency — suitable for CI on macOS runners.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a null display driver that uses an in-memory framebuffer with a
pthread-based tick thread — no SDL2 or display server required.
This enables running openHASP in CI/CD pipelines, Docker containers,
and headless servers.
New files:
- src/drv/tft/tft_driver_null.h / .cpp — null driver implementation
- user_setups/linux/linux_headless.ini — PlatformIO environment
The POSIX screenshot code (fopen/fwrite) works with the null driver
since it hooks the LVGL flush callback via lv_refr_now(). Tested:
MQTT screenshot command produces valid 307KB BMP in Docker.
Closesalvarolobato/home-automation#76
Co-authored-by: Cursor <cursoragent@cursor.com>
The POSIX screenshot code added in #997 uses PATH_MAX and realpath(),
which require <limits.h> on Linux. macOS provides these implicitly
through system headers, so the darwin_sdl build passed but linux_sdl
failed with 'PATH_MAX was not declared in this scope'.
Co-authored-by: Cursor <cursoragent@cursor.com>
Enable pixel-perfect screenshot capture on the Mac build by reading
directly from the LVGL framebuffer, matching the existing ESP32 behavior.
Changes:
- src/hasp_gui.cpp: Add POSIX file-based screenshot using fopen/fwrite/fclose,
extend bitmap header guard to compile on POSIX, log full absolute path
- src/hasp/hasp_dispatch.cpp: Enable the screenshot command for POSIX builds
- src/drv/tft/tft_driver_sdl2.cpp: Add F12 keyboard shortcut via SDL event watch
using a flag polled from guiLoop() for thread safety
Usage:
- MQTT: publish to hasp/<node>/command/screenshot
- Keyboard: press F12 in the simulator window
- Default output: screenshot.bmp in the working directory
Co-authored-by: Cursor <cursoragent@cursor.com>