Go to file
Hermes f86d264f19
Some checks failed
OTA build / build-ota (push) Has been cancelled
ci: trigger actor-namespace ota upload
2026-08-30 10:35:32 -05:00
2026-06-28 13:15:53 -05:00
2026-06-28 13:15:53 -05:00
2026-06-28 16:25:55 -05:00
2026-06-28 13:15:53 -05:00
2026-06-28 13:15:53 -05:00
2026-06-28 13:15:53 -05:00
2026-06-28 13:15:53 -05:00

Boilerplate project for ESP32 (c3) development.

Specifications

Summary

This project is a starting point for ESP32-C3 development. It provides basic "infrastructure" and "Framework" for specific developments.

Functionality

  • Enables the user to connect to their WiFi network
    • Has a mechanism to "Factory reset" the device
  • Eases updates by providing OTA update mechanism using update-server URL
  • Role based User Management
    • Standard Roles
      • Sysadmin (Can administer the system, update, change settings)
      • UserAdmin (Can administer user accounts)
      • WebUIConnect (Allows logging in to the Web UI)
      • Debugger (Allowed to use API Test UI in the Web UI)
  • Secure, Role based Rest API for all functions
    • API functions access can be configured either as "Public" or a user role can be assigned to determine access (only users who have the corresponding rolle can call that API).
    • User authentication needs to return a token that is needed to be passed into all rest API requests (unless the called API endpoint is public)
    • All admin functions (including HTTPS configuration, logs, password changes, ...) are available in the API by default
    • All API calls will return a JSON that returns
      • Result of the last call (either success or detailed error description)
      • If applicable, the result of the call
    • There are 3 boilerplate APIs that are available publicly by default
      • Control the on-board LED (set brightness; 0-> off; 100->full brightness)
      • Ping (that returns the current uptime as JSON)
      • Add (takes 2 integers and returns the result of adding those integers)
  • Admin Web-UI
    • Role based security configuration
      • Roles are centrally maintained
    • User management
      • Users can change their password
      • Users can be assigned zero or more roles
    • Allows to interactively calling of the Rest APIs via the web UI
      • The corresponding user token is preset with the current user's token by decfault, but it can be overwritten
      • This functionality is only accessible when the user has the role Debugger
    • Enables secrity configuration of the exposed API functions
    • Configuration of logging
      • log level
      • log max size in non volatile memory (default 50kb)
    • Viewing- and management of logs
      • filter
      • search
      • view
      • clear logs
    • Networking configuration
      • Host name
      • DHCP or static IPv4 parameters
    • HTTPS certificate configuration
      • By default a self-signed certificate is used
      • Allows the set up of certificates for HTTPS (file upload)
    • Firmware update handling
      • Upload new firmware via file selector
      • Check URL for new firmware button (this will reach out to a configurable (in the code) URL to to try and find new firmware. If new firmware is available it offers to install.)

Technical details

  • The target platform is ESP32-C3 with Arduino Framework
  • Memory is to be treated as a sparse resource, so the size of the code must be kept small
  • The API layer exclusively uses JSON messages to communicate in both direcitons
  • The Web UI is to be implemented as a Vanilla JS single page web site UI that communicates with the backend via the REST API.
  • All documentation goes into the README.md file
  • The Admin UI is implemented in an extendable way, so that developers can easily use the same mechnism to implement their own UIs
  • The "factory reset" function should execute when a definable PIN (choose a good default) is pulled high- or low on boot and held that way for 10 seconds.
  • When starting after initial flashing or factory reset, the device needs to act as an unsecured access point with a defined SSID .
    • The user can then connect to that AP and is presented with a basic user interface (setup screen) where the user can
      • Choose their WIFI
      • Enter the password of their WIFI (can be left empty for unsecuried wifi)
      • Change the admin useranme (defaults to "admin")
      • Enter an Admin password (can be left empty)
      • A submit button that submits the entered information
    • Once the user entered submitted information, the information is stored in non volatile memory, and the device is restarted.
      • On subsequent startups, the device looks for configuraiton stored in non volatile memory
      • The factory reset functionality deletes the information from the non volatile memory, which will lead to the setup screen.
  • Logging is done based on log level.
    • Log levels are Error, Warn, SecurityAudit, Info, and Debug; SecurityAudit records security-relevant events such as login/logout, invalid bearer tokens, authorization failures, unknown API URLs/methods, and security configuration changes.
    • The logs are stored in non volatile memory
    • they need to be implemented as a ring buffer that occupies a configurable space in non volatile memory
    • There is a standard API endpoint that returns the logs (restricted to user role Debugger)

