Skip to content

Buttons in two layers: down/up at once, press/doublePress/longPress once clear; 30 ms debounce; a sound per event - #557

Merged
mariusandra merged 13 commits into
mainfrom
esp32-button-timing
Oct 16, 2026
Merged

mariusandra merged 13 commits into
mainfrom
esp32-button-timing

Conversation

@mariusandra

@mariusandra mariusandra commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

On the ESP32 the button task (fos_buttons.c) queues edges, and the render task drains the queue only between passes. An E1004 refresh is about 30 s. The Nim dispatcher (input_state.nim) timed every edge when it was delivered, so a tap made during a refresh became a 30 s hold, and two taps could not be told from two slow presses.

Review found more:

  • The debounce lost presses. The ESP32 waited for 80 ms of steady level before taking an edge, which swallowed quick taps and the second tap of a quick double. A Pi's lgpio debounce was 100 ms with the same meaning.
  • The events were the wrong abstraction. press was the down edge itself, so a double tap was press, press, doublePress, and a scene binding press to next and double to previous went next, next, previous.
  • A hold chirped too much: at 0 s, 0.5 s and 1 s.

Two layers, one event (docs/events-contract.json)

Still one event, button; action says which. No new event names.

Layer Action When
hardware down the key goes down, at once
hardware up the key comes up, at once, with durationMs
button press a short press, doublePressMs (350) after its up when no second down came
button doublePress the second down inside that window; the pair is that and nothing more
button longPress on the up of a press held at least longPressMs (now 1000), once, with durationMs and heldSeconds; nothing while held

Each press is exactly one of press, doublePress or longPress. A tap is down, up, press; a double tap down, up, down, doublePress, up; a 2.2 s hold down, then up and longPress together. A listener with no action filter still hears press only. A scene that must count every tap listens for down.

  • Removed: repeat and release, and the repeatMs key. longPressMs goes 500 → 1000.
  • Wire: drivers send down / up. The dispatcher takes an edge by its level, so a driver that still says press / release works.
  • Not an edge: a button from HTTP, the cloud, a schedule or a scene moved no line, so it is delivered as said, with no action meaning press.
  • Timing: every rule is timed from the edges' atMs, never from delivery. A tap during a refresh is a tap, a double tap a doublePress, a hold through it a longPress.
  • The timing numbers are emitted into fos_events_gen.h, so the C side mirrors the contract.

Sounds (ESP32)

Every event sounds the instant it happens, played by the button task from fos_button_timing.c: the dispatcher's rules on the same stamps.

Event Sound (pitch × buzzer_tone_hz, ms)
down 1× 14
up 0.7× 14, every up: a tap's, a double press's, a long press's
doublePress its second down is 1.5× 14 instead of 1×
a key held 1× 8 then 0.5× 20 at every whole second it stays down
press none of its own: its clicks were the down and the up

A double tap is 2000, 1400, 3000, 1400 Hz at the default tone. A 2.5 s hold is "click, dun, dun, clack": the duns count the hold, and the scene hears its one longPress with the up. The old per-second tick and the two-note double sound are gone. The playground below has an editor for every note's pitch and length, with a copy-out in the firmware's terms.

Hosts tick while a button is busy

A press is decided 350 ms after its up, and only a tick delivers it.

  • ESP32: fos_buttons_busy() is true while a key is down or a tap is in its window. The idle wait then runs in 50 ms slices instead of 1 s. A frame about to deep-sleep waits for the decision, up to 10 s, and renders if the scene asked, so a tap just before sleep is not lost.
  • Preview: the worker ticks every 50 ms while a button is busy. frameos_wasm_pointers_down counts busy buttons too.
  • Linux: the runner already ticks every loop.

From deep sleep

The press that wakes an ESP32 is a press like any other. Holding it is a long press, with the per-second sounds; tapping it and tapping again is a double press. Until now it was replayed as a plain press with no hold, so it could be neither.

  • Blind boot. The chip cannot see its keys while it boots. On the E1002 the button task's first look comes 0.75 s after the reset, and that first look decides the stamps.
  • Still down: the press began at the reset (atMs 0), so the hold counts from there and its up is a longPress when it should be.
  • Already up: it was a tap. Its down and up are stamped then, so a second tap within 350 ms of that pairs into a doublePress. A second tap made and let go entirely during the boot is not seen.
  • Marking. The press, doublePress or longPress a wake starts carries wake: true.
  • Smaller queue item. It drops its duration field (12 → 8 bytes), which only the old wake release used.
  • Faster boot. The PSRAM memory test is off: it was 0.41 s of every boot, deep-sleep wakes included, and the task now starts at 0.75 s instead of 1.18 s.
  • Not done: skipping the bootloader's image check on deep-sleep wakes would save about 0.5 s more. But OTA never updates the bootloader, and IDF warns against that option when an OTA switches slots and the chip then deep-sleeps.

