Firmware: OTA client, dual-board build (devkit/XIAO), version reporting, XIAO fixes
Build and push server image / build-and-push (push) Successful in 36s

- version.txt + esp_app_desc_t version reporting (X-Frame-Version header);
  new ota_update.c checks the server's advertised version against the
  running one and streams+applies an update via esp_https_ota, gated by
  bootloader rollback (marks the image valid only after a full successful
  cycle, so a bad update can't brick a wall-mounted frame).
- Dual-OTA partition tables: partitions.csv (8MB dev board, 2MB slots) and
  new partitions_xiao.csv (4MB XIAO, 1.875MB slots -- the dev board's
  table doesn't fit the XIAO's flash). New build_for_board.sh gives each
  board its own build dir + generated sdkconfig via SDKCONFIG_DEFAULTS
  layering, so switching boards never clobbers the other's config.
- fetch_photo_info()/fetch_face_labels() were using the short
  reachability-check timeout even though the manage-menu path can be the
  first (cold, TLS-handshake-paying) request of a wake cycle -- switched
  to the longer fetch timeout to stop spurious ESP_ERR_HTTP_CONNECT
  failures.
- XIAO: the RF switch that selects onboard vs. external antenna
  (GPIO3/14) isn't initialized by plain ESP-IDF the way Seeed's Arduino
  package does it, leaving WiFi unable to reliably reach the antenna at
  all -- new board_antenna.c powers the switch and selects the onboard
  antenna, gated behind FRAME_XIAO_ANTENNA_INIT (on by default in
  sdkconfig.xiao). Also remaps the EPD DC/RST/BUSY pins, since the dev
  board's defaults (GPIO9/10/11) aren't physically exposed on the XIAO.
This commit is contained in:
2026-07-20 22:24:15 -04:00
parent f7eaadcbed
commit a3ab6c5f13
17 changed files with 402 additions and 15 deletions
+6
View File
@@ -4,6 +4,12 @@ firmware/sdkconfig
firmware/sdkconfig.old
firmware/managed_components/
firmware/dependencies.lock
# build_for_board.sh's per-board build dir + generated sdkconfig
# (sdkconfig.xiao itself, the committed override fragment, is NOT
# ignored -- only the *_local generated file and its build output).
firmware/build_xiao/
firmware/sdkconfig.xiao_local
firmware/sdkconfig.xiao_local.old
# Python server
server/__pycache__/
+42
View File
@@ -58,6 +58,48 @@ actions where instant, unambiguous response matters most (next, back),
and everything else folded onto the third pin via hold duration instead
of needing its own pin.
### Wiring on the Seeed XIAO ESP32-C6
The XIAO only breaks out 11 GPIOs (0, 1, 2, 16, 17, 18, 19, 20, 21, 22,
23), so the dev-board defaults above don't fit -- DC/RST/BUSY (GPIO 9,
10, 11) aren't exposed on this board at all. Built with
[`build_for_board.sh xiao`](../firmware/README.md#building-for-the-seeed-xiao-esp32-c6-production-board),
which layers [`firmware/sdkconfig.xiao`](../firmware/sdkconfig.xiao) on
top of the dev-board defaults, remapping DC/RST/BUSY onto three of the
remaining free pins:
| Panel pin | XIAO GPIO | XIAO silkscreen label | Kconfig option |
| --------- | --------- | ---------------------- | --------------- |
| CLK | 20 | D9 | `EPD_PIN_CLK` (unchanged) |
| DIN | 19 | D8 | `EPD_PIN_MOSI` (unchanged) |
| CS | 18 | D10 | `EPD_PIN_CS` (unchanged) |
| DC | 16 | D6 | `EPD_PIN_DC` |
| RST | 17 | D7 | `EPD_PIN_RST` |
| BUSY | 21 | D3 | `EPD_PIN_BUSY` |
| VCC | 3.3V | 3V3 | -- |
| GND | GND | GND | -- |
The XIAO's silkscreen labels its pins D0-D10, not raw GPIO numbers --
the table above gives both.
Buttons (unchanged from the table above -- the XIAO's only three
ADC/deep-sleep-capable pins, GPIO 0/1/2, are exactly the ones already
used): next=GPIO2/**D2**, back=GPIO0/**D0**, menu/reset=GPIO1/**D1**.
That leaves GPIO22/**D4** and GPIO23/**D5** free, e.g. for the optional
VBUS mains-sense divider described under
[Battery](#battery-optional-xiao-esp32-c6) below.
`sdkconfig.xiao` also sets `FRAME_XIAO_ANTENNA_INIT=y`, which powers
the XIAO's onboard RF switch and selects its ceramic antenna at boot
(GPIO3/14, internal to the board -- not part of the wiring above).
Without it, WiFi doesn't reliably work on this board at all: the radio
comes up and logs look normal (e.g. the softAP starts and prints its
SSID/password) but the switch's control pins are left floating, so
nothing actually reaches the antenna -- the AP never becomes visible to
a scan, and a station connection would fail to associate the same way.
Seeed's own Arduino board package does this automatically; plain
ESP-IDF (what this firmware uses) doesn't, hence the explicit init.
A couple of things worth knowing if you pick different pins:
- **Avoid the ESP32-C6's strapping pins** (GPIO 4, 5, 8, 9, 15) and the
+25 -6
View File
@@ -22,12 +22,31 @@ idf.py -p PORT flash monitor
(`Ctrl-]` exits the monitor.)
The committed [`sdkconfig.defaults`](sdkconfig.defaults) pins an 8MB flash
size and a custom [`partitions.csv`](partitions.csv) (2MB app partition --
the default "single app" ~1MB partition runs out of room once the HTTP
client, TLS, and vendored fonts/QR library are linked in). If your board
has less flash, you'll need to shrink the app partition and drop features
to fit.
The commands above target the dev board this project is built against
(ESP32-C6-DevKitC-1, 8MB flash) -- the committed
[`sdkconfig.defaults`](sdkconfig.defaults) pins that flash size and a
custom [`partitions.csv`](partitions.csv) with dual OTA app partitions (2MB
each; see [OTA updates](#ota-updates) below), since the default "single
app" ~1MB partition runs out of room once the HTTP client, TLS, and
vendored fonts/QR library are linked in.
### Building for the Seeed XIAO ESP32-C6 (production board)
The XIAO has only 4MB of flash, which doesn't fit the dev board's two 2MB
OTA slots -- it needs its own partition table
([`partitions_xiao.csv`](partitions_xiao.csv), 1.875MB slots) and flash-size
setting. Rather than hand-editing `sdkconfig` back and forth between boards,
use [`build_for_board.sh`](build_for_board.sh), which builds each board into
its own directory with its own generated config, so switching back and forth
never clobbers the other:
```
./build_for_board.sh xiao build
./build_for_board.sh xiao flash monitor -p PORT
```
(`./build_for_board.sh devkit ...` does the same for the dev board --
equivalent to a plain `idf.py`, just consistent with the XIAO invocation.)
## Configuration (`idf.py menuconfig`)
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Builds/flashes for a specific board variant. This project targets two:
#
# devkit ESP32-C6-DevKitC-1 (8MB flash) -- the dev board. This is
# also the plain `idf.py` default (sdkconfig/build/), so this
# script's devkit mode is mostly for symmetry -- normal
# `idf.py build`/`flash` work fine too.
# xiao Seeed XIAO ESP32-C6 (4MB flash) -- the production board.
#
# The two need different partition tables (the XIAO's 4MB doesn't fit
# the dev board's two 2MB OTA app slots -- see partitions_xiao.csv,
# 1.875MB slots instead) and a different flash-size Kconfig. Rather
# than hand-editing the shared sdkconfig back and forth (fragile, easy
# to leave it in the wrong state for whichever board you flash next),
# each board gets its own build directory and its own generated
# sdkconfig, seeded from sdkconfig.defaults (shared) with the board's
# override file layered on top via ESP-IDF's own SDKCONFIG_DEFAULTS
# mechanism. Switching boards is just switching which one you invoke --
# neither ever touches the other's config or build output.
#
# Usage:
# ./build_for_board.sh xiao build
# ./build_for_board.sh xiao flash -p /dev/ttyUSB0
# ./build_for_board.sh xiao flash monitor -p /dev/ttyUSB0
# ./build_for_board.sh devkit build
#
# Defaults to "build" if no idf.py subcommand is given.
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$script_dir"
if [ $# -lt 1 ]; then
echo "Usage: $0 <devkit|xiao> [idf.py args...]" >&2
exit 1
fi
board="$1"
shift
case "$board" in
xiao)
build_dir="$script_dir/build_xiao"
sdkconfig_path="$script_dir/sdkconfig.xiao_local"
defaults="$script_dir/sdkconfig.defaults;$script_dir/sdkconfig.xiao"
;;
devkit)
build_dir="$script_dir/build"
sdkconfig_path="$script_dir/sdkconfig"
defaults="$script_dir/sdkconfig.defaults"
;;
*)
echo "Unknown board '$board' -- expected 'devkit' or 'xiao'" >&2
exit 1
;;
esac
args=("$@")
if [ ${#args[@]} -eq 0 ]; then
args=(build)
fi
# idf.py is normally a shell *function* (defined by ESP-IDF's
# activate/export script), not a real executable on PATH -- that
# function isn't inherited by this script's own subshell even if you
# sourced the activation script first. IDF_PATH (an exported env var,
# which *is* inherited) lets this work the same way regardless: call
# the underlying Python module directly.
if [ -z "${IDF_PATH:-}" ]; then
echo "IDF_PATH is not set -- source your ESP-IDF activation/export.sh first" >&2
exit 1
fi
echo "==> Board: $board (build dir: $(basename "$build_dir"), sdkconfig: $(basename "$sdkconfig_path"))"
exec python "$IDF_PATH/tools/idf.py" -B "$build_dir" -D "SDKCONFIG=$sdkconfig_path" -D "SDKCONFIG_DEFAULTS=$defaults" "${args[@]}"
+2 -2
View File
@@ -1,3 +1,3 @@
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c qr_onboarding.c status_screen.c epd_draw.c next_button.c back_button.c combo_button.c manage_qr_overlay.c battery.c
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_netif esp_http_server esp_http_client mbedtls dns_server epd7in3e qrcode epaper_fonts esp_driver_gpio esp_adc
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c qr_onboarding.c status_screen.c epd_draw.c next_button.c back_button.c combo_button.c manage_qr_overlay.c battery.c ota_update.c board_antenna.c
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_netif esp_http_server esp_http_client mbedtls dns_server epd7in3e qrcode epaper_fonts esp_driver_gpio esp_adc esp_https_ota app_update esp_app_format
EMBED_FILES root.html)
+18
View File
@@ -1,5 +1,23 @@
menu "ESPresso Frame Configuration"
config FRAME_XIAO_ANTENNA_INIT
bool "Select onboard antenna on Seeed XIAO ESP32-C6 (RF switch init)"
default n
help
The XIAO ESP32-C6 routes its antenna through an FM8625H RF
switch (GPIO3 = switch power enable, GPIO14 = antenna
select) so it can optionally use an external u.FL antenna
instead of the onboard ceramic one. Seeed's own Arduino
board package powers the switch and selects the onboard
antenna automatically at boot; plain ESP-IDF (what this
firmware uses) does not -- without this, the switch's
GPIOs are left floating and the radio may not reliably
reach the onboard antenna at all. Symptom: a softAP (or
STA connection) that starts successfully in the log but
is never actually visible/reachable over the air. Enabled
by default in sdkconfig.xiao; leave off for the DevKitC-1
dev board, which has no such switch.
config ESP_AP_SSID
string "Provisioning softAP SSID prefix"
default "ESPRESSO"
+34
View File
@@ -0,0 +1,34 @@
#include "board_antenna.h"
#if CONFIG_FRAME_XIAO_ANTENNA_INIT
#include "driver/gpio.h"
// Fixed by the XIAO ESP32-C6's own PCB (the RF switch wiring, not
// user-configurable), not exposed as Kconfig pins the way the EPD/button
// GPIOs are. GPIO3 is the switch's power enable (active low), GPIO14
// selects onboard (low) vs external u.FL (high) antenna. Seeed's own
// Arduino board package does this at boot automatically; plain ESP-IDF
// does not, so without it the switch is left unpowered/floating and the
// radio doesn't reliably reach the onboard antenna.
#define ANTENNA_SWITCH_POWER_GPIO 3
#define ANTENNA_SELECT_GPIO 14
void board_antenna_select_onboard(void)
{
gpio_config_t cfg = {
.pin_bit_mask = (1ULL << ANTENNA_SWITCH_POWER_GPIO) | (1ULL << ANTENNA_SELECT_GPIO),
.mode = GPIO_MODE_OUTPUT,
};
gpio_config(&cfg);
gpio_set_level(ANTENNA_SWITCH_POWER_GPIO, 0);
gpio_set_level(ANTENNA_SELECT_GPIO, 0);
}
#else
void board_antenna_select_onboard(void)
{
}
#endif
+8
View File
@@ -0,0 +1,8 @@
#pragma once
// Powers the Seeed XIAO ESP32-C6's FM8625H RF switch and selects the
// onboard ceramic antenna (as opposed to the external u.FL antenna the
// switch can otherwise route to). No-op unless CONFIG_FRAME_XIAO_ANTENNA_INIT
// is set -- the DevKitC-1 dev board has no such switch. Call once before
// esp_wifi_start(), in both the softAP and STA bring-up paths.
void board_antenna_select_onboard(void);
+39 -3
View File
@@ -1,6 +1,8 @@
#include <string.h>
#include "esp_app_desc.h"
#include "esp_event.h"
#include "esp_ota_ops.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_wifi_default.h"
@@ -16,6 +18,8 @@
#include "status_screen.h"
#include "manage_qr_overlay.h"
#include "combo_button.h"
#include "ota_update.h"
#include "board_antenna.h"
#include "frame_client.h"
@@ -80,6 +84,8 @@ static void sta_event_handler(void *arg, esp_event_base_t event_base,
esp_err_t frame_wifi_connect_sta(const frame_config_t *cfg)
{
board_antenna_select_onboard();
s_sta_event_group = xEventGroupCreate();
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
@@ -141,6 +147,7 @@ esp_err_t frame_wifi_connect_sta(const frame_config_t *cfg)
typedef struct {
bool reachable;
uint32_t refresh_interval_s; /* CONFIG_FRAME_SLEEP_INTERVAL_S if absent/unparseable */
char firmware_version[32]; /* server's uploaded OTA image version; empty if none/unreachable */
} frame_server_config_t;
/* Finds the first integer value associated with "key" in a small JSON
@@ -216,13 +223,17 @@ static bool json_extract_string(const char *json, const char *key, char *out, si
/* GETs the server's /frame/config -- doubles as both the reachability
* check (any completed HTTP response means the socket-level connection
* succeeded) and the source of the server-configurable refresh interval. */
* succeeded), the source of the server-configurable refresh interval,
* and (via the X-Frame-Version request header / firmware_version
* response field) the device's OTA update check -- piggybacked on a
* request already made every wake, no extra round trip. */
static frame_server_config_t fetch_frame_config(const frame_config_t *cfg)
{
frame_server_config_t result = {
.reachable = false,
.refresh_interval_s = CONFIG_FRAME_SLEEP_INTERVAL_S,
};
result.firmware_version[0] = '\0';
char url[256];
build_url(url, sizeof(url), cfg, "frame/config");
@@ -234,6 +245,7 @@ static frame_server_config_t fetch_frame_config(const frame_config_t *cfg)
.crt_bundle_attach = esp_crt_bundle_attach,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
esp_http_client_set_header(client, "X-Frame-Version", esp_app_get_description()->version);
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
@@ -264,6 +276,7 @@ static frame_server_config_t fetch_frame_config(const frame_config_t *cfg)
ESP_LOGW(TAG, "'%s' response missing refresh_interval_s, using fallback %ds", url,
(int)result.refresh_interval_s);
}
json_extract_string(body, "firmware_version", result.firmware_version, sizeof(result.firmware_version));
return result;
}
@@ -288,10 +301,20 @@ static void fetch_photo_info(const frame_config_t *cfg, char *location_line1, si
char url[256];
build_url(url, sizeof(url), cfg, "frame/photo-info");
/* CONFIG_FRAME_FETCH_TIMEOUT_MS, not the shorter SERVER_CHECK one:
* unlike fetch_frame_config() (always called after the image fetch
* has already warmed the connection, see frame_client_run()), this
* is the *first* network call of the wake cycle whenever the manage
* menu is opened -- same cold-connection latency spike that made
* the short timeout unreliable for /frame/config before, now worse
* with a real TLS handshake on top. Confirmed on hardware: this
* timed out under CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS while the
* rest of the cycle (a fresh connection, but not the *first* one)
* succeeded fine. */
esp_http_client_config_t config = {
.url = url,
.method = HTTP_METHOD_GET,
.timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS,
.timeout_ms = CONFIG_FRAME_FETCH_TIMEOUT_MS,
.crt_bundle_attach = esp_crt_bundle_attach,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
@@ -348,10 +371,12 @@ static int fetch_face_labels(const frame_config_t *cfg, manage_face_label_t *out
char url[256];
build_url(url, sizeof(url), cfg, "frame/face-labels");
/* Same reasoning as fetch_photo_info() -- this is a manage-menu
* request too, not a warmed-connection reachability check. */
esp_http_client_config_t config = {
.url = url,
.method = HTTP_METHOD_GET,
.timeout_ms = CONFIG_FRAME_SERVER_CHECK_TIMEOUT_MS,
.timeout_ms = CONFIG_FRAME_FETCH_TIMEOUT_MS,
.crt_bundle_attach = esp_crt_bundle_attach,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
@@ -786,9 +811,20 @@ void frame_client_run(const frame_config_t *cfg, fetch_action_t action, bool sho
* just be discarded. */
uint32_t sleep_seconds = CONFIG_FRAME_RETRY_INTERVAL_S;
if (image_ok) {
/* A full fetch/display cycle just succeeded -- exactly the proof
* of life needed to confirm a freshly-OTA'd image is good.
* No-op if this image was already marked valid (i.e. every
* normal boot, not just the one right after an update). */
esp_ota_mark_app_valid_cancel_rollback();
report_battery(cfg, battery_percent);
frame_server_config_t server_cfg = fetch_frame_config(cfg);
sleep_seconds = server_cfg.reachable ? server_cfg.refresh_interval_s : CONFIG_FRAME_RETRY_INTERVAL_S;
/* Last, deliberately -- the photo's already on screen and the
* battery report already sent, so a reboot here (whether OTA
* succeeds or the device is mid-update) never loses either. */
ota_update_if_available(cfg, server_cfg.firmware_version);
}
if (have_display) {
+74
View File
@@ -0,0 +1,74 @@
#include <string.h>
#include "esp_app_desc.h"
#include "esp_crt_bundle.h"
#include "esp_http_client.h"
#include "esp_https_ota.h"
#include "esp_log.h"
#include "esp_system.h"
#include "ota_update.h"
static const char *TAG = "ota_update";
/* Generous: the image is ~1.2MB and may stream over a WAN reverse
* proxy; esp_https_ota resets this per-read, so it's an inactivity
* timeout, not a total-transfer cap. */
#define OTA_HTTP_TIMEOUT_MS 30000
/* Built the same way as every other tools-server URL -- scheme/cert/
* token handling all come from build_url()'s conventions. Duplicated
* tiny helper rather than exporting frame_client.c's static build_url();
* kept byte-identical in behavior (see frame_client.c). */
static void build_ota_url(char *out, size_t out_size, const frame_config_t *cfg)
{
const char *toolsserver = cfg->toolsserver;
size_t len;
if (strncmp(toolsserver, "http://", 7) == 0 || strncmp(toolsserver, "https://", 8) == 0) {
len = (size_t)snprintf(out, out_size, "%s/frame/firmware", toolsserver);
} else {
len = (size_t)snprintf(out, out_size, "http://%s/frame/firmware", toolsserver);
}
if (cfg->access_token[0] != '\0' && len < out_size) {
snprintf(out + len, out_size - len, "?token=%s", cfg->access_token);
}
}
void ota_update_if_available(const frame_config_t *cfg, const char *server_version)
{
if (server_version == NULL || server_version[0] == '\0') {
return; /* server has no uploaded firmware */
}
const char *running = esp_app_get_description()->version;
if (strcmp(server_version, running) == 0) {
return; /* already running what the server has */
}
ESP_LOGI(TAG, "Firmware update available: running '%s', server has '%s' -- starting OTA", running,
server_version);
char url[256];
build_ota_url(url, sizeof(url), cfg);
esp_http_client_config_t http_config = {
.url = url,
.timeout_ms = OTA_HTTP_TIMEOUT_MS,
.crt_bundle_attach = esp_crt_bundle_attach,
.keep_alive_enable = true,
};
esp_https_ota_config_t ota_config = {
.http_config = &http_config,
};
esp_err_t err = esp_https_ota(&ota_config);
if (err != ESP_OK) {
/* Not fatal -- the photo already displayed this cycle; we just
* try again on a future wake. */
ESP_LOGW(TAG, "OTA failed (%s), will retry on a later wake", esp_err_to_name(err));
return;
}
ESP_LOGI(TAG, "OTA complete, restarting into '%s'", server_version);
esp_restart();
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "wifi_provisioning.h"
/**
* If server_version is non-empty and differs from the running firmware's
* embedded version (esp_app_desc), streams the server's uploaded image
* (GET /frame/firmware) into the idle OTA slot via esp_https_ota and
* restarts into it -- so this only returns when there's nothing to do or
* the update failed (logged, retried on a later wake). Plain string
* inequality, deliberately: uploading an older image downgrades, which
* is a feature.
*
* Rollback safety is handled elsewhere: the freshly-booted image is
* marked valid in frame_client_run() only after a full successful
* fetch/display cycle (CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) -- until
* then, any reboot reverts to the previous slot.
*/
void ota_update_if_available(const frame_config_t *cfg, const char *server_version);
+3
View File
@@ -22,6 +22,7 @@
#include "epd7in3e.h"
#include "qr_onboarding.h"
#include "wifi_provisioning.h"
#include "board_antenna.h"
#define NVS_NAMESPACE "frame_cfg"
@@ -408,6 +409,8 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base,
static void wifi_init_softap(const char *ap_ssid, const char *ap_password)
{
board_antenna_select_onboard();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
+12 -4
View File
@@ -1,4 +1,12 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 0x200000,
# Name, Type, SubType, Offset, Size, Flags
# nvs/phy_init/ota_0 keep the exact offsets of the pre-OTA layout (nvs
# 0x9000, phy 0xf000, app 0x10000 -- ota_0 replaces the old "factory"
# slot in place), so migrating an existing device to this table via a
# one-time USB `idf.py flash` preserves its provisioning/NVS state.
# otadata/ota_1 land in previously-unused flash. App slots must be 64KB
# (0x10000) aligned, hence ota_1 at 0x220000 rather than 0x212000.
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
ota_0, app, ota_0, 0x10000, 0x200000,
otadata, data, ota, 0x210000, 0x2000,
ota_1, app, ota_1, 0x220000, 0x200000,
Can't render this file because it contains an unexpected character in line 3 and column 61.
+12
View File
@@ -0,0 +1,12 @@
# Name, Type, SubType, Offset, Size, Flags
# Same OTA layout as partitions.csv, resized for the Seeed XIAO
# ESP32-C6's 4MB flash (the dev board's 8MB table's 2x 2MB app slots
# don't fit). App slots are 0x1e0000 (1.875MB) each -- current firmware
# runs at ~1.2MB, so this leaves ~38% headroom per slot, same margin as
# the 8MB table's 2MB slots give today. nvs/phy offsets match the 8MB
# table so provisioning data is byte-compatible between the two.
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
otadata, data, ota, 0x10000, 0x2000,
ota_0, app, ota_0, 0x20000, 0x1e0000,
ota_1, app, ota_1, 0x200000, 0x1e0000,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Same OTA layout as partitions.csv, resized for the Seeed XIAO
3 # ESP32-C6's 4MB flash (the dev board's 8MB table's 2x 2MB app slots
4 # don't fit). App slots are 0x1e0000 (1.875MB) each -- current firmware
5 # runs at ~1.2MB, so this leaves ~38% headroom per slot, same margin as
6 # the 8MB table's 2MB slots give today. nvs/phy offsets match the 8MB
7 # table so provisioning data is byte-compatible between the two.
8 nvs, data, nvs, 0x9000, 0x6000,
9 phy_init, data, phy, 0xf000, 0x1000,
10 otadata, data, ota, 0x10000, 0x2000,
11 ota_0, app, ota_0, 0x20000, 0x1e0000,
12 ota_1, app, ota_1, 0x200000, 0x1e0000,
+8
View File
@@ -23,6 +23,14 @@ CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
# OTA rollback safety: a freshly-OTA'd image boots "pending verify" and
# must call esp_ota_mark_app_valid_cancel_rollback() (done in
# frame_client.c after one full successful fetch/display cycle -- the
# right proof of life for this device) or the bootloader reverts to the
# previous slot on the next reboot. A bad OTA can't brick a wall-mounted
# frame.
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
# The whole boot flow (provisioning, QR rendering, and eventually the HTTP
# fetch cycle) runs in the single default "main" task. The 3584-byte
# default already crashed with a stack protection fault once (a 4KB SPI
+24
View File
@@ -0,0 +1,24 @@
# Board-specific overrides for the Seeed XIAO ESP32-C6, layered on top
# of sdkconfig.defaults (which targets the 8MB ESP32-C6-DevKitC-1 dev
# board) via SDKCONFIG_DEFAULTS -- see build_for_board.sh, which is the
# supported way to build with this file. Don't set this via a plain
# `idf.py menuconfig` on the default build; that writes straight into
# the shared sdkconfig, not this file.
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_xiao.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions_xiao.csv"
# Powers the XIAO's RF switch and selects its onboard antenna -- without
# this the softAP/STA radio doesn't reliably reach the antenna at all.
# See the Kconfig help text (FRAME_XIAO_ANTENNA_INIT) for why.
CONFIG_FRAME_XIAO_ANTENNA_INIT=y
# EPD wiring: the dev-board defaults (DC=9, RST=10, BUSY=11) aren't
# physically exposed on the XIAO -- only GPIO 0,1,2,16,17,18,19,20,21,
# 22,23 are broken out, and 18/19/20 are already the panel's SPI pins
# (CS/MOSI/CLK) while 0/1/2 are the buttons. Remapped to three of the
# remaining free pins; see docs/hardware.md for the full XIAO wiring
# table.
CONFIG_EPD_PIN_DC=16
CONFIG_EPD_PIN_RST=17
CONFIG_EPD_PIN_BUSY=21
+1
View File
@@ -0,0 +1 @@
1.0.0