A 7-day weather display for the Waveshare ESP32-P4-WIFI6-Touch-LCD-7B (1024×600 MIPI-DSI, GT911 touch). The interface is the "Weather App" design from Claude Design, ported to LVGL 9; the data is live from Open-Meteo (no API key required).
Everything is operable from the touchscreen alone — including joining a Wi-Fi network and installing firmware updates — so the device never needs a serial cable after the first flash.
| Main screen | Settings |
|---|---|
![]() |
![]() |
Screenshots are from the host simulator (./scripts/sim.sh --screen main),
which renders the same LVGL UI source the firmware does — see
Host simulator below.
- Current conditions, 7-day forecast strip, and a slide-up per-day detail panel with an hourly temperature + precipitation chart (with a dotted rain-chance line). Wind shows direction and gusts; the daily rain value is "chance · amount"; night hours get moon icons.
- Sunrise/sunset, UV index, and air quality (from a separate Open-Meteo endpoint) on the current-conditions card.
- Up to three favorite cities, alongside city search over Open-Meteo's geocoding API with the on-screen keyboard.
- Adaptive brightness from the board's optional OV5647 camera (ambient light → backlight), with a settling/EMA policy tuned to avoid flicker. Absent gracefully if no camera is fitted.
- On-device Wi-Fi setup: scan, pick a network, type the passphrase, or enter a hidden SSID by hand. Credentials persist in NVS.
- On-device firmware updates: checks a GitHub Release's manifest, downloads and flashes the ESP32-P4 image over HTTPS with a SHA-256 check against the manifest, and optionally updates the ESP32-C6 Wi-Fi coprocessor too — see Firmware updates.
- Device Info dialog: firmware/coprocessor versions, hardware revision, network details, last successful sync.
- Settings: language (EN/DE/ES/FR), °C/°F, km/h · mph · m/s, 12/24-hour clock, auto-refresh interval. All of it persists across reboots.
- Nixie-tube screen saver after 15, 30 or 60 minutes without a touch (or off): six glowing digits showing the local time, plus the date and current temperature, slowly drifting to avoid burn-in. Colours follow the app theme or a fixed Nixie orange; a tap ends it. See Screen saver.
- Header shows online/offline and warns when data is more than an hour stale.
main/
├── main.c display + Wi-Fi bring-up, wiring
├── app_wifi.c/h station, scanning, runtime provisioning, SNTP
├── app_weather.c/h Open-Meteo forecast + geocoding worker task, OTA dispatch
├── app_favorites.c/h favorite-city slots (thin adapter over app_logic/favorites)
├── app_light.c/h camera-driven adaptive brightness (V4L2/esp_video)
├── app_prefs.c/h NVS-backed settings and selected city
├── app_format.c/h locale dates/times and the "real feel" sentence
├── ota_update.c/h dual-chip OTA: manifest fetch, HTTPS flash, esp-hosted RPC OTA
├── app_config.h constants shared across main/
├── auto_refresh_options.h shared auto-refresh interval table
├── storage_backend_nvs.c/h NVS implementation of app_logic's storage_backend_t
├── save_debounce.c/h coalesces rapid settings writes into one NVS commit
├── task_heartbeat.c/h per-task liveness budgets
├── display_lock_probe.c/h warns when the LVGL display lock is held too long
├── app_heap_probe.c/h periodic heap report (adapter over app_logic/heap_watch)
├── lv_mem_psram.c LVGL allocator that prefers PSRAM over internal RAM
├── ui_fonts.h Inter ⇄ Montserrat font mapping
├── weather_chart.c/h hourly temperature + precipitation chart
├── weather_saver.c/h Nixie screen saver overlay (digits in weather_saver_digits.h)
├── weather_ui.c/h ── imported from Claude Design ──
├── weather_icons.c/h ── imported from Claude Design ──
├── weather_i18n.c/h ── imported from Claude Design ──
└── fonts/ generated Inter faces (tools/gen_fonts.sh)
components/app_logic/ hardware-free application logic, host-tested (see Testing)
├── favorites.c favorite-slot selection logic
├── light_policy.c ambient-light EMA/threshold policy for app_light.c
├── storage_record.c versioned NVS record round-trip, over storage_backend.h
├── weather_forecast_parse.c Open-Meteo JSON → plain struct
├── ota_manifest_parse.c manifest.json → plain struct
├── ota_version_compare.c semver-ish "is remote newer" comparison
├── wifi_reconnect_policy.c reconnect back-off decisions
├── link_health_policy.c consecutive fetch failures → verify / reconnect escalation
├── coprocessor_health_policy.c when to restart the C6 link, with cooldown and give-up
├── startup_retry_policy.c retry delays for the initial network steps
├── network_status_policy.c error state and refresh-toast transitions
├── screensaver_policy.c when the saver starts/stops, digit split, burn-in drift
├── hold_gesture.c long-press detection for the hidden saver trigger
└── heap_watch.c low-heap detection and report throttling
host_test/ Unity tests for components/app_logic/, run on the Linux target
sim/ LVGL UI ported to desktop SDL, no board or emulator needed
scripts/ host-test.sh, sim.sh, fw-build.sh, hw-flash.sh,
hil-outage-test.sh (see Testing), C6 UART flash helper
patches/ local patches against managed_components/ (see patches/README.md)
docs/ screenshots, board schematic, ESP-Hosted RPC v1/v2 notes
weather_ui, weather_icons and weather_i18n come from the design project's
lvgl_export/ directory and should be re-synced from Claude Design rather than
edited freely. Local changes to them are all marked with a FIX: comment so a
future pull stays a readable merge. Everything else is firmware written here.
components/app_logic/ is a stricter boundary than "everything else": no IDF
headers, no driver/, no freertos/, no registers — time and I/O come in
through small callback structs (e.g. storage_backend_t) so the same code
compiles and runs on a desktop Linux target under host_test/. New parsing,
state-machine, or protocol logic belongs here, not in main/.
Built and verified against ESP-IDF v6.1. The dual-chip release workflow
(.github/workflows/Manual_Dual-Chip_Release_Build.yml) also builds against
v6.1. Earlier v6.x should work but isn't actively tested.
Note for IDF 6: cJSON is no longer bundled in IDF core, so it is pulled from the
component registry as espressif/cjson (already declared in
main/idf_component.yml).
./scripts/fw-build.sh # build into build/, no flashing
./scripts/hw-flash.sh /dev/ttyACM0 # build, flash, read and check the boot logThe scripts set up ESP-IDF themselves (scripts/lib/idf-env.sh): first
~/.espressif/tools/activate_idf_v6.1.sh (override with IDF_ACTIVATE), then
$IDF_PATH/export.sh. Exit code 127 means ESP-IDF was not found. Logs land in
.logs/. For idf.py menuconfig (default city, refresh interval, fallback
Wi-Fi) activate IDF in your own shell; avoid idf.py monitor without a TTY.
Dependencies resolve automatically through the component manager
(main/idf_component.yml): the Waveshare BSP 3.0.1, LVGL 9.5.0,
esp_hosted 3.0.* / esp_wifi_remote for the radio, and
esp_video/esp_cam_sensor for the optional camera.
dependencies.lock is committed. It pins the component versions that the local
ESP-IDF installation resolved (for example esp_hosted 3.0.7), so local builds,
the release workflow and its test channel build the same versions; the workflow
fails if its own resolution would change the lock. To move to newer versions run
idf.py update-dependencies, re-apply the patch below, test on the board and
commit the lock.
managed_components/ is gitignored and never edited directly. Required
changes live in patches/ (currently esp_hosted_sdio_reserve.patch, which
reserves SDIO transport buffers) and must be re-applied after the dependencies
are re-resolved — see patches/README.md.
The ESP32-P4 has no radio of its own — Wi-Fi runs over SDIO to the on-board ESP32-C6. On a device with no stored network, the Wi-Fi setup screen opens by itself; pick a network and type the passphrase. Credentials go to NVS, so it only happens once. You can also pre-seed a network under Weather Display Configuration → Fallback WiFi SSID if you prefer.
After the configured inactivity time the UI is replaced by a Nixie-tube clock
(main/weather_saver.c): six tubes with the local time, the short date and
the current temperature. The whole thing drifts slowly to spread wear on the
panel. Any tap dismisses it and restarts the countdown.
Settings → Screen saver has two options, stored in their own NVS record and kept across reboots:
- Screen saver after inactivity: Off, 15, 30 or 60 minutes.
- Screen saver colours: App theme or Nixie orange.
| App theme | Nixie orange |
|---|---|
![]() |
![]() |
Both are simulator captures (./scripts/sim.sh --screen screensaver and
--screen screensaver-orange).
Holding the clock in the header for 3 seconds starts the saver immediately, whatever the timeout setting says (also in release builds; nothing in the UI hints at it).
The saver needs the city's UTC offset, so it only starts after the first
successful weather fetch. The digits, glow, tube cage and base are
pre-rendered into images and the normal UI is hidden while the saver runs:
drawing them live as LVGL lines froze the display for tens of seconds. The
start/stop decisions live in components/app_logic/ (screensaver_policy.c,
hold_gesture.c) and are host-tested; how to test the rest without waiting
for the timeout is in docs/screensaver-test.md.
Settings → Network → Update opens a dialog that checks the latest GitHub Release for this repository:
- Fetches the release's
manifest.json(version + SHA-256 of each binary). - If the manifest's version is newer than the running firmware
(
components/app_logic/ota_version_compare.c), downloadsfirmware_p4.binover HTTPS viaesp_https_ota(), then re-reads the flashed partition and verifies its SHA-256 against the manifest before rebooting into it —esp_https_ota()validates the image header itself, but knows nothing about the externally-published hash. - On the next boot, if the "Update Wi-Fi coprocessor with application"
toggle is on, streams
esp32c6_hosted_slave.binto the ESP32-C6 over esp-hosted's own RPC OTA path, verifies it the same way, and reboots both chips to resync before marking the new P4 image valid (esp_ota_mark_app_valid_cancel_rollback()— an update that never gets this far rolls back automatically on the next reset).
The update runs on its own task, off the UI worker, so search/refresh/Wi-Fi stay responsive during a download; a Cancel tap requests a cooperative abort. An optional silent check runs every 12 hours if the "Auto-update" toggle is on, and won't reboot into a freshly-flashed image while a dialog is open on screen.
Releases are built by .github/workflows/Manual_Dual-Chip_Release_Build.yml
(manual workflow_dispatch), which builds both chips' firmware from this
repo and the pinned esp_hosted version's matching coprocessor example, and
publishes firmware_p4.bin + esp32c6_hosted_slave.bin + manifest.json.
The workflow has a channel input:
channel |
Tag | GitHub release | Who sees it |
|---|---|---|---|
test (default) |
v<version.txt>-test.<run number> |
pre-release, no release/ branch |
only devices with the Test channel switch on |
release |
v<version.txt> |
regular release, release/v… branch |
all devices |
The C6 runs esp-hosted slave firmware that must match the host component in
the P4 image (host espressif/esp_hosted 3.0.*, pinned to 3.0.7 in
dependencies.lock). It follows the release channel:
channel |
C6 firmware built |
|---|---|
test |
the newest v3.0.* tag of espressif/esp-hosted-mcu |
release |
exactly the tag in c6_release_version.txt (currently 3.0.7) |
c6_release_version.txt is only raised after that firmware passed the
hardware procedure in docs/c6-firmware-test.md (test build, on-board update,
./scripts/c6-compat-test.sh, outage test, update under load, soak). The
release run fails if the tag is missing, violates the constraint in
main/idf_component.yml, or builds a different version than the file says, so
an untested Espressif tag never reaches a release. The C6 is updated from the
device (toggle "Update Wi-Fi coprocessor with application" in the update
dialog); if a failed update leaves it unreachable, flash it over UART with
scripts/c6_firmware_via_UART/flash_c6_via_uart.sh (binaries_v3.0.7 is in
the repo).
Devices on the normal channel read GitHub's /releases/latest, which never
returns pre-releases. The Test channel switch in the update dialog
(Settings → Network → Update) makes the device look at the newest release of
any kind instead; it is stored in NVS and defaults to off. Test builds embed
<version.txt>-test.<run number> as their version, so bump version.txt
above the version the device is running before a test run, or the device
will not consider the test build newer. Switching back to the normal channel
never downgrades: a test build only updates to a newer real release. With
Auto-update on, a device on the test channel installs new test builds
unattended.
The design specifies Inter. LVGL's built-in Montserrat faces cover ASCII only,
which would leave holes in every accented word in the German, Spanish and French
string tables (Überwiegend, bewölkt, Sensación, dégagé, …). So the build
ships generated Inter faces with the full Latin-1 supplement, in the design's own
weights — Regular (400) for captions, Medium (500) for display sizes.
They are already generated in main/fonts/. To regenerate (Node required):
# Inter is SIL OFL 1.1: https://github.com/rsms/inter/releases
tools/gen_fonts.sh ~/Downloads/inter/extras/ttfLV_SYMBOL_* glyphs (the GPS pin, refresh, settings icons) are FontAwesome
codepoints that exist only in the Montserrat builds, so those specific labels
keep FONT_SYMBOL. That is why the header's location line is two labels rather
than one. Setting CONFIG_WEATHER_USE_INTER_FONTS=n falls back to Montserrat
everywhere and makes the app English-only in practice.
Four layers, matched to what each one can actually catch — see CLAUDE.md for
the full rationale. In order of speed:
| Changed | Run |
|---|---|
components/app_logic/** |
./scripts/host-test.sh |
UI rendering (weather_ui.c, weather_saver.c, weather_chart.c, weather_icons.c, weather_i18n.c, app_format.c, fonts) |
./scripts/sim.sh --shots <dir> |
everything else in main/, sdkconfig, dependencies, CMake |
./scripts/fw-build.sh |
| peripherals, startup, Wi-Fi, OTA, timing, memory layout | additionally ./scripts/hw-flash.sh [/dev/ttyACM0] (REQUIRE_IP=1 for Wi-Fi/OTA) |
Wi-Fi recovery (app_wifi.c, reconnect and link-health policies) |
additionally ./scripts/hil-outage-test.sh (a human triggers the outage) |
host_test/runs Unity tests for everycomponents/app_logic/module natively on the Linux target — no chip needed. This is where manifest/JSON parsing, version comparison, favorites logic, the ambient-light policy and the Wi-Fi/link/coprocessor recovery policies are actually verified.sim/builds the UI layer (weather_ui.c,weather_chart.c,weather_icons.c,weather_i18n.c,app_format.c) against desktop LVGL at the same resolution and color depth, in an SDL window — see Host simulator.fw-build.shbuilds the real firmware intobuild/without flashing; warnings inmain/andcomponents/app_logic/are errors.hw-flash.shis the only way to verify DSI timing, the camera, SDMMC, ESP-Hosted or startup — it builds viafw-build.sh, flashes, reads the boot log with a timeout (neveridf.py monitor, which breaks without a real TTY), and evaluates it withscripts/check-boot-log.sh(panics, reboots, missing>>> BOOT_OK <<<;REQUIRE_IP=1also requiresgot ip).
There is no emulator stage: none of the available emulators model MIPI-DSI, PPA, touch, PSRAM timing or ESP-Hosted. None of the first three layers prove the firmware boots on the actual board — the OTA download/flash path and the coprocessor RPC path in particular are hardware-only.
sim/ opens the UI in a 1024×600 SDL window — no board, no emulator. It
compiles the same weather_ui.c, weather_chart.c, weather_icons.c,
weather_i18n.c and app_format.c the firmware does, against the same
vendored LVGL 9.5 at the same color depth, and stands in for main.c +
app_weather.c with fixture data and fake geocoding, Wi-Fi scan/connect, and
OTA progress.
./scripts/sim.sh # interactive: mouse is the touchscreen
./scripts/sim.sh --screen settings # open one screen straight away
./scripts/sim.sh --shots shots/ # capture all screens as PNG
./scripts/sim.sh --screen settings --screenshot settings.bmp # one screen
./scripts/sim.sh --screen ota --lang de --screenshot ota-de.bmp # language: en|de|es|frRunning sim.sh without --shots or --screenshot opens a window and
blocks until it is closed.
Screens: main, search, settings, settings-adaptive-on, device-info,
forecast-icon-tap, refresh-toast, detail, wifi,
wifi-forget-confirm, favorite-tap, ota, ota-done, screensaver,
screensaver-orange, screensaver-dismiss, screensaver-hold, plus the first-boot
state via --wifi-setup (captured as first-boot by --shots). The capture path is headless
(SDL_VIDEODRIVER=dummy), so it works over SSH. Screens are opened by
walking the object tree for a known label and clicking its owner, not by
clicking fixed coordinates — a layout change moves the target without
breaking the capture.
What it does not show: DSI timing, PPA rotation, the tear-avoid mode, LVGL buffer sizes, PSRAM throughput, the real panel's color behavior, or any real network activity (Wi-Fi, HTTP, OTA downloads are all fixtures here).
Run it from any directory — the script resolves the repo from its own path. It
builds for the host, so a cross-toolchain ahead of /usr/bin on PATH breaks
it: gcc passes --64 to an as that only knows RISC-V or Xtensa. The ESP
toolchains ship an unprefixed as under <toolchain>/<target>/bin/, which is
where that usually comes from. sim.sh probes the compiler first and says so
rather than letting binutils explain it; which -a as gcc confirms it.
tools/extract-design.mjs renders design/src/weather-app/Weather App.dc.html
in headless Chrome and diffs it against the simulator's own screenshots, so
"does the port match the design" becomes a file instead of a memory.
./scripts/sim.sh --shots shots # the LVGL side
node tools/extract-design.mjs --vendor # once: fetch the React UMD builds
node tools/extract-design.mjs --compare shotsIt writes design-<screen>.png and compare-<screen>.png into shots-design/
for each screen it knows about. Each comparison sheet stacks design, LVGL and a
mix-blend-mode: difference overlay where anything the two agree on goes black.
No npm dependencies: Chrome is driven through its own --screenshot flag and
does the pixel diff itself. Two things the script has to work around, both
documented at the top of it — the design is not a standalone page (its canvas
runtime needs window.React before it boots) and it pulls WeatherIcon.dc.html
with fetch(), which CORS blocks under file://, so it is served over a
throwaway localhost server instead.
Screens are opened by patching the design component's initial
state = { ... } block (SCREENS at the top of the script), not by clicking —
deterministic, and it survives a re-layout of the controls that would open them.
If the design renames a state field the script fails loudly rather than
screenshotting the wrong screen. Simulator-only states (first-boot Wi-Fi setup,
OTA dialog, device info) have no counterpart in the design and are skipped.
Note the design fetches live weather from Open-Meteo on mount, so its numbers are the real forecast while the simulator shows fixtures — compare layout, not values.
The design's HTML original drew the temperature line and the precipitation bars on one plot with two independent y-scales. Two scales on one frame invent a correlation that isn't in the data, so this port keeps the same visual intent but stacks two panels sharing a single x-axis — each panel then carries one series against one scale. Bars and line points are placed from the same x coordinates, so the panels stay in register.
LVGL is not thread-safe. Rules:
- Everything touching LVGL runs under
bsp_display_lock()/bsp_display_unlock(), and nothing that can block (network I/O, cross-chip RPC, flash writes) runs while that lock is held — a synchronous coprocessor-version RPC once did, and briefly stalled the LVGL renderer on every render as a result. - No UI callback blocks. Each one posts a command to the
weatherworker task, which owns HTTP, TLS, JSON, Wi-Fi scanning/connecting, and dispatches OTA work to its own dedicated task rather than running it inline — a multi-MB download must not make search, refresh, or Wi-Fi unresponsive for its duration. - The camera's sampling task shuts itself down at a clean checkpoint between samples rather than being deleted from another task's context, so it never races a driver ioctl mid-transaction.
City-search keystrokes are debounced by 450 ms in the worker, so typing a name is one request rather than one TLS handshake per character.
- Builds clean with ESP-IDF v6.1 for
esp32p4— no warnings from any file inmain/. Image is ~2.0 MB, leaving about three-quarters of each 8 MBota_0/ota_1app slot free. - Runs on the physical board, end to end: on-screen Wi-Fi provisioning (scan → passphrase on the built-in keyboard → connect), credentials persisted to NVS, SNTP clock sync, and a live Open-Meteo fetch.
- A real over-the-air update has been completed on hardware: the device fetched a published GitHub Release, downloaded and flashed a newer P4 image over HTTPS, verified its SHA-256 against the manifest, and rebooted into it successfully.
- The ESP32-C6 coprocessor OTA has completed on hardware too: a full image streamed over SDIO, SHA-256 verified, activated, and the next boot negotiated matching host/coprocessor versions (3.0.7). A bug where a successful C6 update rolled back the P4 image that triggered it was found and fixed, and confirmed by a real GitHub Release update of both chips.
- Wi-Fi outage recovery was exercised with
hil-outage-test.sh(HIL_OUTAGE_OK, ~79 s recovery after a stuck connect attempt). - Host-run unit tests (
./scripts/host-test.sh) cover everycomponents/app_logic/module — forecast parsing, OTA manifest parsing, version comparison, favorites, the ambient-light policy, and the Wi-Fi/link/coprocessor/startup/network-status/heap policies. - Touch geometry verified by measurement — corner taps land within ~45 px of the true corners on a 1024×600 panel.
- All generated Inter faces are confirmed linked into the ELF.
Still unverified: the finer visual detail against a physical photo of the panel — specifically whether the generated Inter faces render the German/Spanish/French accented characters correctly on the real display — and a stall seen twice where the RPC/SDIO link to the C6 wedges, likely under heavy SDIO load such as an OTA download.
If taps ever land in the wrong place on a different unit, build with
CONFIG_WEATHER_TOUCH_DEBUG=y, tap the four corners, and read the reported
coordinates off the console rather than guessing at flag combinations.
This board's P4 is silicon revision v1.3, so sdkconfig.defaults uses the
vendor's rev1_3 profile. A rev3_x image is refused at flash time — do not pass
--force, rebuild with the right profile.
esp_hosted is pinned to 3.0.* and esp_wifi_remote to 1.6.*, which is not
what the Waveshare examples use. Their pins (1.4.* / ==1.2.5) double-start the
STA netif on IDF 6 and boot-loop on an lwip netif already added assert. If
you change these versions, delete sdkconfig — stale esp_hosted values persist,
including the SDIO reset polarity.
Host and C6 esp-hosted versions must match (host 3.0.*, slave firmware
3.0.7 — see ESP32-C6 firmware; binaries in scripts/c6_firmware_via_UART/binaries_v3.0.7). A C6 still
on older firmware (e.g. 2.6.7) logs major version mismatch — OTA coprocessor from host; update it via Firmware updates or, if the OTA
path itself is unusable on that firmware, flash it over UART with
scripts/c6_firmware_via_UART/flash_c6_via_uart.sh (background in
docs/esp-hosted-rpc-v1-vs-v2.md).
The C6 only forwards inbound UDP/TCP packets to the P4 when the destination
port is in 49152–61439; everything else is silently dropped (outbound
traffic, DHCP and ping are unaffected). With stock lwIP, about 80% of DNS
lookups and a quarter of TCP connections failed because of it, which broke
startup DNS/SNTP. The top-level CMakeLists.txt therefore pins lwIP's
local-port ranges (HOST_PORT_FIRST/HOST_PORT_COUNT) to that window. If the
C6 firmware's range ever changes, change them to match.
This P4's ECDSA hardware peripheral isn't available on rev 1.3 silicon, so
mbedTLS's PSA driver logs ECDSA peripheral not supported on this chip revision at ESP_LOGE on TLS handshakes that verify an ECDSA-signed
certificate. This is a graceful, automatic fallback to software ECDSA, not a
failure — the handshake completes right after.
The GT911 touch controller's interrupt line is only routed to a test point on
the PCB (net INT_TP: FPC connector J3 pin 4 and test point TP1 — no
microcontroller pin), confirmed from the schematic in
docs/hardware/ESP32-P4-WIFI6-Touch-LCD-7B-schematic.pdf. That's why touch
runs in polling mode (BSP_LCD_TOUCH_INT is GPIO_NUM_NC in the vendor BSP) —
interrupt-driven touch isn't possible on this board without a hardware rework.
The camera module has no power-down or reset pin wired to the P4
(esp_video_init_csi_config_t's pwdn_pin/reset_pin are both -1), so
app_light.c keeps the CSI stream running continuously while adaptive
brightness is on rather than duty-cycling it per sample — a per-sample
stop/start cycle was tried and hung this 2-buffer driver's DQBUF queue on
real hardware.
This project's own code (main/, components/, sim/, host_test/,
scripts/) is licensed under the Apache License 2.0 — see LICENSE.
It builds on ESP-IDF and a number of components pulled in through the IDF
Component Manager (main/idf_component.yml), each under its own license:
| Component | License |
|---|---|
| ESP-IDF (framework) | Apache-2.0 |
| FreeRTOS-Kernel (bundled in ESP-IDF) | MIT |
| mbedTLS (bundled in ESP-IDF) | Apache-2.0 OR GPL-2.0-or-later (dual) |
| lwIP (bundled in ESP-IDF) | BSD-3-Clause-style |
Unity (bundled in ESP-IDF, used by host_test/) |
MIT |
waveshare/esp32_p4_wifi6_touch_lcd_7b (board BSP) |
Apache-2.0 |
lvgl/lvgl |
MIT |
espressif/esp_wifi_remote |
Apache-2.0 |
espressif/esp_hosted |
Apache-2.0, with common/eh_common and common/eh_tlv dual-licensed GPL-2.0-only OR Apache-2.0 (electable), and a bundled BSD-3-Clause protobuf-c |
espressif/cjson |
MIT |
espressif/esp_video |
Espressif MIT |
espressif/esp_cam_sensor |
Apache-2.0 |
espressif/esp_ipa |
Espressif MIT |
espressif/esp_sccb_intf, esp_lvgl_adapter, esp_lcd_touch, esp_lcd_touch_gt911, esp_lcd_ek79007, esp_lv_fs, esp_lv_decoder, esp_mmap_assets, button, knob, cmake_utilities, eppp_link, esp_serial_slave_link, esp_codec_dev, wifi_remote_over_eppp |
Apache-2.0 |
espressif/esp_new_jpeg |
Espressif MIT |
espressif/freetype, libpng, zlib, usb, usb_host_uvc |
Present in the resolved dependency tree (transitive, optional decoder paths) but not required by this project's main/CMakeLists.txt and not compiled into the firmware |
The Inter typeface used to generate main/fonts/inter_*.c
(tools/gen_fonts.sh) is licensed under the SIL Open Font License 1.1.
Fallback glyphs come from LVGL's built-in Montserrat font (MIT).
This is separate from the code licenses above: it covers the data the
firmware fetches at runtime from Open-Meteo, not
any software. Open-Meteo's forecast and air-quality data is
licensed under CC BY 4.0 — free for
non-commercial use, provided the source is attributed, which is why the
Device Info dialog carries an on-device attribution note
(Using Data from Open-Meteo.com and OpenAQ.org..., main/app_weather.c).
This firmware only ever calls Open-Meteo's free, non-commercial,
attribution-based endpoints (api.open-meteo.com,
geocoding-api.open-meteo.com, air-quality-api.open-meteo.com) — no API
key is configured or required for this use.
Commercial use is not covered by CC BY 4.0. If you deploy this firmware (or fork it) as part of a commercial product or service, Open-Meteo requires a separate paid API plan and a commercial API key — see open-meteo.com/en/pricing — and you are responsible for obtaining one and updating the API host/key accordingly; this project does not include or arrange one.