Debounce

  • ESP32: the first change of level is the edge, polled every 20 ms, and bounce is locked out for 30 ms after it.
  • Pi: the lgpio debounce is 30 ms of steady level.

Tests

  • docs/event-fixtures.json inputState: 17 cases, run by test_input_state.nim. They cover a tap and its window, a fast double, the window's edge and the third tap, slow taps, a tap, a double and two slow taps during a render, a hold as one long press on its up, a hold through a render, a key held when the render ends, the long-press line (999 vs 1000 ms), a held double, the wake press, no stamp, a future stamp, someone else's press, and an older driver's edges. Direct tests were rewritten too.

  • Three dispatcher fixture cases used a raw level: 0 edge to mean "a press". They now send action: press, since the case is about queued dispatch, not timing.

  • test_input_state, test_event_loop, test_event_fixtures, evdev test_translate: pass. nim check passes on src/frameos.nim, the wasm entry with the preview's defines, and the gpioButton driver.

  • main/tests/test_button_timing.c: 48 checks, 0 failures, run in CI by backend/app/tasks/tests/test_esp32_button_timing.py. The contract and scene-guide checks and the backend ESP32 tests pass, 22 in all.

  • ESP32 firmware built in the frameos-esp32-ci-check container (EPD_7in3e, 8 MB layout), no warnings in the touched files. Size against main (9857fca), measured with firmware_size.py: +1,936 bytes. The CI comment compares with the v2026.10.15 release, so its +6 KB also counts Photos: shapes, album links, Select all; waiting deploys; JS app console and tests #553 and ESP32 buzzer: wake press clicks like any press, a tick per second held, heldSeconds #554. Of the 1.9 KB:

    Part Δ
    input_state.nim (the dispatcher's button layer) +942
    fos_button_timing.c +434
    fos_buzzer.c (the note sequencer for the long-press pair) +261
    std/monotimes (timing from atMs) +95
    fos_client.c (short slices, sleep settle) +84

    Every button payload is built by one helper, and a waiting tap reuses the held-key record instead of a second type. That cut the first measurement of +4.2 KB to this.

  • pnpm verify from cloud/ (FRAMEOS_WASM_SOURCE=local): 28 of 28.

Needs a bench

The bench E1002 runs this branch (written into its live slot as 2026.10.15, backups kept) and boots, renders and sleeps normally; the gestures themselves, the wake ones included, are still to be pressed.

docs/manual-testing-todo.md has the boxes. On the E1004, check each item:

  • The fastest tap is down, up, press, and the fastest double tap one doublePress with no press.
  • A 2.5 s hold sounds "click, dun, dun, clack" and logs one longPress; a double tap sounds 2000, 1400, 3000, 1400 Hz.
  • A tap right before a battery frame sleeps still lands as a press.
  • Taps and holds through a refresh keep their timing.
  • The double's high second down and the hold's low note carry on the piezo.

On a Pi, the 30 ms debounce must not let a slow press bounce into two.

A browser playground with the same rules (button task, dispatcher, refresh, sounds): https://claude.ai/artifact/HkqaESr9iSoHyqDXz61936

🤖 Generated with Claude Code

…edges, doublePress

On the ESP32 the button task queues edges and the render task drains the
queue only between passes, so a press and its release made during a 30 s
E1004 refresh were both timed at the drain: a tap arrived as a 30 s hold
with a longPress it never had, and two taps could not be told from two
slow presses.

Every `button` edge now carries `atMs`, when the driver saw it, in ms on
the frame's monotonic clock (docs/events-contract.json; esp_timer in
fos_buttons.c, the same clock Nim's getMonoTime reads under freertos;
getMonoTime in the Linux gpioButton driver). The queue item grows from 8
to 12 bytes (64 items, 768 bytes) and the stamp is widened back on drain
by fos_wake_edge_at_ms, wrap-safe. input_state.nim measures the hold
between the edges, advances the holds to an edge's time before applying
it, delivers the longPress that was due right before a late release, and
makes `doublePress` from a second short press within buttons.doublePressMs
(350) of the previous one's release; the third press starts over. The
ESP32/wasm host no longer ticks to now on an event's way in, which is what
turned a queued tap into a 30 s long press. The buzzer's hold ticks count
from the same instant as heldSeconds.

