Automatic updates: a per-frame channel (off / stable / latest), stable by default with a 24 h stability window - #457
Automatic updates: a per-frame channel (off / stable / latest), stable by default with a 24 h stability window#457mariusandra wants to merge 10 commits into
Conversation
ESP32 firmware sizeBuilt from ea47bd1 by this run; compared with the latest release v2026.9.11.
Breakdown by subsystem —
|
| Subsystem | This PR | v2026.9.11 | Δ |
|---|---|---|---|
| FrameOS core (Nim) | 404,278 | 404,262 | +16 (+0.0%) |
| QuickJS | 356,328 | 356,328 | ±0 |
| FrameOS apps (Nim) | 317,919 | 317,919 | ±0 |
| pixie | 301,065 | 301,065 | ±0 |
| Wi-Fi stack | 295,300 | 295,300 | ±0 |
| Nim stdlib | 248,937 | 248,937 | ±0 |
| ESP-IDF misc | 178,045 | 178,045 | ±0 |
| Embedded font | 146,358 | 146,358 | ±0 |
| mbedTLS + certificates | 132,539 | 132,539 | ±0 |
| String pool (attributed to efuse) | 132,195 | 131,977 | +218 (+0.2%) |
| lwIP / HTTP / WebSocket | 126,594 | 126,594 | ±0 |
| fos_* firmware shell (C) | 116,957 | 115,920 | +1,037 (+0.9%) |
| libc / libm / newlib | 116,259 | 116,259 | ±0 |
| Storage (SPIFFS / FatFS / SD / NVS) | 94,136 | 94,136 | ±0 |
| Nim packages (chrono, zippy, chroma, qrgen, ...) | 72,031 | 72,031 | ±0 |
| Display drivers (C) | 63,416 | 63,416 | ±0 |
| Crypto (monocypher) | 35,149 | 35,149 | ±0 |
| Total mapped flash | 3,137,506 | 3,136,235 | +1,271 (+0.0%) |
Inside each subsystem
FrameOS core (Nim) — 404,278
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
frameos/js_runtime |
102,114 | 102,114 | ±0 |
frameos/interpreter.nim |
97,352 | 97,352 | ±0 |
frameos/utils |
89,574 | 89,574 | ±0 |
embedded_main.nim |
18,347 | 18,347 | ±0 |
lib/tz.nim |
16,875 | 16,875 | ±0 |
embedded_runtime.nim |
15,056 | 15,056 | ±0 |
frameos/planner.nim |
9,605 | 9,605 | ±0 |
frameos_nim_glue.c.obj |
9,569 | 9,569 | ±0 |
frameos/types.nim |
7,909 | 7,893 | +16 (+0.2%) |
frameos/values.nim |
6,901 | 6,901 | ±0 |
frameos/apps.nim |
6,046 | 6,046 | ±0 |
frameos/spool.nim |
5,678 | 5,678 | ±0 |
embedded_scene.nim |
4,945 | 4,945 | ±0 |
frameos/app_config.nim |
2,871 | 2,871 | ±0 |
frameos/reboot_reason.nim |
2,628 | 2,628 | ±0 |
frameos/node_config.nim |
2,590 | 2,590 | ±0 |
frameos/app_capabilities.nim |
2,493 | 2,493 | ±0 |
fos_netguard.c.obj |
2,092 | 2,092 | ±0 |
frameos/runtime_diagnostics.nim |
763 | 763 | ±0 |
fos_version.c.obj |
324 | 324 | ±0 |
frameos/cloud |
255 | 255 | ±0 |
frameos/ids.nim |
133 | 133 | ±0 |
frameos/channels.nim |
118 | 118 | ±0 |
frameos/hal |
40 | 40 | ±0 |
QuickJS — 356,328
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
quickjs.c.obj |
283,813 | 283,813 | ±0 |
libunicode.c.obj |
50,174 | 50,174 | ±0 |
libregexp.c.obj |
13,995 | 13,995 | ±0 |
dtoa.c.obj |
5,681 | 5,681 | ±0 |
cutils.c.obj |
2,141 | 2,141 | ±0 |
fos_qjs_glue.c.obj |
449 | 449 | ±0 |
fos_quickjs_tz.c.obj |
75 | 75 | ±0 |
FrameOS apps (Nim) — 317,919
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
apps/data/icalJson |
50,265 | 50,265 | ±0 |
apps/render/calendar |
30,666 | 30,666 | ±0 |
apps/render/chart |
21,541 | 21,541 | ±0 |
apps/data/immich |
17,538 | 17,538 | ±0 |
apps/data/wikicommons |
17,114 | 17,114 | ±0 |
apps/data/openaiImage |
13,926 | 13,926 | ±0 |
apps/data/unsplash |
11,056 | 11,056 | ±0 |
apps/data/beRecycle |
10,749 | 10,749 | ±0 |
apps/data/weather |
10,210 | 10,210 | ±0 |
apps/render/split |
9,648 | 9,648 | ±0 |
apps/data/localImage |
9,190 | 9,190 | ±0 |
apps/data/eventsToAgenda |
9,088 | 9,088 | ±0 |
apps/data/googlePhotos |
9,068 | 9,068 | ±0 |
apps/apps.nim |
8,636 | 8,636 | ±0 |
apps/render/zoomPan |
8,552 | 8,552 | ±0 |
apps/data/openaiText |
7,353 | 7,353 | ±0 |
apps/data/haSensor |
7,291 | 7,291 | ±0 |
apps/data/xmlToJson |
5,420 | 5,420 | ±0 |
apps/render/text |
5,415 | 5,415 | ±0 |
apps/render/svg |
5,176 | 5,176 | ±0 |
apps/render/image |
4,510 | 4,510 | ±0 |
apps/data/rotateImage |
3,457 | 3,457 | ±0 |
apps/logic/setAsState |
3,441 | 3,441 | ±0 |
apps/data/downloadImage |
3,333 | 3,333 | ±0 |
apps/render/gradient |
3,173 | 3,173 | ±0 |
apps/data/qr |
3,114 | 3,114 | ±0 |
apps/data/downloadUrl |
3,065 | 3,065 | ±0 |
apps/data/frameOSGallery |
3,006 | 3,006 | ±0 |
apps/render/color |
2,804 | 2,804 | ±0 |
apps/data/newImage |
2,636 | 2,636 | ±0 |
apps/data/resizeImage |
2,534 | 2,534 | ±0 |
apps/data/log |
2,283 | 2,283 | ±0 |
apps/render/opacity |
2,272 | 2,272 | ±0 |
apps/data/prettyJson |
2,213 | 2,213 | ±0 |
apps/data/parseJson |
1,844 | 1,844 | ±0 |
apps/data/clock |
1,839 | 1,839 | ±0 |
apps/logic/nextSleepDuration |
1,639 | 1,639 | ±0 |
apps/logic/ifElse |
1,476 | 1,476 | ±0 |
apps/logic/breakIfRendering |
1,378 | 1,378 | ±0 |
pixie — 301,065
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
pixie/fontformats/opentype.nim |
55,507 | 55,507 | ±0 |
pixie/fileformats/webp.nim |
37,525 | 37,525 | ±0 |
pixie/fileformats/svg.nim |
37,203 | 37,203 | ±0 |
pixie/paths.nim |
32,570 | 32,570 | ±0 |
pixie/fileformats/jpeg.nim |
25,338 | 25,338 | ±0 |
pixie/fileformats/png.nim |
22,849 | 22,849 | ±0 |
pixie/images.nim |
15,787 | 15,787 | ±0 |
pixie/fonts.nim |
11,019 | 11,019 | ±0 |
pixie/paints.nim |
9,889 | 9,889 | ±0 |
pixie/fileformats/bmp.nim |
8,234 | 8,234 | ±0 |
pixie/common.nim |
7,416 | 7,416 | ±0 |
pixie/blends.nim |
6,540 | 6,540 | ±0 |
pixie/fileformats/gif.nim |
5,929 | 5,929 | ±0 |
pixie/inflatestream.nim |
5,325 | 5,325 | ±0 |
pixie.nim |
5,248 | 5,248 | ±0 |
pixie/fileformats/webp_vp8_tables.nim |
3,881 | 3,881 | ±0 |
pixie/fileformats/ppm.nim |
3,681 | 3,681 | ±0 |
pixie/contexts.nim |
3,038 | 3,038 | ±0 |
pixie/fileformats/qoi.nim |
2,686 | 2,686 | ±0 |
pixie/internal.nim |
754 | 754 | ±0 |
pixie/rgb565.nim |
381 | 381 | ±0 |
pixie/fontformats/svgfont.nim |
215 | 215 | ±0 |
pixie/decodebudget.nim |
50 | 50 | ±0 |
Wi-Fi stack — 295,300
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
libnet80211.a |
136,168 | 136,168 | ±0 |
libpp.a |
62,146 | 62,146 | ±0 |
libwpa_supplicant.a |
54,900 | 54,900 | ±0 |
libphy.a |
34,176 | 34,176 | ±0 |
libesp_wifi.a |
4,788 | 4,788 | ±0 |
libesp_phy.a |
2,541 | 2,541 | ±0 |
libesp_coex.a |
291 | 291 | ±0 |
libcore.a |
287 | 287 | ±0 |
libespnow.a |
3 | 3 | ±0 |
Nim stdlib — 248,937
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
pure/collections/tables.nim |
70,556 | 70,556 | ±0 |
system.nim |
46,665 | 46,665 | ±0 |
pure/unicode.nim |
17,405 | 17,405 | ±0 |
pure/times.nim |
16,984 | 16,984 | ±0 |
pure/json.nim |
13,414 | 13,414 | ±0 |
std/private/dragonbox.nim |
10,351 | 10,351 | ±0 |
pure/strutils.nim |
8,473 | 8,473 | ±0 |
pure/parsexml.nim |
5,975 | 5,975 | ±0 |
pure/algorithm.nim |
5,368 | 5,368 | ±0 |
pure/base64.nim |
5,302 | 5,302 | ±0 |
pure/hashes.nim |
4,114 | 4,114 | ±0 |
pure/xmlparser.nim |
3,567 | 3,567 | ±0 |
pure/parsejson.nim |
3,553 | 3,553 | ±0 |
std/formatfloat.nim |
3,508 | 3,508 | ±0 |
pure/uri.nim |
3,225 | 3,225 | ±0 |
pure/collections/sets.nim |
3,211 | 3,211 | ±0 |
pure/streams.nim |
2,820 | 2,820 | ±0 |
pure/strformat.nim |
2,641 | 2,641 | ±0 |
std/syncio.nim |
2,355 | 2,355 | ±0 |
pure/xmltree.nim |
2,051 | 2,051 | ±0 |
pure/lexbase.nim |
1,805 | 1,805 | ±0 |
pure/parseutils.nim |
1,757 | 1,757 | ±0 |
system/exceptions.nim |
1,598 | 1,598 | ±0 |
pure/strtabs.nim |
1,568 | 1,568 | ±0 |
pure/pathnorm.nim |
1,332 | 1,332 | ±0 |
pure/options.nim |
1,118 | 1,118 | ±0 |
std/private/ospaths2.nim |
1,032 | 1,032 | ±0 |
pure/collections/lists.nim |
983 | 983 | ±0 |
system/dollars.nim |
891 | 891 | ±0 |
pure/random.nim |
835 | 835 | ±0 |
std/private/digitsutils.nim |
651 | 651 | ±0 |
pure/math.nim |
650 | 650 | ±0 |
std/oserrors.nim |
637 | 637 | ±0 |
std/private/osdirs.nim |
580 | 580 | ±0 |
std/monotimes.nim |
401 | 401 | ±0 |
std/private/oscommon.nim |
329 | 329 | ±0 |
std/envvars.nim |
250 | 250 | ±0 |
std/private/decode_helpers.nim |
240 | 240 | ±0 |
std/private/ossymlinks.nim |
187 | 187 | ±0 |
pure/bitops.nim |
162 | 162 | ±0 |
pure/os.nim |
99 | 99 | ±0 |
std/private/osfiles.nim |
96 | 96 | ±0 |
std/assertions.nim |
87 | 87 | ±0 |
core/macros.nim |
69 | 69 | ±0 |
system/iterators.nim |
33 | 33 | ±0 |
system/ctypes.nim |
9 | 9 | ±0 |
ESP-IDF misc — 178,045
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
libesp_hw_support.a |
37,650 | 37,650 | ±0 |
libhal.a |
21,464 | 21,464 | ±0 |
libfreertos.a |
17,194 | 17,194 | ±0 |
libesp_system.a |
13,780 | 13,780 | ±0 |
libesp_driver_uart.a |
11,627 | 11,627 | ±0 |
libheap.a |
8,656 | 8,656 | ±0 |
libesp_driver_i2c.a |
8,062 | 8,062 | ±0 |
libesp_driver_spi.a |
7,756 | 7,756 | ±0 |
libconsole.a |
5,733 | 5,733 | ±0 |
libjson.a |
4,940 | 4,940 | ±0 |
libesp_driver_gpio.a |
3,890 | 3,890 | ±0 |
libbootloader_support.a |
3,813 | 3,813 | ±0 |
libxtensa.a |
3,691 | 3,691 | ±0 |
libesp_driver_usb_serial_jtag.a |
3,408 | 3,408 | ±0 |
libesp_ringbuf.a |
3,322 | 3,322 | ±0 |
libesp_event.a |
3,241 | 3,241 | ±0 |
libesp_mm.a |
2,451 | 2,451 | ±0 |
libapp_update.a |
2,422 | 2,422 | ±0 |
libesp_psram.a |
2,329 | 2,329 | ±0 |
libesp_timer.a |
2,326 | 2,326 | ±0 |
libsoc.a |
2,315 | 2,315 | ±0 |
libesp_adc.a |
2,289 | 2,289 | ±0 |
liblog.a |
1,375 | 1,375 | ±0 |
libpthread.a |
1,365 | 1,365 | ±0 |
libesp_security.a |
1,050 | 1,050 | ±0 |
libesp_rom.a |
902 | 902 | ±0 |
libesp_app_format.a |
507 | 507 | ±0 |
libxt_hal.a |
437 | 437 | ±0 |
libesp_common.a |
26 | 26 | ±0 |
libesp_pm.a |
24 | 24 | ±0 |
Embedded font — 146,358
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
assets/fonts.nim |
146,358 | 146,358 | ±0 |
mbedTLS + certificates — 132,539
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
libmbedcrypto.a |
71,391 | 71,391 | ±0 |
libmbedtls.a |
45,803 | 45,803 | ±0 |
libmbedx509.a |
7,084 | 7,084 | ±0 |
libesp-tls.a |
6,709 | 6,709 | ±0 |
libesp_https_server.a |
1,552 | 1,552 | ±0 |
String pool (attributed to efuse) — 132,195
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
esp_efuse_utility.c.obj |
131,149 | 130,931 | +218 (+0.2%) |
esp_efuse_rtc_calib.c.obj |
566 | 566 | ±0 |
esp_efuse_table.c.obj |
268 | 268 | ±0 |
esp_efuse_api.c.obj |
151 | 151 | ±0 |
esp_efuse_startup.c.obj |
61 | 61 | ±0 |
lwIP / HTTP / WebSocket — 126,594
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
liblwip.a |
78,301 | 78,301 | ±0 |
libhttp_parser.a |
10,350 | 10,350 | ±0 |
libesp_http_server.a |
10,016 | 10,016 | ±0 |
libtcp_transport.a |
8,237 | 8,237 | ±0 |
libespressif__esp_websocket_client.a |
6,826 | 6,826 | ±0 |
libesp_http_client.a |
6,620 | 6,620 | ±0 |
libesp_netif.a |
6,244 | 6,244 | ±0 |
fos_ firmware shell (C)* — 116,957
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
fos_http.c.obj |
22,162 | 21,968 | +194 (+0.9%) |
fos_cloud.c.obj |
17,877 | 17,837 | +40 (+0.2%) |
fos_console.c.obj |
13,576 | 13,498 | +78 (+0.6%) |
fos_client.c.obj |
10,483 | 10,483 | ±0 |
fos_scenes.c.obj |
7,459 | 7,459 | ±0 |
fos_settings.c.obj |
6,611 | 6,511 | +100 (+1.5%) |
fos_ota.c.obj |
6,076 | 5,775 | +301 (+5.2%) |
fos_config.c.obj |
6,022 | 5,755 | +267 (+4.6%) |
fos_assets.c.obj |
3,282 | 3,282 | ±0 |
fos_wifi.c.obj |
3,201 | 3,201 | ±0 |
fos_cloud_contract.c.obj |
2,955 | 2,896 | +59 (+2.0%) |
fos_sd_probe.c.obj |
2,753 | 2,753 | ±0 |
fos_assets_sd.c.obj |
2,480 | 2,480 | ±0 |
fos_schedule.c.obj |
2,339 | 2,339 | ±0 |
fos_buttons.c.obj |
2,139 | 2,139 | ±0 |
fos_status_screen.c.obj |
2,049 | 2,049 | ±0 |
main.c.obj |
1,671 | 1,673 | -2 (-0.1%) |
fos_tz.c.obj |
1,381 | 1,381 | ±0 |
fos_battery.c.obj |
1,140 | 1,140 | ±0 |
fos_framebuffer.c.obj |
483 | 483 | ±0 |
fos_power.c.obj |
241 | 241 | ±0 |
fos_battery_filter.c.obj |
214 | 214 | ±0 |
fos_wake.c.obj |
184 | 184 | ±0 |
fos_board.c.obj |
179 | 179 | ±0 |
libc / libm / newlib — 116,259
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
libc.a |
76,467 | 76,467 | ±0 |
libm.a |
31,563 | 31,563 | ±0 |
libnewlib.a |
6,609 | 6,609 | ±0 |
libstdc++.a |
1,476 | 1,476 | ±0 |
libgcc.a |
98 | 98 | ±0 |
libcxx.a |
46 | 46 | ±0 |
Storage (SPIFFS / FatFS / SD / NVS) — 94,136
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
libspiffs.a |
20,380 | 20,380 | ±0 |
libfatfs.a |
20,133 | 20,133 | ±0 |
libspi_flash.a |
14,168 | 14,168 | ±0 |
libnvs_flash.a |
13,745 | 13,745 | ±0 |
libsdmmc.a |
13,013 | 13,013 | ±0 |
libvfs.a |
6,196 | 6,196 | ±0 |
libesp_driver_sdspi.a |
3,858 | 3,858 | ±0 |
libesp_partition.a |
1,960 | 1,960 | ±0 |
libesp_vfs_console.a |
678 | 678 | ±0 |
libnvs_sec_provider.a |
5 | 5 | ±0 |
Nim packages (chrono, zippy, chroma, qrgen, ...) — 72,031
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
QRgen 3.1.0 |
22,036 | 22,036 | ±0 |
chrono 0.3.1 |
15,514 | 15,514 | ±0 |
zippy 0.10.19 |
15,272 | 15,272 | ±0 |
chroma 1.0.0 |
14,916 | 14,916 | ±0 |
checksums 0.2.1 |
3,371 | 3,371 | ±0 |
flatty 0.3.4 |
533 | 533 | ±0 |
jsony 1.1.5 |
272 | 272 | ±0 |
bumpy 1.1.3 |
117 | 117 | ±0 |
Display drivers (C) — 63,416
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
(everything else) |
20,633 | 20,623 | +10 (+0.0%) |
frameos_panel_table.c.obj |
6,685 | 6,685 | ±0 |
EPD_13in3e.c.obj |
1,792 | 1,796 | -4 (-0.2%) |
EPD_3in7.c.obj |
1,508 | 1,508 | ±0 |
EPD_5in79.c.obj |
1,445 | 1,445 | ±0 |
EPD_2in7.c.obj |
1,259 | 1,259 | ±0 |
EPD_4in2.c.obj |
1,246 | 1,246 | ±0 |
EPD_7in3e.c.obj |
1,208 | 1,208 | ±0 |
DEV_Config_esp.c.obj |
1,180 | 1,180 | ±0 |
EPD_4in2_V2.c.obj |
1,172 | 1,172 | ±0 |
DEV_Debug.c.obj |
1,159 | 1,159 | ±0 |
EPD_4in26.c.obj |
1,110 | 1,110 | ±0 |
| 30 smaller rows | 23,019 | 23,025 | -6 (-0.0%) |
Crypto (monocypher) — 35,149
| Part | This PR | v2026.9.11 | Δ |
|---|---|---|---|
monocypher.c.obj |
31,920 | 31,920 | ±0 |
monocypher-ed25519.c.obj |
3,229 | 3,229 | ±0 |
Biggest movers (per object)
| Object | Δ | This PR | Baseline |
|---|---|---|---|
fos_ota.c.obj |
+301 | 6,076 | 5,775 |
fos_config.c.obj |
+267 | 6,022 | 5,755 |
esp_efuse_utility.c.obj |
+218 | 131,149 | 130,931 |
fos_http.c.obj |
+194 | 22,162 | 21,968 |
fos_settings.c.obj |
+100 | 6,611 | 6,511 |
fos_console.c.obj |
+78 | 13,576 | 13,498 |
fos_cloud.c.obj |
+40 | 17,877 | 17,837 |
frameos/types.nim |
+16 | 7,909 | 7,893 |
Largest 30 objects
| Object | Subsystem | This PR | v2026.9.11 | Δ |
|---|---|---|---|---|
quickjs.c.obj |
QuickJS | 283,813 | 283,813 | ±0 |
assets/fonts.nim |
Embedded font | 146,358 | 146,358 | ±0 |
esp_efuse_utility.c.obj |
String pool (attributed to efuse) | 131,149 | 130,931 | +218 (+0.2%) |
frameos/interpreter.nim |
FrameOS core (Nim) | 97,352 | 97,352 | ±0 |
nim/lib/pure/collections/tables.nim |
Nim stdlib | 70,556 | 70,556 | ±0 |
frameos/js_runtime/app_runtime.nim |
FrameOS core (Nim) | 61,502 | 61,502 | ±0 |
pkgs/pixie-6.1.0/pixie/fontformats/opentype.nim |
pixie | 55,507 | 55,507 | ±0 |
libunicode.c.obj |
QuickJS | 50,174 | 50,174 | ±0 |
nim/lib/system.nim |
Nim stdlib | 46,665 | 46,665 | ±0 |
apps/data/icalJson/ical.nim |
FrameOS apps (Nim) | 44,568 | 44,568 | ±0 |
pkgs/pixie-6.1.0/pixie/fileformats/webp.nim |
pixie | 37,525 | 37,525 | ±0 |
pkgs/pixie-6.1.0/pixie/fileformats/svg.nim |
pixie | 37,203 | 37,203 | ±0 |
frameos/utils/image.nim |
FrameOS core (Nim) | 34,374 | 34,374 | ±0 |
pkgs/pixie-6.1.0/pixie/paths.nim |
pixie | 32,570 | 32,570 | ±0 |
monocypher.c.obj |
Crypto (monocypher) | 31,920 | 31,920 | ±0 |
frameos/js_runtime/runtime.nim |
FrameOS core (Nim) | 30,094 | 30,094 | ±0 |
pkgs/pixie-6.1.0/pixie/fileformats/jpeg.nim |
pixie | 25,338 | 25,338 | ±0 |
apps/render/calendar/app.nim |
FrameOS apps (Nim) | 23,618 | 23,618 | ±0 |
pkgs/pixie-6.1.0/pixie/fileformats/png.nim |
pixie | 22,849 | 22,849 | ±0 |
fos_http.c.obj |
fos_* firmware shell (C) | 22,162 | 21,968 | +194 (+0.9%) |
apps/render/chart/app.nim |
FrameOS apps (Nim) | 18,940 | 18,940 | ±0 |
embedded_main.nim |
FrameOS core (Nim) | 18,347 | 18,347 | ±0 |
x509_crt_bundle.S.obj |
mbedTLS + certificates | 17,928 | 17,928 | ±0 |
frameos/utils/status_screen.nim |
FrameOS core (Nim) | 17,923 | 17,923 | ±0 |
fos_cloud.c.obj |
fos_* firmware shell (C) | 17,877 | 17,837 | +40 (+0.2%) |
nim/lib/pure/unicode.nim |
Nim stdlib | 17,405 | 17,405 | ±0 |
nim/lib/pure/times.nim |
Nim stdlib | 16,984 | 16,984 | ±0 |
lib/tz.nim |
FrameOS core (Nim) | 16,875 | 16,875 | ±0 |
apps/data/immich/app.nim |
FrameOS apps (Nim) | 15,991 | 15,991 | ±0 |
pkgs/pixie-6.1.0/pixie/images.nim |
pixie | 15,787 | 15,787 | ±0 |
Flash = .text + .rodata from the linker map via esp_idf_size; "Inside each subsystem" splits every bucket one level down (the Nim core by directory, apps by app, packages by package, ESP-IDF by archive) over all objects, not just the largest. "String pool (attributed to efuse)" is the linker's merged string-literal pool for the whole image, not efuse code — see docs/esp32-image-size.md.
…ess auto_update is on Only one plane updated itself: the ESP32 firmware ran a 24 h OTA poll unconditionally on backend-managed frames, while the Pi runtime, the cloud-managed ESP32 and the cloud only ever acted on a button or a nudge. One boolean now covers every plane, off by default. - Contract: `auto_update` on both profiles from 2026.9.11 (+ fixtures, regenerated Nim/C/TS tables). - Pi runtime: `autoUpdate` in frame.json; frameos/auto_updater.nim runs a quiet daily GitHub check (04:xx local, minute hashed per frame) and only then the same detached signed upgrade the admin button uses. Skips with a logged reason on compiled scenes, an unversioned binary, or an unsupported target. Re-reads the switch every pass, no restart. - ESP32: the periodic OTA task starts only via fos_ota_sync_periodic_task() when the switch is on and re-checks it every tick; the key rides NVS, the backend poll, the cloud verb, the console and the local HTTP API. The periodic check now falls back to the cloud manifest on a cloud-only frame. - Backend: frame.auto_update column + migration, schema, sync label, backup restore list, ESP32 settings pull and provisioning line. frame.json only carries the key for frames that can take a generic release (frame_can_auto_update). - Cloud + SPA: floor pairs, frameDiffKeys, an Updates section on backend and on-device frames, a switch in the cloud settings block — disabled with the reason on compiled/source-built Pis, below the floor, or on an ESP32 layout without an OTA slot. MCP tool description and docs updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdmCbU4h6hRRaciZ1yxnNX
…a 24 h stability window Automatic updates are now on by default, on the `stable` channel: the device installs the latest signed release only once it has been the latest for a day. A fix published within that day replaces it as latest and resets the clock, so the release the fix was for is never installed. `latest` installs every release as it lands; `off` never checks. - Contract: `auto_update` is a string enum on both profiles (fixtures, regenerated tables). - Pi runtime: FrameConfig.autoUpdate is the channel (absent/true → stable, false → off); upgrade.nim carries GitHub's published_at; auto_updater.nim decides with releaseQualifies() and logs waiting_for_stable / publish_time_unknown instead of scheduling. - ESP32: fos_auto_update_t + parse/name helpers; every settings surface takes the three names (booleans still map); the manifest's `publishedAt` (unix seconds) gates the PERIODIC run on stable — manual `ota`, the backend's action and notify_update_available install at once; an unknown publish time or an unsynced clock waits. - Backend: the column is a String channel (NULL = stable), normalized on the way in and out; the OTA manifest carries publishedAt. - Cloud: the manifest route relays publishedAt (null for a dev image). - SPA: a three-way Select in every place the switch was; docs + MCP text. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdmCbU4h6hRRaciZ1yxnNX
… of the hour The backend's reboot schedule (/etc/cron.d/frameos-reboot) fires on the hour, 04:00 by default, and a check at 04:00 or 04:01 could start an upgrade the reboot then kills half-staged, every day. The per-frame slot now lands in the last forty minutes of the hour: a just-rebooted frame runs its check fresh, and an upgrade started at :20 is done long before the next hour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdmCbU4h6hRRaciZ1yxnNX
…pdate / publishedAt tools/buildroot-images' ReleaseImageFrame hand-lists every frame column and had no auto_update, so get_frame_json raised while composing a card; it now ships "stable", and the three readers of the column go through getattr so a stub frame without it reads as the default. The cloud firmware route's unit test pins the exact manifest shape: publishedAt joins it (unix seconds from the release, null for a dev image). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdmCbU4h6hRRaciZ1yxnNX
…ate joins both profiles Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdmCbU4h6hRRaciZ1yxnNX
… latest
A self-hosted backend deploys ITS version (the precompiled release of
versions.json). Both device types were pulling GitHub's latest instead —
the ESP32 manifest relay always had, the new Pi auto-updater as well — so a
frame climbed past its backend and the next deploy downgraded it again.
- Backend: GET /api/frameos/release (bearer = the frame's server API key)
answers {"version": <this backend's release>}. The ESP32 OTA manifest and
download are pinned to that release's tag; GitHub's latest still feeds
the browser flasher and SD-image listing only.
- Pi runtime: upgrade.nim resolveFrameOSRelease() asks the backend when
frame.json names one (backendPinnedVersion, same host/port/TLS rule as the
log uploader) and fetches that release by tag; no backend → GitHub's
latest. The auto-updater, the admin panel's Upgrade button/status and the
cloud nudge all resolve through it; `frameos upgrade --version=X` pins a
release, and the auto-updater passes the one it just qualified. A backend
that cannot answer (unreachable, or older than 2026.9.11) is an error,
never a fallback to latest.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WdmCbU4h6hRRaciZ1yxnNX
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdmCbU4h6hRRaciZ1yxnNX
8bd22ee to
86109ce
Compare
…t its parent After the rebase two migrations revised d4e6f8a0b2c4 and alembic refused 'head' — every CI job that boots the backend died on it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdmCbU4h6hRRaciZ1yxnNX
…ESP32 checks ten minutes after boot - 2026.9.11 shipped without this PR, so the contract floor for auto_update (and every mirror of it: SPA/cloud gates, tests, docs, the backend's "update the backend first" message) moves to 2026.9.12. - effective_auto_update(frame | frame dict): the channel the DEVICE is told — "off" when the frame cannot take a generic release. frame.json, the shell-less sync push and the sync drawer all go through it, so a Pi with a compiled scene no longer shows "Automatic updates: stable → off" as a pending change forever, and the push never tells such a frame to update. A device that reports no channel at all (firmware before 2026.9.12) is not a choice either. - ESP32: the periodic task's first check comes ten minutes after boot instead of a full interval later — a frame that restarts daily never checked. The `stable` wait is passed into ota_run_signed explicitly (unattended) instead of read off a global by every caller. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WdmCbU4h6hRRaciZ1yxnNX
|
Closing this right now. it's gone stale and the actual fix will also have to take into account on-frame migrations (e.g. moving a nim app to a js app). The decision there is that each version could contain the upgrade script from the previous version, and we'll force the upgrades to not go straight to lateset, but also run certain migration versions in the mean while. |
Why
Only one plane updated itself: the ESP32 firmware ran a 24 h OTA poll unconditionally on backend-managed frames, while the Pi runtime, the cloud-managed ESP32 and the cloud only ever acted on a button or a
notify_update_availablenudge. That was inconsistent, and "always install the newest release the moment it lands" is also the wrong default when releases ship often and carry bugs.This PR replaces it with one explicit per-frame channel,
auto_update, that means the same thing on every plane:ota,notify_update_available) work on every channel.A provider can only pick the channel, never name a release or a URL, so the signed-OTA doctrine is unchanged: the device fetches and verifies every image itself.
Which release a frame follows
serverHost): the release the backend runs, never GitHub's latest. The backend deploys its own version, so a frame ahead of it would be downgraded by the next deploy and climb back the next night. The Pi asksGET /api/frameos/release(bearer = its server API key) and fetches that release by tag; the ESP32's OTA manifest and download are pinned to the same tag. A backend too old to answer (before 2026.9.11) is an error, never a fallback to latest.What
auto_update("off" | "stable" | "latest") on both profiles from 2026.9.11, fixtures, regenerated Nim/C/TS tables.autoUpdatein frame.json (absent/true→ stable,false→ off); newframeos/src/frameos/auto_updater.nimthread: daily at 04:20–04:59 local (minute hashed per frame, never the first twenty minutes of the hour because the backend's reboot cron fires on the hour),resolveFrameOSRelease()(backend pin, else latest),releaseQualifies()on the release'spublished_at, then the same detachedscheduleFrameOSUpgrade()the admin button and the cloud nudge use, pinned to the qualified version. Skips with a loggedframeos:auto_updatereason on compiled scenes (any compiled-in scene other thandefault), an unversioned binary, an unsupported target, or a release still inside the stability window. Re-reads the channel every pass.frameos upgrade --version=Xpins a release.fos_auto_update_t; the periodic OTA task is started only byfos_ota_sync_periodic_task()when the channel is not off and re-checks it every tick. The channel rides NVS, the backend settings poll (autoUpdate), the cloudset_settingsverb, the console (set auto_update off|stable|latest), the local HTTP API and the status JSON. The manifest's newpublishedAtgates the periodic run onstable; an unknown publish time or an unsynced clock waits. The periodic check falls back to the cloud manifest when no backend is configured, so the channel works on cloud-only frames too.frame.auto_updateString column + migration (NULL = stable), normalized on the way in and out, schema validator (booleans still map), sync label, backup restore list, ESP32 settings pull and provisioning line, the SD-image stub frame. frame.json carries"off"for frames that cannot take a generic release (frame_can_auto_update: rpios/buildroot must be on the precompiled build with no compiled scenes).GET /api/frameos/release; the OTA manifest/download pinned to the backend's release and carryingpublishedAt.publishedAt(null for a dev image); floor pairs inframes.ts; MCPframe_settings_updatedescription.cloud-frames.mdpages and the ESP32 README.Behaviour change to note in release notes
Every frame is on stable once it runs this release: Pi/Buildroot frames on the precompiled build start checking daily, ESP32 frames poll their control plane daily. Backend-managed frames follow their backend's version, so updating the backend is what moves them. Set the channel to off on a frame that must not move.
Verified locally (on the rebased branch)
test_auto_updatertests pass.Rebased on main; CI's snapshot commit was dropped so the next run refreshes the visual snapshots against the new base.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WdmCbU4h6hRRaciZ1yxnNX