Source organization

The firmware is split by responsibility:

Path Responsibility
src/main.cpp Arduino setup()/loop() and boot orchestration
include/app.h Framework constants, state, structs, and function declarations
include/custom_api.h Custom API handler declarations
src/config/api_definitions.cpp Central API catalog, route handlers, and default role/public access mapping
src/config/api_definitions_custom.cpp Custom API catalog and default public access mapping
src/core/state.cpp Global state, project/device identity, hashing, persisted settings
src/core/logging.cpp LittleFS log ring buffer
src/core/auth.cpp Users, roles, tokens, and API authorization
src/core/device.cpp LED control, factory reset, and WiFi connection
src/util/json_utils.cpp Small JSON response and request parsing helpers
src/web/ui.cpp LittleFS-backed HTML serving and captive-portal helper pages
src/web/routes.cpp Page/captive route registration and generic API route registration from framework and custom API catalogs
src/handlers/handlers_setup.cpp Setup and WiFi scan route handlers
src/handlers/handlers_custom_api.cpp Custom API handlers for ping, add, and LED brightness
src/handlers/handlers_api.cpp API ACL list/detail management handlers
src/handlers/handlers_auth.cpp Login, users, roles, and password route handlers
src/handlers/handlers_admin.cpp Settings, logs, and certificate route handlers
src/handlers/handlers_ota.cpp Firmware upload and OTA route handlers
data/setup.html First-run provisioning UI served from LittleFS
data/admin.html Admin UI served from LittleFS

Current implementation

The firmware in src/main.cpp implements the boilerplate as a compact Arduino ESP32-C3 application using built-in ESP32 Arduino libraries only:

  • WiFi provisioning access point and first-run setup UI.
  • Factory reset on boot by holding FACTORY_RESET_PIN high/low default behavior: GPIO4 held LOW for 10 seconds.
  • Admin Web UI served from /.
  • Token-based login with role checks.
  • Configurable API access control where each route can be PUBLIC or require one role.
  • Public boilerplate APIs: ping, add, and LED brightness.
  • LED brightness is persisted in non-volatile memory, applied on boot, and loaded into the Admin UI slider.
  • DS18B20 temperature sensor readings are exposed in Celsius and Fahrenheit through the live Admin UI card and /api/temperature endpoint.
  • A dashboard pump tile switches a 5 V DC pump through /api/pump.
  • A dashboard Program tile can run the pump automatically from the measured temperature and a persisted target temperature.
  • User management with the standard roles Sysadmin, UserAdmin, WebUIConnect, and Debugger.
  • Custom role management. System roles are protected and cannot be deleted.
  • Active/inactive user accounts with role checkboxes in the Admin UI.
  • API security management through a list/detail UI with a role dropdown per API.
  • Ring-buffer logging in LittleFS with default maximum size of 50 KiB.
  • Firmware and LittleFS filesystem upload OTA and update-from-URL hooks.
  • HTTPS certificate storage API. The default Arduino WebServer runs HTTP; stored certificate material is available for applications that add TLS termination.
  • Network settings for hostname plus DHCP/static IPv4 configuration.

Web UI files

The setup and admin pages are stored as editable HTML files in the PlatformIO data/ directory and are served from LittleFS by request path:

URL File
/ in setup mode /setup.html
/ in normal mode /admin.html
/setup.html /setup.html
/admin.html /admin.html

After changing files in data/, upload the filesystem image as well as the firmware:

pio run -t uploadfs

In the PlatformIO UI, use the Upload Firmware and Filesystem project task when you want one action to upload both firmware and the LittleFS image that contains the web UI.

Uploading the filesystem image replaces the LittleFS contents, including stored log files. WiFi configuration, users, roles, and settings are stored in NVS preferences and are not part of that filesystem image.

Provisioning

After initial flashing or factory reset, the device starts an unsecured access point:

TSL-Embedded-XXXXXX

Connect to the access point and open:

http://192.168.1.1/

While in setup mode, the device also runs a captive-portal DNS responder. Most phones and laptops will automatically open or suggest the setup page after joining the AP; unknown HTTP requests are redirected to http://192.168.1.1/.