The corpus gains an `inputState` section (test_input_state.nim runs it);
fos_wake's host test covers the stamp arithmetic; docs/events.md,
event-system-analysis.md, the ESP32 README and the manual test boxes say
the new rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

ESP32 firmware size

Built from d62c597 by this run; compared with the latest release v2026.10.15.

Image This PR v2026.10.15 Δ
esp32-s3 app (OTA image) 3,141,472
87.2% of 3520K slot, 452 KB free
3,138,272 +3,200 (+0.1%)
esp32-s3 merged flash image 3,207,008 3,203,808 +3,200 (+0.1%)
esp32-s3-32mb app (OTA image) 3,141,472
76.1% of 4032K slot, 964 KB free
3,138,272 +3,200 (+0.1%)
esp32-s3-32mb merged flash image 3,272,544 3,269,344 +3,200 (+0.1%)

Breakdown by subsystem — esp32-s3

Subsystem This PR v2026.10.15 Δ
FrameOS core (Nim) 479,284 477,042 +2,242 (+0.5%)
QuickJS 360,381 360,373 +8 (+0.0%)
pixie 304,390 304,354 +36 (+0.0%)
Wi-Fi stack 295,304 295,304 ±0
Nim stdlib 251,515 251,436 +79 (+0.0%)
FrameOS apps (Nim) 190,404 190,356 +48 (+0.0%)
ESP-IDF misc 181,367 181,564 -197 (-0.1%)
Embedded font 146,362 146,362 ±0
String pool (attributed to efuse) 136,028 136,134 -106 (-0.1%)
fos_* firmware shell (C) 133,310 132,233 +1,077 (+0.8%)
mbedTLS + certificates 132,439 132,439 ±0
lwIP / HTTP / WebSocket 126,788 126,800 -12 (-0.0%)
libc / libm / newlib 116,263 116,263 ±0
Storage (SPIFFS / FatFS / SD / NVS) 94,076 94,080 -4 (-0.0%)
Nim packages (chrono, zippy, chroma, qrgen, ...) 71,845 71,849 -4 (-0.0%)
Display drivers (C) 64,958 64,946 +12 (+0.0%)
Crypto (monocypher) 35,141 35,141 ±0
Total mapped flash 3,119,855 3,116,676 +3,179 (+0.1%)
Inside each subsystem

FrameOS core (Nim) — 479,284

Part This PR v2026.10.15 Δ
frameos/js_runtime 116,426 115,444 +982 (+0.9%)
frameos/utils 112,365 112,401 -36 (-0.0%)
frameos/interpreter.nim 100,316 100,264 +52 (+0.1%)
embedded_main.nim 19,252 19,252 ±0
embedded_runtime.nim 17,830 17,834 -4 (-0.0%)
lib/tz.nim 16,722 16,722 ±0
frameos_nim_glue.c.obj 10,679 10,635 +44 (+0.4%)
frameos/planner.nim 9,681 9,681 ±0
frameos/event_loop.nim 8,412 8,412 ±0
frameos/types.nim 7,909 7,909 ±0
frameos/values.nim 7,699 7,699 ±0
frameos/single_scene_host.nim 6,687 6,712 -25 (-0.4%)
frameos/apps.nim 6,585 6,585 ±0
frameos/spool.nim 5,787 5,787 ±0
frameos/input_state.nim 4,947 3,766 +1,181 (+31.4%)
embedded_scene.nim 4,945 4,945 ±0
frameos/events_gen.nim 3,948 3,948 ±0
frameos/app_config.nim 2,995 2,935 +60 (+2.0%)
frameos/reboot_reason.nim 2,648 2,648 ±0
frameos/node_config.nim 2,618 2,618 ±0
frameos/app_capabilities.nim 2,489 2,489 ±0
frameos/events.nim 2,410 2,410 ±0
fos_netguard.c.obj 2,175 2,179 -4 (-0.2%)
frameos/refresh_interval.nim 1,635 1,639 -4 (-0.2%)
frameos/runtime_diagnostics.nim 767 767 ±0
frameos/event_log.nim 427 431 -4 (-0.9%)
fos_version.c.obj 328 328 ±0
frameos/cloud 255 255 ±0
frameos/channels.nim 152 152 ±0
frameos/ids.nim 137 137 ±0
frameos/hal 44 44 ±0
frameos/retired_apps.nim 14 14 ±0

