{"id":48703067,"url":"https://github.com/labodj/lsh-core","last_synced_at":"2026-04-24T21:01:15.208Z","repository":{"id":306994965,"uuid":"1027954475","full_name":"labodj/lsh-core","owner":"labodj","description":"Core Arduino C++ framework for the LSH smart home ecosystem, designed for Controllino PLCs.","archived":false,"fork":false,"pushed_at":"2026-04-18T13:33:08.000Z","size":399,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-18T14:37:37.261Z","etag":null,"topics":["automation","avr","buttons","controller","embedded","firmware","home-automation","iot","lsh","microcontroller","mqtt","msgpack","no-heap","relay","serial","smart-home"],"latest_commit_sha":null,"homepage":"https://github.com/labodj/labo-smart-home","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/labodj.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":"CITATION.cff","codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-28T19:39:52.000Z","updated_at":"2026-04-18T13:33:12.000Z","dependencies_parsed_at":"2025-07-28T22:27:25.846Z","dependency_job_id":"a159abb5-41a5-4917-9360-38c4ea52195a","html_url":"https://github.com/labodj/lsh-core","commit_stats":null,"previous_names":["labodj/lsh-core"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/labodj/lsh-core","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/labodj%2Flsh-core","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/labodj%2Flsh-core/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/labodj%2Flsh-core/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/labodj%2Flsh-core/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/labodj","download_url":"https://codeload.github.com/labodj/lsh-core/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/labodj%2Flsh-core/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32240613,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-24T13:21:15.438Z","status":"ssl_error","status_checked_at":"2026-04-24T13:21:15.005Z","response_time":64,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","avr","buttons","controller","embedded","firmware","home-automation","iot","lsh","microcontroller","mqtt","msgpack","no-heap","relay","serial","smart-home"],"created_at":"2026-04-11T10:27:12.532Z","updated_at":"2026-04-24T21:01:15.199Z","avatar_url":"https://github.com/labodj.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# lsh-core: Controller Firmware for Labo Smart Home\n\n[![Build Status](https://github.com/labodj/lsh-core/actions/workflows/ci.yml/badge.svg)](https://github.com/labodj/lsh-core/actions/workflows/ci.yml)\n[![Latest Release](https://img.shields.io/github/v/release/labodj/lsh-core?display_name=tag\u0026sort=semver)](https://github.com/labodj/lsh-core/releases/latest)\n[![API Documentation](https://img.shields.io/badge/API%20Reference-Doxygen-blue.svg)](https://labodj.github.io/lsh-core/)\n\n`lsh-core` is the controller-side firmware library for the **Labo Smart Home\n(LSH)** ecosystem. It runs on Arduino-compatible controllers, reads wired\ninputs, drives relays and indicators, and talks to an ESP32 bridge over serial.\n\nThe default public path is a Controllino-style AVR controller paired with\n`lsh-bridge`, MQTT and Node-RED. The library is still useful to study or reuse\non its own, but the documented adoption path assumes the full stack.\n\nSince `v3.0.0`, user configuration is TOML-first. You describe devices,\nbuttons, relays, pins and click behavior in `lsh_devices.toml`; the generator\nemits the optimized C++ profile before compilation. A typical device profile\ndoes not require C++ setup code for topology.\n\nThe hosted GitHub Pages API reference tracks the latest tagged release so the\npublic class-level documentation stays aligned with released artifacts. This\nREADME on `main` may describe newer work that has not been tagged yet.\n\nIf you are new to the public LSH stack, read the landing repository and its\nreference profile first:\n\n- [Labo Smart Home landing page](https://github.com/labodj/labo-smart-home)\n- [LSH reference stack](https://github.com/labodj/labo-smart-home/blob/main/REFERENCE_STACK.md)\n- [LSH glossary](https://github.com/labodj/labo-smart-home/blob/main/GLOSSARY.md)\n\n## What You Need\n\nFor the documented controller path:\n\n- PlatformIO\n- Python 3.11 or newer for the TOML generator\n- an Arduino-compatible AVR target; Controllino Maxi is the best documented one\n- an ESP32 running `lsh-bridge` if you want MQTT/Homie integration\n- an MQTT broker and Node-RED if you want the full public reference behavior\n\nThe library is optimized for static device topology. If your project needs\ndevices to appear and disappear at runtime, this is not the right abstraction\nwithout additional work.\n\n## Start Here\n\nUse this README in different ways depending on what you need:\n\n- If you are new to LSH, start with the landing page, the reference stack and the glossary before reading this firmware guide.\n- If you want the shortest answers to common adoption questions, skim the landing [`FAQ.md`](https://github.com/labodj/labo-smart-home/blob/main/FAQ.md).\n- If you want the shortest end-to-end bring-up path, read the landing [`GETTING_STARTED.md`](https://github.com/labodj/labo-smart-home/blob/main/GETTING_STARTED.md) before customizing this firmware.\n- If your first lab is partially alive but inconsistent, use the landing [`TROUBLESHOOTING.md`](https://github.com/labodj/labo-smart-home/blob/main/TROUBLESHOOTING.md).\n- If you want to wire a controller correctly, jump to [Hardware \u0026 Electrical Setup](#hardware--electrical-setup).\n- If you want to build your first controller project, jump to [Getting Started: Creating Your Project](#getting-started-creating-your-project).\n- If you want click semantics, fallbacks and network behavior, jump to [Configuring Device Behavior](#configuring-device-behavior).\n- If you want compile-time tuning knobs, jump to [Feature Flags](#feature-flags).\n- If you want class- and method-level details for the latest released API, use the [Doxygen API reference](https://labodj.github.io/lsh-core/).\n\n## Bundled Example\n\nThe fastest concrete starting point in this repository is:\n\n- [examples/multi-device-project](./examples/multi-device-project)\n\nIt already shows a reusable multi-device PlatformIO layout with separate device\nprofiles, a TOML source file and generated headers.\n\nUseful example profiles:\n\n- `J1_release`: leaner profile, MsgPack enabled, no network-click subsystem\n- `J2_release`: richer profile that keeps the network-click path enabled\n\nBuild it directly from this repository:\n\n```bash\nplatformio run -d examples/multi-device-project -e J1_release\nplatformio run -d examples/multi-device-project -e J2_release\n```\n\nFor the stack-level bring-up order around this example, use the landing\n[`GETTING_STARTED.md`](https://github.com/labodj/labo-smart-home/blob/main/GETTING_STARTED.md).\n\n## What is the Labo Smart Home (LSH) Ecosystem?\n\nLSH is a complete, distributed home automation system composed of four public,\nopen-source repositories:\n\n- **`lsh-core` (This Project):** The heart of the physical layer. This modern C++17 framework runs on an Arduino-compatible controller (like a Controllino). Its job is to read inputs (like push-buttons), control outputs (like relays and lights), and execute local logic with maximum speed and efficiency.\n\n- **`lsh-bridge`:** A lightweight firmware designed for an ESP32. It acts as a semi-transparent bridge, physically connecting to `lsh-core` via serial and relaying messages to and from your network via MQTT. This isolates the core logic from Wi-Fi and network concerns.\n\n- **[node-red-contrib-lsh-logic](https://github.com/labodj/node-red-contrib-lsh-logic):** A collection of nodes for Node-RED. This is the brain of your smart home, running on a server or Raspberry Pi. It listens to events from all your `lsh-core` devices and orchestrates complex, network-wide automation logic.\n\n- **[lsh-protocol](https://github.com/labodj/lsh-protocol):** The shared protocol source of truth. It keeps command IDs, compact keys, compatibility metadata and generated artifacts aligned across the controller, bridge and Node-RED layers.\n\n### Runtime Path\n\nThe active runtime path involves three peers. `lsh-protocol` sits beside them as\nthe shared contract that keeps the payload model aligned.\n\n```text\n+-----------------+                      +-----------------+                      +-----------------+\n|   lsh-core      | --(1) Click Event--\u003e |   lsh-bridge    | --(2) MQTT Publish-\u003e |   MQTT Broker   |\n|(Physical Layer) |      [Serial]        | (Gateway/Bridge)|                      |  (Message Hub)  |\n|                 | \u003c----(7) Command---- |                 | \u003c----(6) Command---- |                 |\n+-----------------+      [Serial]        +-----------------+                      +--------+--------+\n                                                                                           |\n                                                                                 (3) Event |\n                                                                                           v\n                                                                                 +--------+--------+\n                                                                                 | lsh-logic (NR)  |\n                                                                                 |  (Logic Layer)  |\n                                                                                 | --(5) Command --+\n                                                                                 +-----------------+\n```\n\n### Operational Invariants\n\nThe serial contract between `lsh-core` and `lsh-bridge` is intentionally strict:\n\n- The device topology is built during `Configurator::configure()` and is considered static until the next controller reboot.\n- Generated `LSH_STATIC_CONFIG_ACTUATORS`, `LSH_STATIC_CONFIG_CLICKABLES`, and `LSH_STATIC_CONFIG_INDICATORS` define the exact static capacity of the selected device.\n- The runtime counts are produced by the generated registration pass and must match those static capacities before the controller enters the main loop.\n- `lsh-core` sends a `BOOT` payload at startup. That payload invalidates any cached bridge-side model and forces a fresh `details + state` re-sync.\n- A topology change is only supported through reflashing + reboot. Hot runtime topology changes are out of scope by design.\n- The LSH protocol assumes a trusted environment: there is no built-in authentication or hardening against hostile peers on the serial link or MQTT path.\n- Serial transport is codec-specific: JSON uses newline-delimited frames, while MsgPack uses a delimiter-and-escape framed transport.\n\n### API Documentation\n\nWhile this README provides a comprehensive guide for getting started and common use cases, a full, in-depth API reference is also available. This documentation is automatically generated using Doxygen from the source code comments and provides detailed information on all public classes, methods, and namespaces.\n\nUse it when you need class-level details, method signatures or implementation\nnotes beyond the examples in this README.\n\nThe hosted site tracks the latest tagged release. If you are reading `main`\nbetween releases, the repository sources and this README may already include\nchanges that are not reflected on the published API pages yet.\n\n**[Browse the full API Documentation here](https://labodj.github.io/lsh-core/)**\n\n## Hardware \u0026 Electrical Setup\n\nThis section keeps the `lsh-core`-specific electrical assumptions. For the full\npublic panel pattern and the cross-repo controller/bridge split, see:\n\n- [Labo Smart Home hardware overview](https://github.com/labodj/labo-smart-home/blob/main/HARDWARE_OVERVIEW.md)\n- [LSH reference stack](https://github.com/labodj/labo-smart-home/blob/main/REFERENCE_STACK.md)\n\n`lsh-core` was designed around the **Controllino Maxi**, but can be adapted. The following setup is considered standard.\n\n### Power Supply\n\nThe controller is typically powered by a **12V or 24V DC** power supply. This voltage is referred to as `VDD` throughout the electrical schematics.\n\n### Push-Button Inputs\n\nEach physical input pin is designed to be connected to one or more push-buttons. The standard wiring is:\n\n\u003e **INPUT PIN** ← Push-Button → **VDD**\n\nWhen a button is pressed, it closes the circuit, connecting the input pin to `VDD` and signaling a \"high\" state to the controller.\n\n### Output Wiring\n\n- **Relay Outputs:** The Controllino relay outputs can be used to switch loads at **12 V / 24 V / 115 V / 230 V**, within the limits documented by the official Controllino datasheet and the rest of the installation.\n- **Low-Voltage Outputs (Digital Out):** These outputs provide a `VDD` signal and are typically used to power status LEDs and illuminated push-buttons on button panels.\n\nTypical field-model assumptions in the real installation:\n\n- wall push-buttons stay on the low-voltage side and are fed from the same controller supply (`VDD`)\n- indicator lights also stay at the controller supply voltage\n- the controller owns the direct relationship between field inputs, relays and indicator outputs\n\n### ESP32 (`lsh-bridge`) Connection\n\nFor network functionality, `lsh-core` communicates with an `lsh-bridge` device over a hardware serial port.\n\n\u003e **Crucial:** The Controllino operates at 5V logic, while the ESP32 operates at 3.3V. A **bi-directional logic level shifter** is **required** between them to prevent damage to the ESP32.\n\n- **Controllino `TX` pin** → Logic Level Shifter (HV side) → (LV side) → **ESP32 `RX` pin**\n- **Controllino `RX` pin** → Logic Level Shifter (HV side) → (LV side) → **ESP32 `TX` pin**\n\nTypically, `Serial2` on the Controllino Maxi is used for this communication.\n\n### Local-First Runtime Boundary\n\n`lsh-core` is meant to own the deterministic part of the installation.\n\n- short-click logic, relay ownership and indicator behavior live on the controller\n- network-assisted logic extends the device behavior, but should not be the only thing making the panel usable\n- when Wi-Fi, MQTT or the central logic node are unavailable, local behavior should still remain coherent\n\nThis is why the bridge and orchestration layers are treated as additive rather than authoritative over the physical panel.\n\n## Getting Started: Creating Your Project\n\n### 1. Project Setup\n\n1. Create a new, blank PlatformIO project.\n2. In your `platformio.ini`, add `lsh-core` as a dependency:\n\n   ```ini\n    [env:my_device]\n    platform = atmelavr\n    framework = arduino\n    board = controllino_maxi\n    build_unflags = -std=gnu++11 -std=c++11\n    build_flags =\n        -I include\n        -std=gnu++17\n    lib_deps = https://github.com/labodj/lsh-core.git\n   ```\n\n   If you are building the bundled example inside this repository, keep the local\n   `lsh-core=symlink://../..` dependency used by\n   `examples/multi-device-project/platformio.ini`.\n\n3. Add the generator hook and select a device profile:\n\n   ```ini\n   extra_scripts = pre:path/to/lsh-core/tools/platformio_lsh_static_config.py\n   custom_lsh_config = lsh_devices.toml\n\n   [env:my_device]\n   custom_lsh_device = my_device\n   ```\n\n   The `extra_scripts` path must point to an accessible `lsh-core` checkout.\n   The bundled example uses a local symlink dependency; consumer projects can\n   use an adjacent checkout, a submodule or another fixed local path.\n\n4. Create the following directory structure inside your project:\n\n   ```text\n   LSH-User-Project/\n   ├── platformio.ini\n   ├── lsh_devices.toml          # Human-authored device topology\n   ├── include/\n   │   ├── lsh_user_config.hpp    # Generated router header\n   │   └── lsh_configs/\n   │       └── ... generated device headers\n   └── src/\n       └── main.cpp\n   ```\n\n5. Write `lsh_devices.toml`, then build. PlatformIO runs the generator before\n   compilation and injects the correct `LSH_BUILD_*` selector for the selected\n   `custom_lsh_device`.\n\nFor a complete working layout, copy the shape of\n[examples/multi-device-project](./examples/multi-device-project) instead of\nstarting from a blank file.\n\n### Core Configuration Concepts\n\nDevice-specific topology is described in `lsh_devices.toml`. The pre-build\ngenerator validates that file and emits the static C++ profile consumed by\n`Configurator::configure()`.\n\nThe generated profile calls the same low-level registration API that older\nhand-written profiles used, but most users never touch those calls directly:\n\n- `addActuator(Actuator* actuator)`: Registers an actuator with the system.\n- `addClickable(Clickable* clickable)`: Registers a clickable with the system.\n- `addIndicator(Indicator* indicator)`: Registers an indicator with the system.\n- `getIndex(const Actuator\u0026 actuator)`: Resolves the dense runtime actuator index used by generated links.\n\nKeep the TOML as the source of truth and regenerate the headers. The generated\nprofile owns registration order, dense indexes, resource counts and lookup\naccessors.\n\nGenerated capacity rule:\n\n- The generator emits exact `LSH_STATIC_CONFIG_*` resource macros for the selected profile.\n- `src/internal/user_config_bridge.hpp` imports those macros and exposes internal `CONFIG_*` `constexpr` values for allocation code.\n- Fixed-capacity containers are therefore sized from the real topology, not from hand-maintained worst-case numbers.\n- Zero-count resources are still represented with one physical ETL slot where ETL requires a strictly positive array capacity; the logical count remains zero and the extra slot is never used.\n\nGenerated ID lookup:\n\n- Public actuator and clickable IDs may be sparse as long as they stay in `1..255`.\n- The generator emits branch/range accessors for `id -\u003e dense index` and `dense index -\u003e id`; no user-authored lookup tables are needed.\n- The highest accepted ID is generated as `LSH_STATIC_CONFIG_MAX_ACTUATOR_ID` and `LSH_STATIC_CONFIG_MAX_CLICKABLE_ID`.\n\nGenerated actuator-link pools:\n\n- Short, long, super-long and indicator link totals are counted from the TOML.\n- Duplicate local targets inside one action are rejected by the generator.\n- Network-only clicks do not consume local link entries unless they also list local fallback targets.\n- Runtime storage stays static and heap-free; generated compile-time checks reject counts outside the supported AVR-friendly field widths.\n\nGenerated runtime pools:\n\n- Per-click timing overrides are counted from `long.time` and `super_long.time`.\n- Auto-off pool size is counted from actuators with non-zero `auto_off`.\n- Active network-click capacity is counted from configured network actions; one held button with both long and super-long network clicks needs two active transactions.\n- `LSH_COMPACT_ACTUATOR_SWITCH_TIMES` remains a user-facing optimization define. It removes the per-`Actuator` 32-bit switch timestamp and keeps timestamps only for auto-off actuators. It requires `CONFIG_ACTUATOR_DEBOUNCE_TIME_MS=0` so debounce semantics remain exact.\n\nOptional network-click exclusion:\n\n- If a device never uses `network = true`, the generator emits a static profile with the network-click runtime compiled out.\n- `LSH_NETWORK_CLICKS = false` in TOML rejects network-click actions for that profile at generation time.\n- `LSH_NETWORK_CLICKS = true` can force the runtime path on for experiments, but normal profiles should let the generator derive it.\n\nGenerated validation rule:\n\n- The generator registers actuators, clickables and indicators in a deterministic order.\n- It rejects missing references, duplicated targets, empty indicators, disabled actions with active options and unsupported path/identifier expressions before compilation.\n- `Configurator::finalizeSetup()` still validates compact manager invariants before runtime starts.\n\nControllino setup helpers:\n\n- On Controllino Maxi / Maxi Automation / Mega profiles, `Configurator::configure()` can call `disableRtc()` and `disableEth()`.\n- `disableRtc()` forces the onboard RTC chip select inactive when the AVR profile does not use the RTC.\n- `disableEth()` forces the Ethernet controller chip select inactive when Ethernet is not owned by the AVR firmware.\n- TOML fields `disable_rtc = true` and `disable_eth = true` emit these calls for the selected static profile.\n\nCompile-time constants layout:\n\n- User profile macros are imported by `src/internal/user_config_bridge.hpp` and exposed as `CONFIG_*` values used by low-level allocation code.\n- The same resource-limit values are also mirrored under `constants::config` for documentation-oriented code and future references.\n- Timing constants live in `src/util/constants/timing.hpp`.\n- Serial/bridge constants live in `src/communication/constants/config.hpp`.\n\nOptional receive-path fairness guard:\n\n- `CONFIG_COM_SERIAL_MAX_RX_PAYLOADS_PER_LOOP` bounds how many complete bridge payloads the controller dispatches in a single `loop()` iteration.\n- `CONFIG_COM_SERIAL_MAX_RX_BYTES_PER_LOOP` bounds how many raw UART bytes the controller may drain in the same iteration, including malformed or incomplete traffic.\n- The defaults let one normal bridge burst make progress without allowing serial noise to monopolize the hot loop.\n- Increase them only after measuring the real hardware tradeoff between bridge throughput and local button latency.\n\n### 2. How to Add a New Device (e.g., \"LivingRoom\")\n\n**Step 1: Let the Generator Own the Headers**\n\nDo not write `include/lsh_user_config.hpp`, `include/lsh_configs/*_config.hpp`\nor resource-count macros by hand for new devices. The TOML generator creates\nthose files and derives exact counts from the real topology, including sparse\nIDs, link totals, network-click pool size, auto-off timers and timing overrides.\n\n**Step 2: Describe the Device in TOML**\n\nCreate `lsh_devices.toml` in your consumer project. Users configure names,\npublic IDs, pins and click behavior; the generator emits the C++ objects,\nresource counts and lookup accessors.\n\n```toml\n[generator]\noutput_dir = \"include\"\nconfig_dir = \"lsh_configs\"\nuser_config_header = \"lsh_user_config.hpp\"\n\n[common]\nhardware_include = \"Controllino.h\"\ndebug_serial = \"Serial\"\ncom_serial = \"Serial2\"\n\n[devices.living_room]\nname = \"LivingRoom\"\n\n[[devices.living_room.actuators]]\nname = \"mainLight\"\nid = 1\npin = \"CONTROLLINO_R0\"\n\n[[devices.living_room.clickables]]\nname = \"wallSwitch\"\nid = 1\npin = \"CONTROLLINO_A0\"\nshort = [\"mainLight\"]\n```\n\n**Step 3: Add the Generator to the Build System**\n\nCreate the build environments in `platformio.ini`. The pre-build hook validates\nthe TOML, writes `include/lsh_user_config.hpp`, generates the selected static\nprofile and adds the correct `LSH_BUILD_*` macro.\n\n```ini\n[common_base]\nextra_scripts = pre:path/to/lsh-core/tools/platformio_lsh_static_config.py\ncustom_lsh_config = lsh_devices.toml\nbuild_src_filter = +\u003c*\u003e -\u003cconfigs/\u003e\n\n[env:LivingRoom_release]\nextends = common_release\ncustom_lsh_device = living_room\nbuild_src_filter = ${common_base.build_src_filter}\nbuild_flags =\n    ${common_release.build_flags}\n    ${common_base.default_feature_flags}\n```\n\n## Configuring Device Behavior\n\nThe current public configuration surface is TOML. New profiles should follow\n[docs/static-toml-config.md](docs/static-toml-config.md); the build generates\nthe static C++ profile from that file and keeps dense indexes, resource counts\nand lookup accessors out of user-authored code.\n\nThe bundled [examples/all-options-toml](examples/all-options-toml) catalog shows\nevery accepted TOML option and is validated by CI. Use it as a syntax reference;\nuse [examples/multi-device-project](examples/multi-device-project) as the\nbuildable starting point.\n\nThe sections below use the public TOML format. Generated C++ remains an\nimplementation detail.\n\n### Actuators (Relays)\n\nDeclare an actuator in TOML. IDs must be unique in the device and stay in the\nwire range `1..255`.\n\n```toml\n[[devices.living_room.actuators]]\nname = \"main_light\"\nid = 1\npin = \"CONTROLLINO_R0\"\ndefault_state = false\nprotected = false\nauto_off = \"10m\"\n```\n\nThe `pin` value must be a compile-time Arduino expression such as a board macro\n(`CONTROLLINO_R0`, `CONTROLLINO_A0`, ...) or a numeric literal. On supported AVR\nboards, the generator lets `lsh-core` resolve the final port/mask binding at\ncompile time while keeping the hot write path on direct register access.\n\n`auto_off` accepts durations such as `\"900ms\"`, `\"30s\"`, `\"10m\"` or `\"1h\"`.\n`protected = true` excludes that relay from global all-off super-long actions.\n\n### Clickables (Buttons)\n\nDeclare inputs in TOML and reference actuators by name. The generator resolves\nthose names into dense indexes and exact link pools before compilation.\n\n```toml\n[[devices.living_room.clickables]]\nname = \"wall_switch\"\nid = 1\npin = \"CONTROLLINO_A0\"\nshort = [\"main_light\"]\nlong = { targets = [\"main_light\"], type = \"on_only\", time = \"900ms\" }\nsuper_long = { type = \"selective\", targets = [\"main_light\"] }\n```\n\nShort clicks toggle their local targets. Long clicks support `normal`,\n`on_only`/`on-only` and `off_only`/`off-only`. Super-long clicks support\n`normal` global all-off behavior or `selective` target lists. Both long and\nsuper-long actions can set `network = true` and choose a fallback policy.\n\n### Indicators (LEDs)\n\nDeclare an indicator and the actuators it watches:\n\n```toml\n[[devices.living_room.indicators]]\nname = \"main_light_led\"\npin = \"CONTROLLINO_D0\"\nactuators = [\"main_light\"]\nmode = \"any\"\n```\n\n`mode` can be `any`, `all` or `majority`.\n\n### Network Clicks and Fallback Logic\n\nA key feature of LSH is its ability to operate reliably both online and offline. Long clicks and super-long clicks can be configured to send a request over the network to `lsh-bridge` and `lsh-logic` for complex, multi-device automations. However, you must define what should happen if the network is unavailable. This is called **fallback logic**.\n\nTo enable a network click, set `network = true` on the TOML `long` or\n`super_long` action. The `fallback` field specifies what happens if the network\npath is unavailable.\n\nIf the same button has both long and super-long network clicks enabled, `lsh-core` preserves the natural sequence for a held press: the long network click is requested first, then the super-long network click is requested while the button remains pressed. The generator accounts for that single button as two active network-click slots.\n\n#### Fallback Types\n\nYou can choose between two different fallback types:\n\n1. **`local` / `local_fallback` (Default)**\n   If a network problem occurs, the click is treated as a standard, local-only action. The actuators listed in the same action's `targets` field will be triggered on the device itself. This ensures the button always does _something_.\n\n   ```toml\n   long = { network = true, fallback = \"local\", targets = [\"main_light\"], type = \"on_only\" }\n   ```\n\n2. **`do_nothing` / `do-nothing`**\n   If a network problem occurs, the click is simply ignored. This is useful for actions that only make sense in a network context (e.g., \"All Lights Off\" across the entire house).\n\n   ```toml\n   super_long = { network = true, fallback = \"do_nothing\" }\n   ```\n\n#### The Network Communication Flow\n\nUnderstanding the handshake between devices helps clarify when a fallback is triggered.\n\n1. **Initial Request:** The user long-presses a network-enabled button on a Controllino running `lsh-core`.\n2. `lsh-core` sends the click event (e.g., \"Button ID 5, Long Click, Request\") to the connected `lsh-bridge` (ESP32) and starts a short timeout timer.\n3. **Gateway to MQTT:** `lsh-bridge` publishes the request to the controller-backed MQTT runtime topic (for example `LSH/\u003cdevice\u003e/events`).\n4. **Central Logic:** `lsh-logic` (Node-RED) receives the message, validates it against its configuration, and checks the status of any other devices involved.\n5. **Acknowledgement (ACK):** If the request is valid, `lsh-logic` immediately sends `NETWORK_CLICK_ACK` back on the device command topic (for example `LSH/\u003cdevice\u003e/IN`).\n6. **Confirmation:** `lsh-bridge` receives the ACK and forwards it to `lsh-core` via serial.\n7. **Execution:** Upon receiving the ACK, `lsh-core` stops its timeout, confirms the action (e.g., with a quick LED blink), and sends `NETWORK_CLICK_CONFIRM` back through `lsh-bridge`.\n8. **Final Action:** `lsh-logic` receives the final confirmation and executes the network-wide automation (e.g., turning on lights on three different devices).\n\nThe same bootstrapping contract is used outside of clicks:\n\n- `lsh-core` sends `BOOT` during startup after configuration has been finalized.\n- When the bridge receives controller `BOOT`, it stops trusting controller-derived runtime state and requests fresh `DEVICE_DETAILS`.\n- After validated details are accepted, the bridge requests fresh `ACTUATORS_STATE` before it treats the controller path as synchronized again.\n- If the bridge has no validated cached topology yet, or if the topology changed, it persists the new details and performs one controlled reboot so MQTT topics and Homie nodes are rebuilt from a coherent snapshot.\n- MQTT reconnects do not redefine the serial protocol. The bridge re-subscribes and re-synchronizes its MQTT-side runtime around the cached or freshly confirmed controller model.\n- A bridge-local service-topic `BOOT` may be used by orchestration peers to request a replay when snapshots are missing. That is a profile behavior of the public stack, not a mandatory end-to-end forwarding rule for `BOOT`.\n\nFor the public reference profile behind this flow, see:\n\n- [LSH reference stack](https://github.com/labodj/labo-smart-home/blob/main/REFERENCE_STACK.md)\n- [vendor/lsh-protocol/docs/profiles-and-roles.md](vendor/lsh-protocol/docs/profiles-and-roles.md)\n\nFor the canonical command IDs, compact key map and golden JSON examples generated from the shared spec, see [vendor/lsh-protocol/shared/lsh_protocol.md](vendor/lsh-protocol/shared/lsh_protocol.md).\n\nThe protocol maintenance workflow itself is documented once in the vendored subtree README at `vendor/lsh-protocol/README.md`. This README only keeps the `lsh-core`-specific invariants and runtime behavior.\n\nTo verify that the generated protocol files in this repository are aligned with the vendored source of truth:\n\n```bash\npython3 tools/update_lsh_protocol.py --check\n```\n\n#### When is Fallback Logic Triggered?\n\nThe configured fallback logic is applied instantly if any step in this chain fails:\n\n- The `lsh-bridge` (ESP32) is physically disconnected or unreachable.\n- The `lsh-bridge` has no Wi-Fi connection or cannot reach the MQTT broker.\n- The `lsh-logic` controller sends a negative acknowledgement (NACK) because the request is invalid or other devices are offline.\n- **Most importantly: If the initial ACK from `lsh-logic` does not arrive back at the `lsh-core` device within the timeout period (typically ~1 second).**\n\nThis keeps user feedback predictable whether the network path is healthy,\nslow or unavailable.\n\n## Feature Flags\n\nLSH-Core can be fine-tuned at compile-time using feature flags. These flags allow you to enable or disable specific functionalities to optimize for performance, memory usage, or specific hardware capabilities.\n\nFor TOML-backed profiles, put per-device flags in `[devices.\u003cname\u003e.defines]`.\nPlatformIO-only global defaults can still live in `platformio.ini` when they are\nintentionally shared by every environment.\n\n### Communication Protocol\n\n#### `CONFIG_MSG_PACK`\n\n- **Description:** Switches the serial communication protocol between `lsh-core` and `lsh-bridge` from human-readable JSON to the more efficient, binary MessagePack format.\n- **When to use:** Recommended for most production environments. MessagePack significantly reduces the size of the payloads, leading to faster and more reliable serial communication. This also reduces the RAM required for serialization buffers on both the Controllino and the ESP32.\n- **Serial transport:** When this flag is enabled, the controller uses a framed MessagePack serial transport: `END + escaped(payload) + END`. JSON mode continues to use newline-delimited text frames.\n- **Compile-time static payloads:** Static control payloads such as `BOOT` and `PING` are generated in both raw and serial-ready forms. `lsh-core` writes the serial-ready bytes directly to the UART, so static MessagePack control frames do not pay framing work at runtime.\n- **Impact:** Smaller firmware size and lower RAM usage. Requires the `lsh-bridge` firmware to also be configured for MessagePack.\n\n### I/O Performance\n\nThese flags replace standard `digitalRead()` and `digitalWrite()` calls with direct port manipulation for maximum speed. This is especially useful on AVR-based controllers like the ATmega2560, where it can dramatically reduce I/O latency.\n\nWhen the device is declared through the public `LSH_*` macros and the selected\npin is a compile-time constant, the AVR fast-I/O path also resolves the final\nregister binding at compile time on supported Mega/Controllino-class boards.\nThe hot path still uses the same cached direct register access as before; only\nthe setup-time lookup changes. Unsupported boards or pins fall back to the\ntraditional Arduino table lookup path automatically.\n\n#### `CONFIG_USE_FAST_CLICKABLES`\n\n- **Description:** Optimizes the reading of input pins for buttons (`Clickable` objects).\n- **When to use:** Always recommended unless you are using a non-standard board or core where direct port manipulation might not be supported. The performance gain ensures that even very rapid button presses are never missed.\n- **Compile-time path:** With a generated static profile and a compile-time pin constant, supported AVR boards avoid the setup-time Arduino lookup tables entirely and still keep the polling path as one direct register read.\n- **Impact:** Faster input polling.\n\n#### `CONFIG_USE_FAST_ACTUATORS`\n\n- **Description:** Optimizes the writing to output pins for relays (`Actuator` objects).\n- **When to use:** Always recommended for performance-critical applications.\n- **Compile-time path:** With a generated static profile and a compile-time pin constant, supported AVR boards resolve the port binding at compile time while leaving the steady-state write path as a direct register update.\n- **Impact:** Faster relay switching.\n\n#### `CONFIG_USE_FAST_INDICATORS`\n\n- **Description:** Optimizes the writing to output pins for status LEDs (`Indicator` objects).\n- **When to use:** Always recommended.\n- **Compile-time path:** With a generated static profile and a compile-time pin constant, supported AVR boards resolve the indicator binding at compile time and keep runtime LED updates on the direct port path.\n- **Impact:** Faster LED state changes.\n\n### Timing Configuration\n\nThese flags allow you to override the default timing behavior of the framework. You typically don't need to define these unless you have specific hardware or user experience requirements.\n\n#### `CONFIG_ACTUATOR_DEBOUNCE_TIME_MS`\n\n- **Default:** `100U` (100 milliseconds)\n- **Description:** Sets the minimum delay between two consecutive switches of the same actuator. This protects relays and other outputs from overly rapid toggling caused by noisy or repeated commands.\n- **Example:** `-D CONFIG_ACTUATOR_DEBOUNCE_TIME_MS=150U`\n\n#### `CONFIG_CLICKABLE_DEBOUNCE_TIME_MS`\n\n- **Default:** `20U` (20 milliseconds)\n- **Description:** Sets the debounce time for all buttons. This is the minimum time a button state must be stable before being recognized as a valid press or release, preventing electrical noise from causing multiple triggers.\n- **Example:** `-D CONFIG_CLICKABLE_DEBOUNCE_TIME_MS=30U`\n\n#### `CONFIG_CLICKABLE_SCAN_INTERVAL_MS`\n\n- **Default:** `1U` (1 millisecond)\n- **Description:** Sets the minimum elapsed time between two input scan passes. With the default value, the historical policy remains approximately `~1000 Hz` when the main loop is otherwise free to run.\n- **Behavior note:** This is a scan policy knob, not a hard real-time guarantee. If the controller is busy, `lsh-core` passes the whole accumulated elapsed time to the clickable state machine so debounce and long-click timing stay coherent.\n- **Bridge note:** Bridge heartbeat pacing and handshake retries use their own elapsed-time gate and are not paced by this input scan interval.\n- **When to tune:** Increase it only after measuring the real hardware tradeoff between button latency, serial fairness and CPU headroom.\n- **Example:** `-D CONFIG_CLICKABLE_SCAN_INTERVAL_MS=2U`\n\n#### `CONFIG_CLICKABLE_LONG_CLICK_TIME_MS`\n\n- **Default:** `400U` (400 milliseconds)\n- **Description:** Sets the time a button must be held down to be registered as a \"long click\".\n- **Example:** `-D CONFIG_CLICKABLE_LONG_CLICK_TIME_MS=500U`\n\n#### `CONFIG_CLICKABLE_SUPER_LONG_CLICK_TIME_MS`\n\n- **Default:** `1000U` (1000 milliseconds)\n- **Description:** Sets the time a button must be held down to be registered as a \"super-long click\".\n- **Example:** `-D CONFIG_CLICKABLE_SUPER_LONG_CLICK_TIME_MS=1500U`\n\n#### `CONFIG_LCNB_TIMEOUT_MS`\n\n- **Default:** `1000U` (1000 milliseconds)\n- **Description:** Sets the timeout for network clicks. If `lsh-core` sends a network click request and does not receive an ACK within this period, it will trigger the configured fallback logic.\n- **Example:** `-D CONFIG_LCNB_TIMEOUT_MS=1200U`\n\n### Network and Communication Buffers\n\n#### `CONFIG_PING_INTERVAL_MS`\n\n- **Default:** `10000U` (10 seconds)\n- **Description:** Sets the interval at which `lsh-core` sends a \"ping\" message to `lsh-bridge` to keep the connection alive and verify that the bridge is responsive.\n- **Example:** `-D CONFIG_PING_INTERVAL_MS=15000U`\n\n#### `CONFIG_CONNECTION_TIMEOUT_MS`\n\n- **Default:** `PING_INTERVAL_MS + 200U`\n- **Description:** The duration after the last received message from `lsh-bridge` before `lsh-core` considers the connection to be lost.\n- **Example:** `-D CONFIG_CONNECTION_TIMEOUT_MS=15500U`\n\n#### `CONFIG_BRIDGE_BOOT_RETRY_INTERVAL_MS`\n\n- **Default:** `250U` (250 milliseconds)\n- **Description:** Sets how often `lsh-core` retries the bridge bootstrap handshake after sending `BOOT`, while the bridge has not yet completed its startup sequence.\n- **Example:** `-D CONFIG_BRIDGE_BOOT_RETRY_INTERVAL_MS=500U`\n\n#### `CONFIG_BRIDGE_AWAIT_STATE_TIMEOUT_MS`\n\n- **Default:** `1500U` (1500 milliseconds)\n- **Description:** Sets how long `lsh-core` waits for the bridge to request the authoritative state after the device details have already been sent. If this timeout expires, the bootstrap handshake restarts from `BOOT`.\n- **Example:** `-D CONFIG_BRIDGE_AWAIT_STATE_TIMEOUT_MS=2000U`\n\n#### `CONFIG_DEBUG_SERIAL_BAUD`\n\n- **Default:** `115200U`\n- **Description:** Sets the baud rate used by the debug serial port when `LSH_DEBUG` is enabled.\n- **Example:** `-D CONFIG_DEBUG_SERIAL_BAUD=500000U`\n\n#### `CONFIG_COM_SERIAL_BAUD`\n\n- **Default:** `250000U`\n- **Description:** Sets the baud rate of the controller-to-bridge serial link used to talk to `lsh-bridge`.\n- **Example:** `-D CONFIG_COM_SERIAL_BAUD=500000U`\n\n#### `CONFIG_COM_SERIAL_TIMEOUT_MS`\n\n- **Default:** `5U` (5 milliseconds)\n- **Description:** Defines the compatibility fallback used as the default value for `CONFIG_COM_SERIAL_MSGPACK_FRAME_IDLE_TIMEOUT_MS`.\n- **Behavior note:** The current receive path does not use timeout-based framing. Changing this flag only changes the default housekeeping timeout for incomplete MsgPack frames unless you also override `CONFIG_COM_SERIAL_MSGPACK_FRAME_IDLE_TIMEOUT_MS`.\n- **Example:** `-D CONFIG_COM_SERIAL_TIMEOUT_MS=10U`\n\n#### `CONFIG_COM_SERIAL_MSGPACK_FRAME_IDLE_TIMEOUT_MS`\n\n- **Default:** `CONFIG_COM_SERIAL_TIMEOUT_MS`\n- **Description:** Sets the housekeeping timeout used to drop one incomplete framed MsgPack payload after the UART goes silent for too long. This timeout only cleans up truncated frames; it does not define frame boundaries.\n- **Example:** `-D CONFIG_COM_SERIAL_MSGPACK_FRAME_IDLE_TIMEOUT_MS=8U`\n\n#### `CONFIG_COM_SERIAL_MAX_RX_BYTES_PER_LOOP`\n\n- **Default:** `RAW_INPUT_BUFFER_SIZE` in JSON mode, `MSGPACK_SERIAL_MAX_FRAME_SIZE` in MsgPack mode\n- **Description:** Bounds the total number of raw UART bytes that `lsh-core` may drain in one `loop()` iteration before returning to local input scanning and logic.\n- **When to tune:** Raise it only if the bridge regularly delivers bursts that should be drained faster and hardware tests confirm that button latency stays acceptable.\n- **Example:** `-D CONFIG_COM_SERIAL_MAX_RX_BYTES_PER_LOOP=48U`\n\n#### `CONFIG_COM_SERIAL_FLUSH_AFTER_SEND`\n\n- **Default:** `1` in `LSH_DEBUG` builds, `0` in release builds\n- **Description:** Controls whether `lsh-core` calls `flush()` on the serial link after each payload sent to `lsh-bridge`.\n- **Current status:** Debug builds keep the conservative validated behavior, while release builds avoid the blocking flush unless explicitly requested.\n- **Why this exists:** This flag lets a profile force the conservative behavior or explicitly benchmark the non-flushing serial path.\n- **Recommendation:** Keep the release default unless hardware tests show that the bridge link needs flushes on that specific installation.\n- **Examples:**\n  - Keep the validated behavior: `-D CONFIG_COM_SERIAL_FLUSH_AFTER_SEND=1`\n  - Force the release-style non-blocking send path: `-D CONFIG_COM_SERIAL_FLUSH_AFTER_SEND=0`\n\n#### `CONFIG_DELAY_AFTER_RECEIVE_MS`\n\n- **Default:** `50U` (50 milliseconds)\n- **Description:** Sets the short quiet window used after receiving a bridge-side state-changing payload before `lsh-core` mirrors the new authoritative state back out. This reduces duplicate publish bursts when multiple single-actuator updates arrive close together.\n- **Example:** `-D CONFIG_DELAY_AFTER_RECEIVE_MS=75U`\n\n#### `CONFIG_NETWORK_CLICK_CHECK_INTERVAL_MS`\n\n- **Default:** `50U` (50 milliseconds)\n- **Description:** Sets how often pending network-click requests are revisited to detect ACK timeouts and trigger fallback logic when needed.\n- **Example:** `-D CONFIG_NETWORK_CLICK_CHECK_INTERVAL_MS=25U`\n\n#### `CONFIG_ACTUATORS_AUTO_OFF_CHECK_INTERVAL_MS`\n\n- **Default:** `1000U` (1 second)\n- **Description:** Sets how often `lsh-core` scans actuators with auto-off timers to decide whether they must be turned off.\n- **Example:** `-D CONFIG_ACTUATORS_AUTO_OFF_CHECK_INTERVAL_MS=250U`\n\n### Benchmarking (for developers)\n\nThese flags are intended for development and performance testing of the LSH-Core library itself.\n\n#### `CONFIG_LSH_BENCH`\n\n- **Description:** Enables a simple benchmarking routine in the main `loop()`. It measures the time taken to complete a fixed number of empty loop iterations.\n- **When to use:** Only for library development or performance tuning to measure the overhead of the core loop. This should be disabled in production.\n\n#### `CONFIG_BENCH_ITERATIONS`\n\n- **Default:** `1000000U` (1 million)\n- **Description:** Sets the number of iterations for the benchmark loop enabled by `CONFIG_LSH_BENCH`.\n- **Example:** `-D CONFIG_BENCH_ITERATIONS=500000U`\n\n### ETL profile override\n\n`lsh-core` ships with a default [etl_profile.h](./include/etl_profile.h) so the\ncommon Arduino/PlatformIO case works out of the box.\n\nThat default profile intentionally sets only the library policy knobs that are\npart of the current project assumptions, while ETL still auto-detects the\nactive compiler and language support through `etl/profiles/auto.h`.\n\nIf you need a different ETL setup for another target or toolchain, the\nrecommended override path is:\n\n1. Create your own small header in the consumer project, for example `include/lsh_etl_profile_override.h`\n2. Pass the `LSH_ETL_PROFILE_OVERRIDE_HEADER` build flag and point it at your override header.\n3. In that header, `#undef` and redefine only what you need\n\nExample:\n\n```cpp\n// include/lsh_etl_profile_override.h\n#pragma once\n\n#undef ETL_CHECK_PUSH_POP\n#define ETL_THROW_EXCEPTIONS\n```\n\nIf your build system prefers full ownership, you may also provide your own\nproject-level `etl_profile.h` earlier in the include path and bypass the one\nshipped by `lsh-core`.\n\nThe bundled example project already demonstrates this hook through\n[examples/multi-device-project/include/lsh_etl_profile_override.h](./examples/multi-device-project/include/lsh_etl_profile_override.h)\nand the matching `LSH_ETL_PROFILE_OVERRIDE_HEADER` flag in\n[examples/multi-device-project/platformio.ini](./examples/multi-device-project/platformio.ini).\n\n## Building and Uploading\n\nUse the standard PlatformIO commands from within your user project folder, specifying the target environment.\n\n```bash\n# Build the 'J1_release' environment\nplatformio run -e J1_release\n\n# Build and upload the 'J1_debug' environment\nplatformio run -e J1_debug --target upload\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flabodj%2Flsh-core","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flabodj%2Flsh-core","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flabodj%2Flsh-core/lists"}