Welcome to lsh-core, the core firmware engine for the Labo Smart Home (LSH) ecosystem. This framework was originally designed for personal use with industrial-grade Controllino PLCs to ensure maximum reliability.
This document serves as the official guide for using the lsh-core library in your own PlatformIO projects.
LSH is a complete, distributed home automation system composed of three distinct, open-source projects:
-
lsh-core(This Project): The heart of the physical layer. This C++23 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. -
lsh-bridge: A lightweight firmware designed for an ESP32. It acts as a transparent bridge, physically connecting tolsh-corevia serial and relaying messages to and from your network via MQTT. This isolates the core logic from Wi-Fi and network concerns. -
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-coredevices and orchestrates complex, network-wide automation logic.
The three components work together to create a robust and responsive system.
+-----------------+ +-----------------+ +-----------------+
| lsh-core | --(1) Click Event--> | lsh-bridge | --(2) MQTT Publish-> | MQTT Broker |
|(Physical Layer) | [Serial] | (Gateway/Bridge)| | (Message Hub) |
| | <----(7) Command---- | | <----(6) Command---- | |
+-----------------+ [Serial] +-----------------+ +--------+--------+
|
(3) Event |
v
+--------+--------+
| lsh-logic (NR) |
| (Logic Layer) |
| --(5) Command --+
+-----------------+
While 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.
It is the perfect resource for developers who want to understand the inner workings of the library or explore advanced functionalities beyond the examples provided here.
Browse the full API Documentation here
lsh-core was designed around the Controllino Maxi, but can be adapted. The following setup is considered standard.
The controller is typically powered by a 12V or 24V DC power supply. This voltage is referred to as VDD throughout the electrical schematics.
Each physical input pin is designed to be connected to one or more push-buttons. The standard wiring is:
INPUT PIN ← Push-Button → VDD
When a button is pressed, it closes the circuit, connecting the input pin to VDD and signaling a "high" state to the controller.
- High-Voltage Outputs (Relays): The Controllino's relay outputs are used to switch high-voltage loads like 230V AC for lighting.
- Low-Voltage Outputs (Digital Out): These outputs provide a
VDDsignal and are typically used to power status LEDs on button panels.
For network functionality, lsh-core communicates with an lsh-bridge device over a hardware serial port.
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.
- Controllino
TXpin → Logic Level Shifter (HV side) → (LV side) → ESP32RXpin - Controllino
RXpin → Logic Level Shifter (HV side) → (LV side) → ESP32TXpin
Typically, Serial2 on the Controllino Maxi is used for this communication.
-
Create a new, blank PlatformIO project.
-
In your
platformio.ini, add the LSH-Core library as a dependency:[env:my_device] lib_deps = https://github.com/labodj/lsh-core.git -
Create the following directory structure inside your project:
LSH-User-Project/ ├── platformio.ini ├── include/ │ ├── lsh_user_config.hpp # The "router" for your configurations │ └── lsh_configs/ │ └── ... (your device header files go here) └── src/ ├── main.cpp └── configs/ └── ... (your device logic files go here)
All device-specific logic is defined in the Configurator::configure() function within your src/configs/your_device.cpp file. The LSH library provides a set of helper functions within the Configurator class to make this process clean and readable.
addActuator(Actuator* actuator): Registers an actuator with the system.addClickable(Clickable* clickable): Registers a clickable with the system.addIndicator(Indicator* indicator): Registers an indicator with the system.getIndex(const Actuator& actuator): A crucial helper that returns the unique internal index of a registered actuator. You must use this function when connecting an actuator to a button or indicator, as shown below:
// GOOD: Connects the button to the actuator using its safe index.
btn0.addActuatorShort(getIndex(rel0));
// BAD: This will not compile. You cannot pass the object directly.
// btn0.addActuatorShort(&rel0);Step 1: Create the Device Header (.hpp)
Create include/lsh_configs/living_room_config.hpp. This file defines the hardware and build constants for this specific device.
#ifndef LIVING_ROOM_CONFIG_HPP
#define LIVING_ROOM_CONFIG_HPP
// 1. Define the hardware library to include for this device.
#define LSH_HARDWARE_INCLUDE <Controllino.h>
// 2. Define the build constants required by the LSH library.
#define LSH_DEVICE_NAME "LivingRoom"
#define LSH_MAX_CLICKABLES 8
#define LSH_MAX_ACTUATORS 6
#define LSH_MAX_INDICATORS 2
#define LSH_COM_SERIAL &Serial1
#define LSH_DEBUG_SERIAL &Serial
#endifStep 2: Create the Device Logic File (.cpp)
Create src/configs/living_room_config.cpp. This is where you define your objects (relays, buttons) and their behavior.
#include <lsh.hpp> // Gives access to LSH_ACTUATOR, etc.
// Define all your device objects in an anonymous namespace to prevent name clashes.
namespace {
LSH_ACTUATOR(mainLight, CONTROLLINO_R0, 1);
LSH_BUTTON(wallSwitch, CONTROLLINO_A0, 1);
}
// Implement the configuration logic for this device.
void Configurator::configure() {
addActuator(&mainLight);
addClickable(&wallSwitch);
wallSwitch.addActuatorShort(getIndex(mainLight));
}Step 3: Add the Device to the Build System
First, tell the "router" header about your new device.
In include/lsh_user_config.hpp:
#if defined(LSH_BUILD_LIVING_ROOM)
#include "lsh_configs/living_room_config.hpp"
#endifNext, create the build environments in platformio.ini.
[device_LivingRoom]
device_feature_flags =
-D CONFIG_MSG_PACK
[env:LivingRoom_release]
extends = common_release
src_filter = ${common_base.src_filter} +<configs/living_room_config.cpp>
build_flags =
${common_release.build_flags}
${common_base.default_feature_flags}
${device_LivingRoom.device_feature_flags}
-D LSH_BUILD_LIVING_ROOMAll your logic is written inside the Configurator::configure() function in your device's .cpp file.
Declare an actuator using the LSH_ACTUATOR macro. IDs must be unique and greater than 0.
LSH_ACTUATOR(variable_name, PIN, UNIQUE_NUMERIC_ID);Set a relay to automatically turn off after a predefined time (in milliseconds).
rel0.setAutoOffTimer(600000); // 10-minute timerProtect a relay from being turned off by a global "all off" command (like a super-long click).
rel0.setProtected(true);Declare buttons using the LSH_BUTTON macro. IDs must be unique and greater than 0.
LSH_BUTTON(variable_name, PIN, UNIQUE_NUMERIC_ID);A brief press of the button. It toggles the state of all associated relays. This is the default behavior.
// This makes the button toggle rel0 on short click.
btn0.addActuatorShort(getIndex(rel0));You can disable this behavior:
btn0.setClickableShort(false);A press held longer than a short click.
// Chain methods to add multiple relays to the long click action.
btn0.addActuatorLong(getIndex(rel1))
.addActuatorLong(getIndex(rel2));
// Configure the long click behavior
btn0.setClickableLong(true, LongClickType::NORMAL); // Default: turns ON if most are OFF, else turns OFF.
btn0.setClickableLong(true, LongClickType::ON_ONLY); // Always turns relays ON.
btn0.setClickableLong(true, LongClickType::OFF_ONLY); // Always turns relays OFF.A press held even longer.
// By default, turns off ALL unprotected relays on the device.
btn0.setClickableSuperLong(true);
// Or, make it turn off only a specific list of relays.
btn0.addActuatorSuperLong(getIndex(rel0))
.addActuatorSuperLong(getIndex(rel1));
btn0.setClickableSuperLong(true, SuperLongClickType::SELECTIVE);Long and super-long clicks can be forwarded over the network. You must specify a fallback behavior in case of network failure.
// If the network fails, execute the long click action locally.
btn0.setClickableLong(true, LongClickType::ON_ONLY, true, NoNetworkClickType::LOCAL_FALLBACK);
// If the network fails, do nothing.
btn1.setClickableSuperLong(true, SuperLongClickType::NORMAL, true, NoNetworkClickType::DO_NOTHING);Declare an indicator light using the LSH_INDICATOR macro.
LSH_INDICATOR(variable_name, PIN);Link one or more actuators to the indicator. Its behavior depends on the configured mode.
// Link the indicator to two relays
statusLED.addActuator(getIndex(rel0));
statusLED.addActuator(getIndex(rel1));
// Configure the operating mode
statusLED.setMode(constants::IndicatorMode::ANY); // Default: LED is ON if ANY linked relay is ON.
statusLED.setMode(constants::IndicatorMode::ALL); // LED is ON only if ALL linked relays are ON.
statusLED.setMode(constants::IndicatorMode::MAJORITY); // LED is ON if more than half of the linked relays are ON.A 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.
To enable a network click, set the third parameter of setClickableLong() or setClickableSuperLong() to true. The fourth parameter specifies the fallback behavior.
You can choose between two different fallback types:
-
NoNetworkClickType::LOCAL_FALLBACK(Default) If a network problem occurs, the click is treated as a standard, local-only action. The actuators defined withaddActuatorLong()oraddActuatorSuperLong()for that button will be triggered on the device itself. This ensures the button always does something.// This long click is a network action. // If the network is down, it will fall back to its local long-click logic. btn0.setClickableLong(true, LongClickType::ON_ONLY, true, NoNetworkClickType::LOCAL_FALLBACK);
-
NoNetworkClickType::DO_NOTHINGIf 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).// This super-long click is a network-only action. // If the network is down, pressing the button will have no effect. btn1.setClickableSuperLong(true, SuperLongClickType::NORMAL, true, NoNetworkClickType::DO_NOTHING);
Understanding the handshake between devices helps clarify when a fallback is triggered.
- Initial Request: The user long-presses a network-enabled button on a Controllino running
lsh-core. lsh-coresends the click event (e.g., "Button ID 5, Long Click, Request") to the connectedlsh-bridge(ESP32) and starts a short timeout timer.- Gateway to MQTT:
lsh-bridgepublishes the event to the MQTT broker (e.g., on topicMyDevice/OUT). - Central Logic:
lsh-logic(Node-RED) receives the message, validates it against its configuration, and checks the status of any other devices involved. - Acknowledgement (ACK): If the request is valid,
lsh-logicimmediately sends an ACK back via MQTT (e.g., on topicMyDevice/IN). - Confirmation:
lsh-bridgereceives the ACK and forwards it tolsh-corevia serial. - Execution: Upon receiving the ACK,
lsh-corestops its timeout, confirms the action (e.g., with a quick LED blink), and sends a final confirmation message tolsh-bridge("Button ID 5, Long Click, Confirmed"). - Final Action:
lsh-logicreceives the final confirmation and executes the network-wide automation (e.g., turning on lights on three different devices).
The configured fallback logic is applied instantly if any step in this chain fails:
- The
lsh-bridge(ESP32) is physically disconnected or unreachable. - The
lsh-bridgehas no Wi-Fi connection or cannot reach the MQTT broker. - The
lsh-logiccontroller sends a negative acknowledgement (NACK) because the request is invalid or other devices are offline. - Most importantly: If the initial ACK from
lsh-logicdoes not arrive back at thelsh-coredevice within the timeout period (typically ~1 second).
This robust system ensures that the user gets immediate feedback and predictable behavior, whether the network is perfectly responsive or completely offline.
LSH-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.
You can set these flags globally for all devices or on a per-device basis in your platformio.ini file.
- Description: Switches the serial communication protocol between
lsh-coreandlsh-bridgefrom human-readable JSON to the more efficient, binary MessagePack format. - 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.
- Impact: Smaller firmware size and lower RAM usage. Requires the
lsh-bridgefirmware to also be configured for MessagePack.
These 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.
- Description: Optimizes the reading of input pins for buttons (
Clickableobjects). - 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.
- Impact: Faster input polling.
- Description: Optimizes the writing to output pins for relays (
Actuatorobjects). - When to use: Always recommended for performance-critical applications.
- Impact: Faster relay switching.
- Description: Optimizes the writing to output pins for status LEDs (
Indicatorobjects). - When to use: Always recommended.
- Impact: Faster LED state changes.
These 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.
- Default:
20U(20 milliseconds) - 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.
- Example:
-D CONFIG_CLICKABLE_DEBOUNCE_TIME_MS=30U
- Default:
400U(400 milliseconds) - Description: Sets the time a button must be held down to be registered as a "long click".
- Example:
-D CONFIG_CLICKABLE_LONG_CLICK_TIME_MS=500U
- Default:
1000U(1000 milliseconds) - Description: Sets the time a button must be held down to be registered as a "super-long click".
- Example:
-D CONFIG_CLICKABLE_SUPER_LONG_CLICK_TIME_MS=1500U
- Default:
1000U(1000 milliseconds) - Description: Sets the timeout for network clicks. If
lsh-coresends a network click request and does not receive an ACK within this period, it will trigger the configured fallback logic. - Example:
-D CONFIG_LCNB_TIMEOUT_MS=1200U
- Default:
10000U(10 seconds) - Description: Sets the interval at which
lsh-coresends a "ping" message tolsh-bridgeto keep the connection alive and verify that the bridge is responsive. - Example:
-D CONFIG_PING_INTERVAL_MS=15000U
- Default:
PING_INTERVAL_MS + 200U - Description: The duration after the last received message from
lsh-bridgebeforelsh-coreconsiders the connection to be lost. - Example:
-D CONFIG_CONNECTION_TIMEOUT_MS=15500U
These flags are intended for development and performance testing of the LSH-Core library itself.
- Description: Enables a simple benchmarking routine in the main
loop(). It measures the time taken to complete a fixed number of empty loop iterations. - When to use: Only for library development or performance tuning to measure the overhead of the core loop. This should be disabled in production.
- Default:
1000000U(1 million) - Description: Sets the number of iterations for the benchmark loop enabled by
CONFIG_LSH_BENCH. - Example:
-D CONFIG_BENCH_ITERATIONS=500000U
Use the standard PlatformIO commands from within your user project folder, specifying the target environment.
# Build the 'J1_release' environment
platformio run -e J1_release
# Build and upload the 'J1_debug' environment
platformio run -e J1_debug --target upload