QuickJS — 360,381

Part This PR v2026.10.15 Δ
quickjs.c.obj 287,854 287,846 +8 (+0.0%)
libunicode.c.obj 50,178 50,178 ±0
libregexp.c.obj 13,999 13,999 ±0
dtoa.c.obj 5,685 5,685 ±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

pixie — 304,390

Part This PR v2026.10.15 Δ
pixie/fontformats/opentype.nim 55,460 55,460 ±0
pixie/fileformats/webp.nim 37,493 37,493 ±0
pixie/fileformats/svg.nim 36,983 36,983 ±0
pixie/paths.nim 32,875 32,875 ±0
pixie/fileformats/jpeg.nim 29,436 29,436 ±0
pixie/fileformats/png.nim 22,101 22,101 ±0
pixie/images.nim 15,779 15,779 ±0
pixie/fonts.nim 10,995 10,995 ±0
pixie/paints.nim 9,869 9,869 ±0
pixie/fileformats/bmp.nim 8,278 8,258 +20 (+0.2%)
pixie/common.nim 7,413 7,413 ±0
pixie/blends.nim 6,516 6,516 ±0
pixie/fileformats/gif.nim 5,957 5,965 -8 (-0.1%)
pixie/inflatestream.nim 5,233 5,233 ±0
pixie.nim 5,224 5,224 ±0
pixie/fileformats/webp_vp8_tables.nim 3,881 3,881 ±0
pixie/fileformats/ppm.nim 3,717 3,713 +4 (+0.1%)
pixie/contexts.nim 3,054 3,054 ±0
pixie/fileformats/qoi.nim 2,722 2,702 +20 (+0.7%)
pixie/internal.nim 754 754 ±0
pixie/rgb565.nim 385 385 ±0
pixie/fontformats/svgfont.nim 215 215 ±0
pixie/decodebudget.nim 50 50 ±0

Wi-Fi stack — 295,304

Part This PR v2026.10.15 Δ
libnet80211.a 136,168 136,168 ±0
libpp.a 62,154 62,154 ±0
libwpa_supplicant.a 54,904 54,904 ±0
libphy.a 34,176 34,176 ±0
libesp_wifi.a 4,780 4,780 ±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 — 251,515

Part This PR v2026.10.15 Δ
pure/collections/tables.nim 72,312 72,312 ±0
system.nim 48,465 48,457 +8 (+0.0%)
pure/unicode.nim 17,405 17,405 ±0
pure/times.nim 16,888 16,888 ±0
pure/json.nim 13,722 13,722 ±0
std/private/dragonbox.nim 10,351 10,351 ±0
pure/strutils.nim 8,047 8,047 ±0
pure/parsexml.nim 6,003 6,011 -8 (-0.1%)
pure/algorithm.nim 5,372 5,372 ±0
pure/base64.nim 5,290 5,290 ±0
pure/hashes.nim 4,181 4,181 ±0
pure/xmlparser.nim 3,555 3,555 ±0
pure/parsejson.nim 3,553 3,553 ±0
std/formatfloat.nim 3,504 3,504 ±0
pure/uri.nim 3,225 3,225 ±0
pure/streams.nim 2,820 2,820 ±0
pure/strformat.nim 2,546 2,546 ±0
std/syncio.nim 2,339 2,355 -16 (-0.7%)
pure/collections/sets.nim 2,075 2,075 ±0
pure/xmltree.nim 2,047 2,047 ±0
pure/lexbase.nim 1,801 1,801 ±0
pure/parseutils.nim 1,750 1,750 ±0
system/exceptions.nim 1,598 1,598 ±0
pure/strtabs.nim 1,572 1,572 ±0
pure/pathnorm.nim 1,336 1,336 ±0
pure/options.nim 1,016 1,016 ±0
std/private/ospaths2.nim 994 994 ±0
pure/collections/lists.nim 979 979 ±0
pure/math.nim 841 841 ±0
pure/collections/deques.nim 822 822 ±0
pure/random.nim 696 696 ±0
std/private/digitsutils.nim 651 651 ±0
std/oserrors.nim 633 633 ±0
std/private/osdirs.nim 572 572 ±0
system/dollars.nim 529 529 ±0
std/monotimes.nim 501 406 +95 (+23.4%)
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 65 65 ±0
system/ctypes.nim 9 9 ±0

FrameOS apps (Nim) — 190,404