The setup page lets you choose WiFi, set the WiFi password, set the admin username, and set the admin password. The default admin username is admin; the password may be empty. After submit, the device stores the configuration in non-volatile memory and restarts.

The default project name is TSL-Embedded. The firmware combines the project name with the chip suffix to form the device name, for example TSL-Embedded-BDF5F0. That name is used as the setup AP SSID, the station-mode WiFi hostname, and the mDNS hostname. Override the project name at compile time if needed:

#define PROJECT_NAME "MyProject"

On normal boot, the device connects to the configured WiFi and serves the Admin UI at the IP printed to serial.

Networking

Open Networking in the Admin UI to configure the station-mode host name and IP parameters.

The host name field is optional. If it is empty, the firmware uses the generated default host name based on PROJECT_NAME and the chip suffix, for example TSL-Embedded-BDF5F0. A custom host name must be 1-31 characters and may contain only letters, digits, and hyphens. It cannot start or end with a hyphen.

Address mode defaults to DHCP. To use a static IPv4 address, select Static IPv4 and provide:

Field Required Example
Static IP Yes 192.168.1.50
Gateway Yes 192.168.1.1
Subnet mask Yes 255.255.255.0
DNS 1 No 192.168.1.1
DNS 2 No 8.8.8.8

Networking changes are stored immediately, but they apply on the next WiFi reconnect or reboot.

Factory reset

The default reset pin is GPIO4. Hold GPIO4 LOW during boot for 10 seconds to clear stored configuration and logs, then the device restarts into setup mode.

The defaults can be changed at compile time:

#define FACTORY_RESET_PIN 4
#define FACTORY_RESET_ACTIVE_LEVEL LOW

DS18B20 temperature sensor

The live Admin UI card reads a DS18B20 one-wire temperature sensor and displays both Celsius and Fahrenheit. The default data pin is GPIO3, which is exposed as D1 on the Seeed Studio XIAO ESP32C3.

Connect the 3-wire sensor as follows:

Sensor wire Connect to device Notes
Black GND Common ground
Yellow GPIO3 / D1 One-wire data signal
Red 3V3 Use the board's 3.3 V output

Add a 4.7 kOhm pull-up resistor between the Yellow data wire and the Red 3V3 wire. The DS18B20 bus needs this pull-up for reliable readings; do not rely on the ESP32 internal pull-up.

Keep the sensor powered from 3V3, not 5V, so the data line stays safe for the ESP32-C3. If you need a different data pin, override the default at compile time:

#define DS18B20_PIN 3

Read the current temperature with:

GET /api/temperature
Authorization: Bearer <token>

The response includes temperatureC, temperatureF, sensorConnected, and pin. The live dashboard subscribes to:

GET /api/temperature/events
Authorization: Bearer <token>

The SSE stream emits temperature events with the temperature payload and pump events with enabled and pin.

DC pump switch

The dashboard includes a Pump tile that switches a 5 V DC pump on and off. The default control pin is GPIO5, exposed as D3 on the Seeed Studio XIAO ESP32C3.

Use a separate 5 V supply that can provide more than the pump's rated current. A 5 V, 3 W pump draws about 600 mA while running and can draw more at startup. The ESP32 pin must only drive the transistor base; never power the pump from an ESP32 GPIO pin.

BC337 low-side switch schematic without a flyback diode:

                         +5 V pump supply
                              |
                            Pump
                              |
                              +------ C
                                     |
GPIO5 / D3 -- 330 ohm to 1 kOhm -- B  BC337
                                     |
                              +------ E
                              |
GND --------------------------+------------------ 5 V supply GND

Optional: add 100 kOhm from BC337 base to GND to keep the pump off while the ESP32 boots.

Connections:

Circuit node Connect to
Pump positive wire External +5 V
Pump negative wire BC337 collector
BC337 emitter Common GND
BC337 base GPIO5 / D3 through a 330 Ohm to 1 kOhm resistor
ESP32 GND External 5 V supply GND

This simplified diagram omits the flyback diode. A DC pump motor is an inductive load, so omitting the diode can let turn-off voltage spikes stress or damage the BC337 and possibly the ESP32. Use this version only if your pump module already includes suppression or you have another protection method.

Check the BC337 pinout from the exact transistor datasheet or package marking before wiring it; TO-92 pin order is not universal across manufacturers.

