Getting Started
Take one bare ESP32-S3 dev board from unboxed to publishing sensor data over MQTT. No soldering, no custom hardware - a dev board and a USB cable. Plan for about 30 minutes the first time, most of it tool install and the first build.
This walkthrough uses the esp32-s3-debug build: USB serial, SHT31 temperature and
humidity sensor enabled, cellular and battery hardware switched off. It is the
firmware’s bench target and the right place to start before moving to a field board.
What you need
| Item | Notes |
|---|---|
| ESP32-S3 dev board | Any bare ESP32-S3 devkit with USB. PSRAM build is assumed (most S3 devkits have it) |
| USB cable | Data cable, not charge-only |
| A computer | macOS, Linux, or Windows with Python 3 and git |
| A WiFi network | 2.4 GHz SSID and password the board can reach |
mosquitto clients |
mosquitto_sub / mosquitto_pub, to watch the device publish |
The SHT31 sensor is optional. Without it the board still boots, connects, and publishes device telemetry (heap, MQTT status); the temperature rows just stay empty.
1. Clone and install the toolchain
The firmware builds with PlatformIO. Install the CLI
(pip install platformio), then clone the repo:
git clone https://github.com/Thesada/thesada-fw.git
cd thesada-fw
python3 scripts/check_deps.py
check_deps.py verifies PlatformIO and the pinned library versions before you build.
2. Stage the filesystem image
The firmware reads its config and TLS root from a LittleFS partition. Seed both before flashing:
cp examples/config.json.example data/config.json
curl -o data/ca.crt https://test.mosquitto.org/ssl/mosquitto.org.crt
data/config.jsonis the editable runtime config. Make two edits before flashing:web.password- a real value. The firmware refuses every admin login while the password ischangemeor empty, so the placeholder would lock you out of the Config tab in step 5. There is no default-credential window.wifi.ap_password- at least 8 characters, and notchangeme. The fallback AP refuses to start on an absent, short, or placeholder passphrase, so leaving the shipped empty value in place means no captive portal appears in step 5.
Keep
web.enabled: trueas shipped (without it the HTTP server, dashboard, and captive portal never start), and leave the placeholder WiFi in place for now - you will set the real SSID over the web UI in step 5.data/ca.crtis the CA the device trusts for MQTT over TLS. The firmware always uses TLS, so the broker’s CA must be present. This walkthrough uses the publictest.mosquitto.orgbroker, so its CA goes here. For your own broker, use that broker’s CA instead.
3. Build and flash
Plug in the board, then flash the firmware and the filesystem image:
pio run -e esp32-s3-debug --target upload # firmware
pio run -e esp32-s3-debug --target uploadfs # LittleFS (config.json + ca.crt)
The first build downloads the toolchain and compiles every module, so it takes a
few minutes; later builds are incremental. A successful upload ends like this:
Auto-detected: /dev/ttyACM0
Chip is ESP32-S3 (QFN56) (revision v0.2)
Features: WiFi, BLE, Embedded PSRAM 8MB
Writing at 0x0015ef79... (100 %)
Wrote 1372896 bytes (860762 compressed) at 0x00010000 in 9.0 seconds
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
========================= [SUCCESS] Took 50.28 seconds =========================
uploadfs then writes the LittleFS image and lists what it packed:
Building FS image from 'data' directory to .pio/build/esp32-s3-debug/littlefs.bin
/ca.crt
/config.json
Wrote ... Hash of data verified.
The /ca.crt is what lets the TLS broker connection verify in step 6.
4. Open the serial console
Watch the boot log at 115200 baud:
pio device monitor -e esp32-s3-debug
The bare S3 uses the chip’s native USB-Serial/JTAG, which re-enumerates on reset,
so a monitor opened beforehand stays blank. Press the RST button, then launch
the monitor within a couple of seconds: the firmware waits up to 3 s for a USB
host, so it catches the boot from the top. If more than one board is plugged in,
pin the port with -p /dev/ttyACM0 so you watch the right one.
You should see the modules register, LittleFS mount, and - because the placeholder WiFi cannot connect - the device fall back to a setup access point:
ESP-ROM:esp32s3-20210327
entry 0x403c98d0
[INF][Boot] thesada-fw <version>
[INF][Identity] identity.generated device_id=thesada-0123456789ab
[INF][WiFi] wifi.scan_start
[INF][WiFi] wifi.scan_done visible=18
[INF][WiFi] wifi.connect_attempt ssid=primary-ssid rssi=-67 attempt=1 max=2 timeout_s=10
[WRN][WiFi] wifi.state_change from=scanning to=all_failed reason=all_ssids_failed
[WRN][WiFi] wifi.fallback_ap_started ssid=thesada-0123456789ab-setup ip=192.168.4.1 timeout_s=300
The AP name is <device_id>-setup. The device id is minted on first boot from the
board’s factory MAC, so it is unique per unit and yours will differ from the one
above - read it off the boot log, or run identity.info on the serial console. The
AP times out after 5 minutes and retries the WiFi scan, so if it disappears just
wait for it to come back. (Log lines carry no timestamp until NTP syncs - on the
fallback AP there is no internet yet, so they stay bare.)
If the passphrase from step 2 never landed, the AP does not come up at all and the log says so instead:
[WRN][WiFi] wifi.ap_refused reason=no_password hint="seed via secret.set wifi.ap_password"
reason=default_or_short_password is the other case: a passphrase that is under 8
characters or still the changeme placeholder. Set a real one and reboot.
5. Set WiFi and broker over the captive portal
- On a laptop or phone, join the
<device_id>-setupWiFi network with thewifi.ap_passwordyou set in step 2. A captive portal opens the dashboard automatically; if it does not, browse tohttp://192.168.4.1/. - Open the Config tab and log in as
adminwith theweb.passwordyou set in step 2.changemeand an empty password are rejected by design - the firmware locks the whole admin surface until a real password is set. If you flashed with the placeholder, set one over the serial console (secret.set web.password) and reload. -
The Config tab is a JSON editor holding the full
config.json. Set your network and broker:"wifi": { "networks": [ { "ssid": "your-ssid", "password": "your-password" } ] }, "mqtt": { "broker": "test.mosquitto.org", "port": 8883, "topic_prefix": "thesada/demo/s3-01" }test.mosquitto.orgis open and unauthenticated, somqtt.userandmqtt.passwordcan stay empty for this first run. Pick atopic_prefixunlikely to clash with anyone else on the public broker. - Click Save & Restart. The device writes
config.json, reboots, and applies the new settings.
6. Watch it connect and publish
Re-open the serial console (RST, then launch). After the reboot the device joins your WiFi, syncs NTP, loads the CA, and connects over TLS:
[INF][WiFi] wifi.state_change from=scanning to=connected ssid=your-ssid ip=192.168.1.42
[INF][MQTT] mqtt.ca_loaded path=/ca.crt bytes=1310 heap=PSRAM
[INF][MQTT] mqtt.connect_start client_id=thesada-0123456789ab
[INF][MQTT] mqtt.state_change from=disconnected to=connected broker=test.mosquitto.org
From your computer, subscribe to the device’s prefix and watch it publish. Point
mosquitto_sub at the same CA you uploaded:
curl -o mosquitto.org.crt https://test.mosquitto.org/ssl/mosquitto.org.crt
mosquitto_sub --cafile mosquitto.org.crt -h test.mosquitto.org -p 8883 \
-t 'thesada/demo/s3-01/#' -v
thesada/demo/s3-01/status online
thesada/demo/s3-01/info {"firmware_version":"<version>","hardware_type":"esp32-s3","board":"s3-bare","chip_model":"esp32-s3","chip_revision":0,"chip_cores":2,"mac":"<mac>","psram":true,"build_time":"Jan 1 2026 12:00:00","config_hash":"<sha256>","scripts_main_hash":"<sha256>","scripts_rules_hash":"<sha256>"}
The mac field is shown as a placeholder deliberately. test.mosquitto.org is unauthenticated and world-readable, and device_id is derived from that MAC, so a real device pointed at a public broker publishes its identity to anyone watching. Use a private broker and a prefix of your own for anything but this demo.
A retained online on .../status plus an .../info payload confirm the device is
connected and publishing. Telemetry (heap, MQTT state, and SHT31 readings if the
sensor is attached) follows on its own topics.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No serial output | Charge-only cable or wrong port | Use a data cable; check the port in pio device list |
| Monitor stays blank on an S3 | Native USB re-enumerates on reset | Press RST, then launch the monitor within ~3 s; pin -p /dev/ttyACMx if several boards are attached |
| Watching the wrong device | More than one board plugged in | pio device list, match by MAC, then -p the right port |
Could not find port / permission denied (Linux) |
Missing udev rules | Install 99-platformio-udev.rules (see the PlatformIO udev-rules docs) |
| AP never appears | WiFi actually connected, or AP timed out | Check the log for a connect line; wait for the 5 min retry |
AP never appears, log says wifi.ap_refused |
wifi.ap_password absent, under 8 chars, or still changeme |
Set a real passphrase in config.json, or secret.set wifi.ap_password <value> over serial, then reboot |
| MQTT never connects | Missing or wrong ca.crt |
Re-run the uploadfs step with the broker’s CA in data/ca.crt |
mosquitto_sub sees nothing |
Prefix mismatch | Match the -t filter to the topic_prefix you set |
Next steps
You now have a device that boots, connects, and publishes. From here:
- Build your first module - a sensor module of your own, publishing to MQTT, with a Lua rule reacting to it.
- Why this way - the design decisions behind the firmware, stated as opinions.
- Write your own alert rules - the Lua runtime lets you add rules without recompiling.
- Provisioning - the serial-console and over-MQTT paths for configuring a device.
- Config Management - the full
config.jsonschema. - Modules - what each hardware module does and how to enable it.