Part This PR v2026.10.15 Δ
apps/data/icalJson 50,737 50,733 +4 (+0.0%)
apps/render/calendar 30,886 30,874 +12 (+0.0%)
apps/data/localImage 10,432 10,432 ±0
apps/render/split 9,732 9,772 -40 (-0.4%)
apps/data/eventsToAgenda 9,064 9,068 -4 (-0.0%)
apps/render/zoomPan 8,668 8,648 +20 (+0.2%)
apps/apps.nim 7,251 7,247 +4 (+0.1%)
apps/render/text 5,483 5,455 +28 (+0.5%)
apps/data/xmlToJson 5,428 5,428 ±0
apps/render/svg 5,208 5,192 +16 (+0.3%)
apps/render/image 4,708 4,708 ±0
apps/data/rotateImage 3,465 3,465 ±0
apps/logic/setAsState 3,453 3,453 ±0
apps/data/downloadImage 3,321 3,325 -4 (-0.1%)
apps/render/gradient 3,213 3,229 -16 (-0.5%)
apps/data/qr 3,118 3,122 -4 (-0.1%)
apps/data/downloadUrl 3,053 3,053 ±0
apps/render/color 2,844 2,824 +20 (+0.7%)
apps/data/newImage 2,668 2,668 ±0
apps/data/resizeImage 2,558 2,558 ±0
apps/render/opacity 2,320 2,296 +24 (+1.0%)
apps/data/log 2,299 2,299 ±0
apps/data/prettyJson 2,225 2,225 ±0
apps/data/clock 1,871 1,875 -4 (-0.2%)
apps/data/parseJson 1,848 1,856 -8 (-0.4%)
apps/logic/nextSleepDuration 1,663 1,663 ±0
apps/logic/ifElse 1,494 1,494 ±0
apps/logic/breakIfRendering 1,394 1,394 ±0

ESP-IDF misc — 181,367

Part This PR v2026.10.15 Δ
libesp_hw_support.a 37,636 37,632 +4 (+0.0%)
libhal.a 21,865 21,865 ±0
libfreertos.a 17,240 17,240 ±0
libesp_system.a 13,736 13,780 -44 (-0.3%)
libesp_driver_uart.a 11,627 11,627 ±0
libheap.a 8,660 8,660 ±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,878 3,878 ±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,253 3,253 ±0
libesp_driver_ledc.a 3,054 3,054 ±0
libesp_mm.a 2,451 2,451 ±0
libapp_update.a 2,414 2,414 ±0
libesp_timer.a 2,337 2,337 ±0
libesp_psram.a 2,172 2,329 -157 (-6.7%)
libsoc.a 2,316 2,316 ±0
libesp_adc.a 2,285 2,285 ±0
libpthread.a 1,397 1,397 ±0
liblog.a 1,375 1,375 ±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,362

Part This PR v2026.10.15 Δ
assets/fonts.nim 146,362 146,362 ±0

String pool (attributed to efuse) — 136,028

Part This PR v2026.10.15 Δ
esp_efuse_utility.c.obj 134,982 135,088 -106 (-0.1%)
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

fos_ firmware shell (C)* — 133,310

Part This PR v2026.10.15 Δ
fos_http.c.obj 22,185 22,185 ±0
fos_cloud.c.obj 19,258 19,258 ±0
fos_console.c.obj 13,950 13,950 ±0
fos_client.c.obj 12,350 12,266 +84 (+0.7%)
fos_settings.c.obj 7,618 7,618 ±0
fos_scenes.c.obj 7,578 7,578 ±0
fos_ota.c.obj 5,467 5,467 ±0
fos_cloud_contract.c.obj 4,744 4,744 ±0
fos_config.c.obj 4,619 4,619 ±0
fos_wifi.c.obj 3,189 3,189 ±0
fos_assets.c.obj 3,124 3,124 ±0
fos_buttons.c.obj 2,772 2,493 +279 (+11.2%)
fos_sd_probe.c.obj 2,753 2,753 ±0
fos_schedule.c.obj 2,602 2,602 ±0
fos_assets_sd.c.obj 2,484 2,484 ±0
fos_events.c.obj 2,320 2,320 ±0
fos_status_screen.c.obj 2,049 2,049 ±0
main.c.obj 1,703 1,699 +4 (+0.2%)
fos_config_parse.c.obj 1,467 1,467 ±0
fos_url_guard.c.obj 1,397 1,397 ±0
fos_tz.c.obj 1,294 1,294 ±0
fos_buzzer.c.obj 1,162 901 +261 (+29.0%)
fos_battery.c.obj 1,140 1,140 ±0
fos_minisig.c.obj 1,140 1,140 ±0
fos_backend_http.c.obj 1,072 1,072 ±0
fos_commands.c.obj 940 940 ±0
fos_scene_state.c.obj 650 650 ±0
fos_framebuffer.c.obj 483 483 ±0
fos_button_timing.c.obj 434 — +434
fos_board.c.obj 334 334 ±0
fos_assets_path.c.obj 259 259 ±0
fos_power.c.obj 241 241 ±0
fos_battery_filter.c.obj 214 214 ±0
fos_wake.c.obj 199 184 +15 (+8.2%)
fos_json_guard.c.obj 119 119 ±0