Important current note: a BC337 can switch this pump only marginally. At 600 mA, it may not saturate well from an ESP32 GPIO pin, can drop voltage, and can heat up. For reliable continuous use, replace the BC337 with a logic-level N-channel MOSFET rated for at least 1 A, keeping the same low-side layout. The BC327 is a PNP transistor and is not needed for this low-side switch.

If you need a different control pin, override the default at compile time:

#define PUMP_PIN 5

Pump API:

GET /api/pump
Authorization: Bearer <token>
POST /api/pump
Authorization: Bearer <token>
Content-Type: application/json

{"enabled":true}

The response includes enabled and pin. The pump defaults to off after boot.

Temperature program

The dashboard Program tile controls the pump automatically from the DS18B20 temperature reading. Program settings are stored in NVS preferences, and the control loop runs in firmware even when no browser client is connected.

Modes:

Mode Behavior
off Keeps the pump off
cool Runs the pump when measured temperature is above the target
warm Runs the pump when measured temperature is below the target

The target temperature and threshold are stored in Celsius and support up to two decimals. The firmware compares the measured temperature, target temperature, and threshold at two-decimal precision. The threshold acts as the start offset only. In cool mode, the pump starts when the measured temperature is at least the threshold above the target and then stops only when the measured temperature reaches the target again. In warm mode, the pump starts when the measured temperature is at least the threshold below the target and then stops only when the measured temperature reaches the target again. The default threshold is 0.25 C.

The max runtime is stored in minutes and defaults to 5 minutes. It limits one continuous automatic pump run. If the pump runs for the configured max runtime without reaching the target temperature, the firmware switches the program mode to off, turns the pump off, persists the new mode, and emits a program SSE event with only the changed fields, such as mode, pumpEnabled, and reason, so the Program tile can update without overwriting settings the user is editing.

Read the current program:

GET /api/program
Authorization: Bearer <token>

Set the program:

POST /api/program
Authorization: Bearer <token>
Content-Type: application/json

{"mode":"cool","targetTemperatureC":22.75,"toleranceC":0.25,"maxRuntimeMinutes":5}

The response includes mode, targetTemperatureC, targetTemperatureF, toleranceC, maxRuntimeMinutes, pumpEnabled, sensorConnected, and the latest temperatureC.

Authentication

Login:

POST /api/login
Content-Type: application/json

{"username":"admin","password":""}

The response contains a bearer token. Pass it to protected APIs:

Authorization: Bearer <token>

HTTPS certificate configuration

The Admin UI stores HTTPS certificate material so applications built on this boilerplate can use it when adding TLS termination. The default Arduino WebServer used by this project serves HTTP only; uploading a certificate stores the material in NVS preferences but does not by itself switch the built-in web server to HTTPS.

To configure the stored certificate material:

  1. Log in as a user with the Sysadmin role.
  2. Open Networking -> HTTPS Certificate.
  3. Select a certificate file and click Save certificate.
  4. Use Load current to verify what is currently stored.

The upload file must be a plain text PEM-style file. Use UTF-8 or ASCII text and preserve the PEM block line breaks exactly. The file may use .pem, .cer, .crt, or .txt.

Valid content is one or more PEM blocks, for example a certificate chain:

-----BEGIN CERTIFICATE-----
...base64 certificate data...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
...base64 intermediate certificate data...
-----END CERTIFICATE-----

If your TLS integration expects both the certificate and private key from this stored value, put both PEM blocks in the same text file:

-----BEGIN CERTIFICATE-----
...base64 certificate data...
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
...base64 private key data...
-----END PRIVATE KEY-----

Do not upload binary DER, PKCS#12/PFX, or password-protected keystore files directly. Convert those to PEM text first. The equivalent API is:

POST /api/cert
Authorization: Bearer <token>
Content-Type: application/json

{"certificate":"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"}

API response shape

Every API returns JSON with a success field. Errors include an error string:

{"success":false,"error":"Authentication required"}

Public boilerplate APIs

Ping:

GET /api/ping

Add two integers:

POST /api/add
Content-Type: application/json

{"a":1,"b":2}

Set LED brightness from 0 to 100:

POST /api/led
Content-Type: application/json

{"brightness":50}

Admin APIs

Protected APIs and their default roles:

