A minimal, modern device runtime for SignalK-connected ESP32 hardware, built natively on ESP-IDF 6.
espOS is the plumbing every SignalK ESP32 device needs and nobody wants to rewrite: WiFi, persistent config, a web config UI, SignalK server discovery, token acquisition, delta output, and OTA. It is deliberately not a sensor framework — application code sits on top and calls a small API.
Targets: ESP32, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-P4 — one codebase.
Toolchain: ESP-IDF pinned in .idf-version. HTTP:
esp_http_server. Storage: NVS for config and secrets.
espOS lives at github.com/signalk-espOS/espOS
and publishes its components as signalk-espos/espos_*. It is a community
project, not an official Signal K repository.
espOS is pre-1.0; version.txt and the
releases say where it is
today. The core is done and in use: config store, HTTP
server and REST API, WiFi state machine with captive portal, SignalK
discovery, access token and delta stream in both directions, web UI,
device-health notifications and signed OTA with rollback -- all host-tested
and running on ESP32-P4 hardware against signalk-server 2.31; the BLE,
NMEA 2000 and voice components serve firmware built on top. What comes next
is in docs/roadmap.md; the decisions taken so far are in
docs/decisions.md.
. $IDF_PATH/export.sh # ESP-IDF 6.0.x, the release in .idf-version
idf.py set-target esp32c6 # or esp32 / esp32s3 / esp32c3 / esp32p4
idf.py build flash monitor # on a shared or small host: scripts/build.sh build
# the monitor says what to do next: join the "espOS-xxxx" access point and open
# http://192.168.4.1 to pick a WiFi; then approve the device in signalk-server
# (Security → Access Requests). Web UI afterwards: http://<hostname>.localThe web UI bundle is committed, so Node is not needed to build a device that
serves it. scripts/build.sh wraps idf.py with a machine-wide lock and a
capped job count for hosts that freeze under a full parallel build.
Every component is published to the Espressif Component
Registry under
the signalk-espos namespace, so a firmware can use espOS without cloning
it:
idf.py add-dependency "signalk-espos/espos_sk^0.9.0"espos_sk names the rest of the core as its own dependencies, so that one
line installs what a SignalK device needs. Keep the ^ range: espOS is
pre-1.0, where a minor bump does the work a major will do later, so an
unpinned dependency would take the next one unannounced. The components are
released in lockstep — one version of any of them works with the same
version of every other.
That line is the dependency; a firmware needs three more things, because a
registry consumer has no espos_project_prologue() to set them (the prologue
must run before project(), and a component's project_include.cmake runs
after, so it cannot ship as a component):
-
A root
CMakeLists.txt— plain IDF, plus the web UI partition:cmake_minimum_required(VERSION 3.22) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(my_firmware) espos_project_ui_partition() # after project(), always
-
A partition table with two OTA slots and a
storagepartition, which IDF's single-app default has neither of. Copy one of the bundled tables rather than pointing intomanaged_components/, which is a build artefact:cp managed_components/signalk-espos__espos_core/partitions/4mb.csv partitions.csv
-
sdkconfig.defaults— the task stacks, core dump, OTA rollback, image signing, that partition table and a matchingCONFIG_ESPTOOLPY_FLASHSIZE_*. You do not have to guess:espos_corechecks at configure time and prints every missing line with the reason it matters.
Then generate the app-signing key once — it is deliberately not created for you, because a key invented by a build step is a key nobody kept, and a device accepts an OTA only from the key it was flashed with (docs/ota.md):
espsecure generate-signing-key --version 2 --scheme rsa3072 secure_boot_signing_key.pemcomponents/espos_core/examples/from_registry
is all of the above as a working project, built by CI on every change.
The whole of an application on espOS is one call; everything else is yours (docs/concepts.md has the order and the threading rules):
#include "espos.h"
void app_main(void)
{
ESP_ERROR_CHECK(espos_start(NULL)); /* log → config → httpd → wifi → sk → ota → ble */
/* your application: espos_sk_publish_*, espos_sk_subscribe, espos_config_get_* */
}Docs: signalk-espos.github.io/espOS — Getting started · Concepts · Examples · REST API contract · Config store & descriptors · WiFi · SignalK discovery & token · OTA & signing · Web UI · Device health · BLE gateway · NMEA 2000 gateway · Voice satellite · Hardware · Troubleshooting · Development & host tests · Releasing · Security notes
Everything is an ESP-IDF component; an application depends on the ones it needs and ignores the rest. The core four are what "running espOS" means; the rest are optional.
| Component | What it gives you | Docs |
|---|---|---|
espos_core |
espos_start(): brings up everything below in the right order |
concepts.md |
espos_config |
NVS config store, JSON-Schema descriptors, REST-backed settings | config.md |
espos_httpd |
HTTP server, REST API, SSE, the web UI from a LittleFS partition | rest-api.md · ui.md |
espos_net |
Interface-agnostic network status and default route, mDNS responder, device id; WiFi/Ethernet plug in underneath | net.md |
espos_wifi |
Station + provisioning portal, a pure-C state machine, co-processor watchdog | wifi.md |
espos_log |
Log ring served over REST, so a device is debuggable without a serial cable | — |
espos_health |
Device conditions (warn/alarm) and the sinks that consume them | health.md |
espos_sk |
SignalK: mDNS discovery, access token, delta stream in and out | signalk.md |
espos_ota |
Signed OTA with rollback, from a URL or a version manifest | ota.md |
espos_event |
The ESPOS_EVENT base other components post their state on |
concepts.md |
espos_time |
SNTP, wall-clock timestamps on deltas and log lines | time.md |
espos_eth |
Ethernet as an espos_net transport (P4 EMAC, W5500 over SPI) |
net.md |
espos_flow |
Typed data-flow graph, timer wheel, one loop task | flow.md |
espos_formulas |
Marine maths with no IDF dependency: curves, dew point, densities | transforms.md |
espos_sensors |
ADC, GPIO, pulse counter, PWM, I2C, 1-Wire as flow nodes | sensors.md |
espos_sk_flow |
SignalK output, listener and PUT handler as flow nodes | signalk.md |
espos_devices |
Whole devices composed from the above: tank level, engine RPM, … | devices.md |
espos_power |
Deep-sleep duty cycle: wake, publish, flush, sleep | power.md |
espos_prov |
BLE provisioning: WiFi credentials from a phone over GATT | provisioning.md |
espos_ble |
BLE gateway | ble.md |
espos_n2k |
NMEA 2000 over TWAI + a candump TCP server | n2k.md |
espos_audio |
The AudioDriver contract a board implements (header-only) |
voice.md |
espos_voice |
Wyoming voice satellite with esp-sr wake word | voice.md |
Board-specific code — display HALs, audio codecs, pin maps — stays in the
application. espOS defines the contracts and never assumes a particular
board; espos_audio::AudioDriver is the pattern to copy when something
similar is needed.
components/espos_*/ one directory per component (table above), each with
include/ src/ and, where it has one, examples/
main/ example app
tools/ generators
test/host/ linux-target tests (no hardware needed)
test/fuzz/ libFuzzer harnesses for the network-facing parsers
docs/ contracts and guides
espOS is open source under the Apache License 2.0; every source
file carries an SPDX header naming the license and the tree is
REUSE-compliant. Contributions are accepted under the
same license with a Signed-off-by line (Developer Certificate of Origin).
Third-party components pulled in by the build (ESP-IDF, Espressif component
registry packages, joltwallet/littlefs, npm packages) keep their own
licenses; see THIRD-PARTY-NOTICES.md.