mbedTLS + certificates — 132,439

Part This PR v2026.10.15 Δ
libmbedcrypto.a 71,295 71,291 +4 (+0.0%)
libmbedtls.a 45,799 45,799 ±0
libmbedx509.a 7,088 7,092 -4 (-0.1%)
libesp-tls.a 6,705 6,705 ±0
libesp_https_server.a 1,552 1,552 ±0

lwIP / HTTP / WebSocket — 126,788

Part This PR v2026.10.15 Δ
liblwip.a 78,433 78,445 -12 (-0.0%)
libhttp_parser.a 10,350 10,350 ±0
libesp_http_server.a 10,020 10,020 ±0
libtcp_transport.a 8,245 8,245 ±0
libespressif__esp_websocket_client.a 6,822 6,822 ±0
libesp_http_client.a 6,666 6,666 ±0
libesp_netif.a 6,252 6,252 ±0

libc / libm / newlib — 116,263

Part This PR v2026.10.15 Δ
libc.a 76,471 76,471 ±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,076

Part This PR v2026.10.15 Δ
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,681 13,685 -4 (-0.0%)
libsdmmc.a 13,013 13,013 ±0
libvfs.a 6,196 6,196 ±0
libesp_driver_sdspi.a 3,866 3,866 ±0
libesp_partition.a 1,956 1,956 ±0
libesp_vfs_console.a 678 678 ±0
libnvs_sec_provider.a 5 5 ±0

Nim packages (chrono, zippy, chroma, qrgen, ...) — 71,845

Part This PR v2026.10.15 Δ
QRgen 3.1.0 22,056 22,060 -4 (-0.0%)
chrono 0.3.1 15,586 15,586 ±0
zippy 0.10.19 15,200 15,200 ±0
chroma 1.0.0 14,714 14,714 ±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 113 113 ±0

Display drivers (C) — 64,958

Part This PR v2026.10.15 Δ
(everything else) 21,278 21,282 -4 (-0.0%)
frameos_panel_table.c.obj 6,685 6,685 ±0
EPD_13in3e.c.obj 1,796 1,796 ±0
EPD_3in7.c.obj 1,564 1,564 ±0
EPD_5in79.c.obj 1,481 1,481 ±0
DEV_Debug.c.obj 1,325 1,317 +8 (+0.6%)
EPD_2in7.c.obj 1,292 1,292 ±0
EPD_4in2.c.obj 1,262 1,262 ±0
EPD_7in3e.c.obj 1,208 1,208 ±0
EPD_4in2_V2.c.obj 1,184 1,184 ±0
DEV_Config_esp.c.obj 1,180 1,180 ±0
EPD_4in26.c.obj 1,123 1,123 ±0
29 smaller rows 23,580 23,572 +8 (+0.0%)

Crypto (monocypher) — 35,141

