Firmware: OTA client, dual-board build (devkit/XIAO), version reporting, XIAO fixes
Build and push server image / build-and-push (push) Successful in 36s
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:
@@ -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__/
|
||||
|
||||
@@ -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
@@ -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`)
|
||||
|
||||
|
||||
Executable
+75
@@ -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[@]}"
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
@@ -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.
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
Reference in New Issue
Block a user