API Role
POST /api/logout WebUIConnect
GET /api/me WebUIConnect
GET /api/pump WebUIConnect
POST /api/pump WebUIConnect
GET /api/program WebUIConnect
POST /api/program WebUIConnect
GET /api/temperature WebUIConnect
GET /api/temperature/events WebUIConnect
GET /api/apis Sysadmin
POST /api/apis Sysadmin
GET /api/users UserAdmin
POST /api/users UserAdmin
POST /api/password WebUIConnect
GET /api/settings Sysadmin
POST /api/settings Sysadmin
GET /api/logs Debugger
POST /api/logs/clear Debugger
GET /api/files Debugger
GET /api/files/download Debugger
POST /api/factory-reset Sysadmin
POST /api/ota/check Sysadmin
POST /api/ota/run Sysadmin
POST /api/update Sysadmin
GET /api/cert Sysadmin
POST /api/cert Sysadmin

Change API security:

POST /api/apis
Authorization: Bearer <token>
Content-Type: application/json

{"path":"/api/led","method":"POST","role":"WebUIConnect"}

Use "role":"PUBLIC" to make a known API public.

Project scripts

The scripts/ directory contains small build and packaging helpers. They assume they are run from the project checkout and use the seeed_xiao_esp32c3 PlatformIO environment by default.

Script Purpose
scripts/platformio_targets.py Registers the custom PlatformIO target uploadall, shown in the PlatformIO UI as Upload Firmware and Filesystem.
scripts/create_update_package.py Combines an existing firmware image and LittleFS image into one .tslpkg OTA update package.
scripts/build_ota_package.py Builds LittleFS and firmware, then creates a timestamped .tslpkg file in dist/.
scripts/build_upload_device.py Builds LittleFS and firmware, then uploads both to a connected device.

Create a timestamped OTA package in dist/:

python scripts/build_ota_package.py

Useful options:

python scripts/build_ota_package.py -e seeed_xiao_esp32c3 -o dist
python scripts/build_ota_package.py --pio C:\Users\<user>\.platformio\penv\Scripts\pio.exe

Upload firmware and filesystem to a connected device:

python scripts/build_upload_device.py --upload-port COM3

Useful options:

python scripts/build_upload_device.py -e seeed_xiao_esp32c3 -p COM3
python scripts/build_upload_device.py --pio C:\Users\<user>\.platformio\penv\Scripts\pio.exe

Create a combined package from already-built images:

pio run
pio run -t buildfs
python scripts/create_update_package.py

Useful options:

python scripts/create_update_package.py --firmware .pio/build/seeed_xiao_esp32c3/firmware.bin --filesystem .pio/build/seeed_xiao_esp32c3/littlefs.bin --output .pio/build/seeed_xiao_esp32c3/update.tslpkg

Firmware and filesystem updates

Because the Admin UI is stored in LittleFS, OTA releases are distributed as one combined update package. The package contains both the LittleFS filesystem image and the firmware image, so UI and firmware changes cannot drift apart during an update.

Artifact Purpose Build output
firmware.bin Intermediate ESP32 application firmware .pio/build/seeed_xiao_esp32c3/firmware.bin
littlefs.bin Intermediate files from data/, including admin.html and setup.html .pio/build/seeed_xiao_esp32c3/littlefs.bin
update.tslpkg Single OTA package containing LittleFS plus firmware .pio/build/seeed_xiao_esp32c3/update.tslpkg

Create the single OTA package:

python scripts/build_ota_package.py

For a direct USB flash during development:

python scripts/build_upload_device.py --upload-port COM3

For OTA updates, host update.tslpkg on your update server. The Admin UI has one update package URL. The default is:

http://example.com/update.tslpkg

The package format is intentionally small:

Offset Size Value
0 8 Magic bytes TSLUPD1\0
8 4 Header size, little-endian uint32, currently 32
12 4 LittleFS image size, little-endian uint32
16 4 Firmware image size, little-endian uint32
20 12 Reserved, zero-filled
32 variable LittleFS image bytes
32 + littlefsSize variable Firmware image bytes

Upload an update package from the Admin UI or post multipart form data to:

POST /api/update

/api/ota/check checks reachability and content length for the configured package URL. /api/ota/run streams the package from the URL, applies the LittleFS image first, applies the firmware image second, and restarts.

Installing an update package replaces the LittleFS partition, including stored log files. WiFi configuration, users, roles, API ACLs, and settings are stored in NVS preferences and are not part of the LittleFS image.

Description
No description provided
Readme 1.3 MiB
Languages
C++ 54%
JavaScript 29.2%
HTML 7%
Python 4.9%
C 4.3%
Other 0.6%