Part This PR v2026.10.15 Δ
monocypher.c.obj 31,916 31,916 ±0
monocypher-ed25519.c.obj 3,225 3,225 ±0
Biggest movers (per object)
Object Δ This PR Baseline
frameos/input_state.nim +4,947 4,947 —
pkgs/QRgen-3.1.0/QRgen/renderer.nim -3,780 — 3,780
frameos/js_runtime/app_runtime.nim +938 73,573 72,635
esp_efuse_utility.c.obj -106 134,982 135,088
fos_client.c.obj +84 12,350 12,266
frameos/interpreter.nim +52 100,316 100,264
frameos_nim_glue.c.obj +44 10,679 10,635
frameos/js_runtime/runtime.nim +36 31,722 31,686
apps/render/split/app_loader.nim -36 4,666 4,702
frameos/single_scene_host.nim -25 6,687 6,712
pkgs/pixie-6.1.0/pixie/fileformats/bmp.nim +20 8,278 8,258
apps/render/calendar/app_loader.nim +12 7,069 7,057
tcp.c.obj -12 5,068 5,080
quickjs.c.obj +8 287,854 287,846
nim/lib/system.nim +8 48,465 48,457
Largest 30 objects
Object Subsystem This PR v2026.10.15 Δ
quickjs.c.obj QuickJS 287,854 287,846 +8 (+0.0%)
assets/fonts.nim Embedded font 146,362 146,362 ±0
esp_efuse_utility.c.obj String pool (attributed to efuse) 134,982 135,088 -106 (-0.1%)
frameos/interpreter.nim FrameOS core (Nim) 100,316 100,264 +52 (+0.1%)
frameos/js_runtime/app_runtime.nim FrameOS core (Nim) 73,573 72,635 +938 (+1.3%)
nim/lib/pure/collections/tables.nim Nim stdlib 72,312 72,312 ±0
pkgs/pixie-6.1.0/pixie/fontformats/opentype.nim pixie 55,460 55,460 ±0
libunicode.c.obj QuickJS 50,178 50,178 ±0
nim/lib/system.nim Nim stdlib 48,465 48,457 +8 (+0.0%)
apps/data/icalJson/ical.nim FrameOS apps (Nim) 45,020 45,016 +4 (+0.0%)
pkgs/pixie-6.1.0/pixie/fileformats/webp.nim pixie 37,493 37,493 ±0
pkgs/pixie-6.1.0/pixie/fileformats/svg.nim pixie 36,983 36,983 ±0
frameos/utils/image.nim FrameOS core (Nim) 35,942 35,942 ±0
pkgs/pixie-6.1.0/pixie/paths.nim pixie 32,875 32,875 ±0
monocypher.c.obj Crypto (monocypher) 31,916 31,916 ±0
frameos/js_runtime/runtime.nim FrameOS core (Nim) 31,722 31,686 +36 (+0.1%)
pkgs/pixie-6.1.0/pixie/fileformats/jpeg.nim pixie 29,436 29,436 ±0
apps/render/calendar/app.nim FrameOS apps (Nim) 23,817 23,817 ±0
fos_http.c.obj fos_* firmware shell (C) 22,185 22,185 ±0
pkgs/pixie-6.1.0/pixie/fileformats/png.nim pixie 22,101 22,101 ±0
fos_cloud.c.obj fos_* firmware shell (C) 19,258 19,258 ±0
embedded_main.nim FrameOS core (Nim) 19,252 19,252 ±0
frameos/utils/status_screen.nim FrameOS core (Nim) 17,939 17,943 -4 (-0.0%)
x509_crt_bundle.S.obj mbedTLS + certificates 17,928 17,928 ±0
embedded_runtime.nim FrameOS core (Nim) 17,830 17,834 -4 (-0.0%)
nim/lib/pure/unicode.nim Nim stdlib 17,405 17,405 ±0
frameos/utils/asset_colors.nim FrameOS core (Nim) 17,333 17,333 ±0
nim/lib/pure/times.nim Nim stdlib 16,888 16,888 ±0
lib/tz.nim FrameOS core (Nim) 16,722 16,722 ±0
pkgs/pixie-6.1.0/pixie/images.nim pixie 15,779 15,779 ±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.

FrameOS Bot and others added 2 commits October 16, 2026 12:54
…press kind

The ESP32 button task needed the level settled for 80 ms before it took an
edge, which lost any tap shorter than that and the second tap of a quick
double. It now takes the first change as the edge and locks bounce out for
30 ms (fos_button_timing.c, pure and host tested). A Pi's lgpio debounce
had the same flaw at 100 ms; it is 30 ms now.

`buttons.repeatMs` is 1000, counted in whole seconds from the press, so a
`repeat` is the buzzer's per-second tick and one more `heldSeconds`; a late
tick catches up with one repeat, not a burst. The contract's timing numbers
are emitted into fos_events_gen.h so the C side mirrors them.

The buzzer sounds each press kind when the button task's timing (the
dispatcher's rules on the same stamps) says it happened: the press pair is
unchanged, a double press plays two rising notes on its second tap, a long
press a short note falling an octave, each repeat the 7 ms tick. The buzzer
gains a small note-sequence player for that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mariusandra mariusandra changed the title Buttons: edges carry when they were seen, holds are measured between edges, doublePress Buttons: edges carry when they were seen, 30 ms debounce, repeat is the 1 s tick, a sound per press kind Oct 16, 2026
FrameOS Bot and others added 2 commits October 16, 2026 14:20
…nce clear

The hardware layer is what the line did, delivered the moment the driver
reports it: `down`, and `up` with `durationMs`. The button layer is what
the person meant, and each press is exactly one of: `press` (a short press,
350 ms after its up when no second down came), `doublePress` (the second
down inside that window; no press, no long press, a third down starts
over) or `longPress` (at every whole `longPressMs` held, now 1000: 1 s,
2 s, ..., `heldSeconds` counting them). `repeat` and `release` are gone;
`longPressMs` 500 and `repeatMs` 1000 become one `longPressMs` of 1000, so
a hold no longer chirps at half a second. A listener with no action filter
still hears `press` only.

Drivers send `down` / `up` (ESP32, Pi gpioButton, the preview); the
`level` decides, so an older driver's press / release still works. A
`button` that is not an edge (HTTP, cloud, schedule, scene) is delivered
as said.

Every event sounds the instant it happens on the ESP32: click on down,
lower click on up, two rising notes for a double press, the falling pair
at each long press; a press adds none. The per-second tick is gone.

A press is decided after a window and a long press each second, and only
a tick delivers them, so hosts tick while a key is busy: the ESP32 waits
in 50 ms slices and waits (up to 10 s) before deep sleep, the preview
ticks every 50 ms while a button is busy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mariusandra mariusandra changed the title Buttons: edges carry when they were seen, 30 ms debounce, repeat is the 1 s tick, a sound per press kind Buttons in two layers: down/up at once, press/doublePress/longPress once clear; 30 ms debounce; a sound per event Oct 16, 2026
FrameOS Bot and others added 8 commits October 16, 2026 14:56
A press held at least `longPressMs` (1000) is a `longPress` delivered once,
right after its `up`, with the same `durationMs` and `heldSeconds`.
Nothing is delivered while a key is held: no long press at 1 s, none each
second after. The tick now only turns a tap whose double-press window
closed into a `press`.

On the ESP32 the long press's sound plays in place of the up click, as the
double press's notes take the down click's place; a held key is silent.
fos_button_timing drops its per-second hold logic: its release says
whether the up is a long press.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A double press's second down plays two rising notes (1.5x, 2x the tone),
and its release now plays one more above them (2.5x, 12 ms) instead of
the press's lower up click. A double press rises to its end; a tap falls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a key is held the buzzer sounds the long-press pair at every whole
longPressMs (1 s, 2 s, ...), and its release is the regular up click. The
scene's `longPress` still comes once, with the `up`: the sounds count the
hold, they are not events.

A double press is the plain down click twice; only its second release
differs, a 14 ms click at 1.5x the press pitch instead of 0.7x, so the
pair ends on a rise where a tap falls. The two-note double sound is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_single_scene_host sent a raw `level: 0` edge and expected the scene's
press listener to run; with the two layers that edge is a `down`, and the
`press` waits for the release. The case is the battery ESP32's wake press,
so it now sends one (`wake: true`, a press at once); the abort case sends a
press as said.

The status screen's "Last button" line now follows the hardware `down`
rather than the button layer's `press`: it answers "I pressed it and
nothing happened", so every touch counts at once, a double or a long
press too.

ESP32 size against main went from +4,208 to +1,936 bytes:
- one `addButton` builds every button payload, where seven call sites
  each built their own (input_state +2,435 -> +942);
- a waiting tap is the same `ButtonKey` record as a held key, marked not
  down, instead of a second type with its own seq and string hooks
  (system.nim +647 -> +8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A double tap now sounds 2000, 1400, 3000, 1400 Hz at the default tone: the
second down clicks at 1.5x the press pitch instead of 1x, and every up is
the same lower click (0.7x), a double press's included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Until now the wake press was replayed as a plain `press` with no hold, so
holding the key that woke the frame could not be a long press, and it could
not start a double press. Now the button task's first look queues its edges
like any other press's: a key still down began at the reset (atMs 0), so
its hold counts from there, it duns each second and its up is a
`longPress`; a key already up was a tap, its down and up stamped when first
seen, so a second tap within doublePressMs pairs into a `doublePress`. The
button-layer event a wake starts carries `wake: true`. The queue item drops
its duration field (12 -> 8 bytes), which only the wake release used.

The PSRAM memory test is off at boot: it took 0.41 s of every boot on the
E1002, deep-sleep wakes included. The button task now starts 0.75 s after
the reset instead of 1.18 s; the rest is the bootloader checking the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mariusandra
mariusandra merged commit b65672b into main Oct 16, 2026
25 checks passed
@mariusandra
mariusandra deleted the esp32-button-timing branch October 16, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant