Skip to content

Commit c040f85

Browse files
committed
Merge board-detect: firmware-identifying beacon, version-aware install, multi-board builds (v0.6.0)
2 parents ba34f12 + d22da2d commit c040f85

18 files changed

Lines changed: 380 additions & 125 deletions

.github/workflows/build.yml

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
path: flipper/flytrap/dist/flytrap.fap
3737

3838
esp32-firmware:
39-
name: Build ESP32-S2 firmware
39+
name: Build ESP32 firmware (S2, WROOM, C5)
4040
runs-on: ubuntu-latest
4141
steps:
4242
- uses: actions/checkout@v4
@@ -45,23 +45,52 @@ jobs:
4545
mkdir -p "$HOME/bin"
4646
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR="$HOME/bin" sh
4747
echo "$HOME/bin" >> "$GITHUB_PATH"
48-
- name: Install esp32 core + async libs
48+
- name: Fetch async libs (maintained ESP32Async forks)
49+
# The unmaintained me-no-dev forks don't build on the 3.x core the C5 needs.
50+
# The ESP32Async forks build on both the pinned 2.0.17 (S2/WROOM) and 3.x (C5).
51+
run: |
52+
mkdir -p esp32/libs
53+
git clone --depth 1 --branch v3.3.2 https://github.com/ESP32Async/AsyncTCP.git esp32/libs/AsyncTCP
54+
git clone --depth 1 --branch v3.6.0 https://github.com/ESP32Async/ESPAsyncWebServer.git esp32/libs/ESPAsyncWebServer
55+
- name: Install esp32 core 2.0.17 (S2/WROOM)
4956
run: |
5057
arduino-cli config init --overwrite
5158
arduino-cli config set board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
5259
arduino-cli core update-index
5360
arduino-cli core install esp32:esp32@2.0.17
54-
mkdir -p esp32/libs
55-
git clone --depth 1 https://github.com/me-no-dev/AsyncTCP.git esp32/libs/AsyncTCP
56-
git clone --depth 1 https://github.com/me-no-dev/ESPAsyncWebServer.git esp32/libs/ESPAsyncWebServer
57-
- name: Compile firmware
61+
- name: Compile S2 + WROOM (core 2.0.17)
62+
# These stay pinned to 2.0.17 (different WiFi/USB behaviour on 3.x). The C5
63+
# install below REPLACES this core, so these must build first. The S2 images
64+
# go to build/ (flat) because build-fap.sh and the fap bundle that exact path.
5865
run: |
5966
arduino-cli compile \
6067
--fqbn esp32:esp32:esp32s2:PartitionScheme=huge_app \
6168
--libraries esp32/libs \
6269
--output-dir esp32/flytrap-fw/build \
6370
esp32/flytrap-fw
71+
arduino-cli compile \
72+
--fqbn esp32:esp32:esp32 \
73+
--libraries esp32/libs \
74+
--output-dir esp32/flytrap-fw/build-wroom \
75+
esp32/flytrap-fw
76+
- name: Compile C5 (core 3.3.11)
77+
# The C5 only exists in the 3.x line. Installing it REPLACES 2.0.17, so this
78+
# runs after the S2/WROOM builds. This is the guard that keeps the firmware
79+
# 3.x-clean (see onStaIp's ESP_ARDUINO_VERSION_MAJOR branch). CDCOnBoot stays
80+
# default so Serial is UART0 -- the pins the Flipper talks to.
81+
run: |
82+
arduino-cli core install esp32:esp32@3.3.11
83+
arduino-cli compile \
84+
--fqbn esp32:esp32:esp32c5:PartitionScheme=huge_app,CDCOnBoot=default \
85+
--libraries esp32/libs \
86+
--output-dir esp32/flytrap-fw/build-c5 \
87+
esp32/flytrap-fw
6488
- uses: actions/upload-artifact@v4
89+
# Only the S2 images are consumed downstream: the flipper-app job unpacks this
90+
# artifact straight into esp32/flytrap-fw/build/ and build-fap.sh bundles the S2
91+
# images from there. The WROOM/C5 compiles above are 3.x/board build guards --
92+
# release.yml is what ships their images. Keep this S2-only so the artifact root
93+
# stays esp32/flytrap-fw/build (a multi-dir upload would nest it and break the fap).
6594
with:
6695
name: flytrap-firmware
6796
path: esp32/flytrap-fw/build/*.bin

.github/workflows/release.yml

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ jobs:
1717
- uses: actions/checkout@v4
1818

1919
# ESP build runs first, on the runner's system Python (which has pyserial
20-
# for the core's esptool). A setup-python before this would shadow it and
21-
# break esptool with "No module named 'serial'".
22-
- name: Build ESP32-S2 firmware
20+
# for the core's esptool elf2image step). A setup-python before this would
21+
# shadow it and break arduino-cli's image step with "No module named 'serial'".
22+
# tools/merge-image.py is stdlib-only, so the merges below are unaffected.
23+
- name: Build ESP32 firmware (S2, WROOM, C5)
2324
run: |
2425
mkdir -p "$HOME/bin"
2526
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR="$HOME/bin" sh
@@ -28,14 +29,53 @@ jobs:
2829
arduino-cli config set board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
2930
arduino-cli core update-index
3031
arduino-cli core install esp32:esp32@2.0.17
32+
# Maintained ESP32Async forks build on both the pinned 2.0.17 (S2/WROOM)
33+
# and the 3.x core the C5 needs; the me-no-dev forks don't build on 3.x.
3134
mkdir -p esp32/libs
32-
git clone --depth 1 https://github.com/me-no-dev/AsyncTCP.git esp32/libs/AsyncTCP
33-
git clone --depth 1 https://github.com/me-no-dev/ESPAsyncWebServer.git esp32/libs/ESPAsyncWebServer
35+
git clone --depth 1 --branch v3.3.2 https://github.com/ESP32Async/AsyncTCP.git esp32/libs/AsyncTCP
36+
git clone --depth 1 --branch v3.6.0 https://github.com/ESP32Async/ESPAsyncWebServer.git esp32/libs/ESPAsyncWebServer
37+
38+
# S2 (core 2.0.17): the fap-bundled board. Build into build/ (flat) so the
39+
# bundle step below and build-fap.sh find it, then merge into one 0x0 image.
3440
arduino-cli compile \
3541
--fqbn esp32:esp32:esp32s2:PartitionScheme=huge_app \
3642
--libraries esp32/libs \
3743
--output-dir esp32/flytrap-fw/build \
3844
esp32/flytrap-fw
45+
BOOT_APP0="$HOME/.arduino15/packages/esp32/hardware/esp32/2.0.17/tools/partitions/boot_app0.bin"
46+
python3 tools/merge-image.py esp32/flytrap-fw/build/flytrap-s2-merged.bin \
47+
0x1000 esp32/flytrap-fw/build/flytrap-fw.ino.bootloader.bin \
48+
0x8000 esp32/flytrap-fw/build/flytrap-fw.ino.partitions.bin \
49+
0xe000 "$BOOT_APP0" \
50+
0x10000 esp32/flytrap-fw/build/flytrap-fw.ino.bin
51+
52+
# WROOM (core 2.0.17, plain esp32 FQBN), merged the same way.
53+
arduino-cli compile \
54+
--fqbn esp32:esp32:esp32 \
55+
--libraries esp32/libs \
56+
--output-dir esp32/flytrap-fw/build-wroom \
57+
esp32/flytrap-fw
58+
python3 tools/merge-image.py esp32/flytrap-fw/build-wroom/flytrap-wroom-merged.bin \
59+
0x1000 esp32/flytrap-fw/build-wroom/flytrap-fw.ino.bootloader.bin \
60+
0x8000 esp32/flytrap-fw/build-wroom/flytrap-fw.ino.partitions.bin \
61+
0xe000 "$BOOT_APP0" \
62+
0x10000 esp32/flytrap-fw/build-wroom/flytrap-fw.ino.bin
63+
64+
# C5 (core 3.3.11): the chip does not exist in 2.0.17. Installing the 3.x
65+
# core REPLACES 2.0.17 (and its esptool), so this runs last. NOTE the C5
66+
# bootloader offset is 0x2000, not the 0x1000 the other boards use.
67+
arduino-cli core install esp32:esp32@3.3.11
68+
C5_BOOT_APP0="$HOME/.arduino15/packages/esp32/hardware/esp32/3.3.11/tools/partitions/boot_app0.bin"
69+
arduino-cli compile \
70+
--fqbn esp32:esp32:esp32c5:PartitionScheme=huge_app,CDCOnBoot=default \
71+
--libraries esp32/libs \
72+
--output-dir esp32/flytrap-fw/build-c5 \
73+
esp32/flytrap-fw
74+
python3 tools/merge-image.py esp32/flytrap-fw/build-c5/flytrap-c5-merged.bin \
75+
0x2000 esp32/flytrap-fw/build-c5/flytrap-fw.ino.bootloader.bin \
76+
0x8000 esp32/flytrap-fw/build-c5/flytrap-fw.ino.partitions.bin \
77+
0xe000 "$C5_BOOT_APP0" \
78+
0x10000 esp32/flytrap-fw/build-c5/flytrap-fw.ino.bin
3979
4080
- uses: actions/setup-python@v5
4181
with:
@@ -64,14 +104,19 @@ jobs:
64104
TAG: ${{ github.ref_name }}
65105
REPO: ${{ github.repository }}
66106
run: |
67-
# Two artifacts: the fap (one-step install; bundles portal + firmware)
68-
# and the firmware bundle zip (all images + boot_app0 + flash.txt
69-
# manifest) for computer/esptool flashing or manual SD flasher setup.
70-
# The loose .bin images are redundant with the zip (and incomplete —
71-
# they omit boot_app0), so they're no longer attached.
107+
# Attachments:
108+
# - the fap (one-step install; bundles the portal + the S2 firmware)
109+
# - the S2 firmware bundle zip (images + boot_app0 + flash.txt manifest)
110+
# for the on-device flasher / manual SD setup
111+
# - one merged .bin per board (S2/WROOM/C5), each flashable at 0x0 with
112+
# esptool from a computer: what WROOM and C5 owners use, and what
113+
# issue #1 asked for.
72114
gh release create "$TAG" \
73115
--repo "$REPO" \
74116
--title "Flytrap $TAG" \
75117
--generate-notes \
76118
flipper/flytrap/dist/flytrap.fap \
77-
flytrap-firmware-bundle.zip
119+
flytrap-firmware-bundle.zip \
120+
esp32/flytrap-fw/build/flytrap-s2-merged.bin \
121+
esp32/flytrap-fw/build-wroom/flytrap-wroom-merged.bin \
122+
esp32/flytrap-fw/build-c5/flytrap-c5-merged.bin

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ build/
1313
*.o
1414
*.a
1515
esp32/**/build/
16+
# per-board release build dirs (build-wroom, build-c5)
17+
esp32/**/build-*/
1618
# ...but the firmware-bundle boot_app0 is a shipped asset, not a build artifact
1719
!esp32/flytrap-fw/boot_app0.bin
1820

CHANGELOG.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,51 @@
22

33
All notable changes to Flytrap are documented here.
44

5+
## [0.6.0] — 2026-09-11
6+
7+
Board detection now keys off a firmware-identifying beacon, and the flasher UI is
8+
simplified around the embedded firmware.
9+
10+
### Protocol
11+
- The liveness beacon is now **`PING FTRP <ver>`** (was a bare `PING`). The `FTRP`
12+
magic identifies *Flytrap* firmware so the app won't adopt a board flashed with
13+
something else, and `<ver>` is the wire-protocol version. Detection now **parses**
14+
the beacon (magic-matched) instead of treating "any bytes received" as a board —
15+
the latter let foreign firmware slip through. **Requires reflashing the ESP**
16+
(Start Portal will prompt for it automatically).
17+
18+
### Flipper app
19+
- **Version-aware install/update.** Start Portal has three outcomes: no magic beacon
20+
*"Firmware needed"* (install); magic but an older `<ver>`*"Update firmware?"*;
21+
current → it proceeds. The prompt is phrased around the **firmware**, not the
22+
board's presence (an off/absent/foreign board are indistinguishable).
23+
- **Handshake watchdog.** A start that detects the board but never reaches
24+
broadcasting within ~8 s drops back to the install prompt instead of hanging on
25+
"Starting portal…".
26+
- **"Reinstall firmware"** replaces the old **Flash Firmware** menu item — it flashes
27+
the firmware bundled in the fap directly (no SD file picker, so no stale-folder
28+
clutter). Start Portal's auto-install/update uses the same bundled image.
29+
- `deploy-to-flipper.py` no longer pushes a firmware bundle to the SD (it rides
30+
inside the fap and extracts to `apps_assets`).
31+
- **Flasher tells you to reset.** The "Flashed!" screen now says *"Tap RESET on the
32+
board to run it, then Continue"* — the board stays in the download bootloader
33+
after flashing (the app can't drive the S2's reset line), so it needs a manual
34+
RESET to boot the new firmware. (Was mislabeled "Board rebooting…".)
35+
36+
### Boards
37+
- **WROOM and C5 builds** alongside the S2. Releases now attach one merged image per
38+
board (`flytrap-s2-merged.bin`, `flytrap-wroom-merged.bin`, `flytrap-c5-merged.bin`),
39+
each flashable at `0x0` with `esptool` from a computer, so nothing has to be compiled
40+
to get running. The fap still bundles the S2 image alone, so the on-device
41+
Install/Update path stays S2-only.
42+
- The firmware is **3.x-core clean**. `onStaIp()` resolved a station's MAC through
43+
`esp_netif_get_sta_list()`, which esp-idf 5 removed, so the sketch would not compile
44+
on the 3.x core at all. On 3.x the assign event carries the MAC directly, so the two
45+
paths now sit behind an `ESP_ARDUINO_VERSION_MAJOR` check. The C5 needs 3.x (the chip
46+
does not exist in 2.0.17); S2 and WROOM stay pinned to 2.0.17. Fixes #1.
47+
- Async libraries moved to the maintained **ESP32Async** forks (AsyncTCP v3.3.2,
48+
ESPAsyncWebServer v3.6.0). The unmaintained me-no-dev forks do not build on 3.x.
49+
550
## [0.5.0] — 2026-07-17
651

752
Flash the ESP firmware from the Flipper — no computer.

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@ If that's not your use case, this isn't the project for you.
4646
everything also logged to `capture_<N>.txt` on the SD card.
4747
- **Live clients** list → detail (MAC, IP, joined time). Joins and leaves update the
4848
count in real time, so a device that disconnects drops off instead of lingering.
49-
- **Flash the ESP from the Flipper** — no computer. **Flash Firmware** picks a
50-
firmware bundle off the SD, auto-detects the board in download mode, and writes it
51-
over the GPIO UART (vendored [esp-serial-flasher](https://github.com/espressif/esp-serial-flasher)).
49+
- **Flash the ESP from the Flipper** — no computer. The firmware is bundled in the
50+
fap; **Start Portal** auto-detects the board and offers to **Install** (or
51+
**Update**, when it's behind) and continues once flashed, and a **Reinstall
52+
firmware** menu item reflashes on demand. It auto-detects the board in download
53+
mode and writes over the GPIO UART (vendored [esp-serial-flasher](https://github.com/espressif/esp-serial-flasher)).
5254
- **Capture alerts** (haptic / beep / LED) with a **Settings** screen to toggle them.
5355
- A **Console** view (from the menu during a session) showing the raw serial protocol live.
5456
- Lightweight enough to run reliably on the SD-less ESP32-S2 dev board.
@@ -59,6 +61,10 @@ If that's not your use case, this isn't the project for you.
5961
`ufbt` SDK).
6062
- **Official Flipper WiFi Dev Board (ESP32-S2)** — or any generic ESP32-S2. It mounts on the
6163
Flipper's GPIO header, which wires the two together over UART.
64+
- **ESP32-WROOM and ESP32-C5** also run the firmware; releases attach a ready-made image
65+
for each. Only the S2 image is bundled in the fap, so those two are flashed once from a
66+
computer (see [tools/README.md](tools/README.md)), after which the Flipper drives them
67+
the same way.
6268

6369
## Install
6470

docs/PROTOCOL.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ All line-oriented. The Flipper parser matches on the leading token.
3737
| `BYE mac=<AA:BB:...>` | A station left the AP — the Flipper drops it from the live client list. |
3838
| `IP mac=<AA:BB:...> ip=<ip>` | DHCP assigned a station its IP (MAC resolved from the soft-AP table). Falls back to `IP ip=<ip>` when the MAC can't be resolved; the Flipper then pairs it to the most recent client without an IP. |
3939
| `CRED ip=<ip>&<field>=<val>&...` | A form was submitted — the client IP plus every submitted field, url-encoded. One line per submission. |
40-
| `PING` | Liveness beacon, emitted ~every 2s. The Flipper uses it to detect an unplugged board (no `PING`/traffic for 5s ⇒ link lost); it's dropped from the console so it doesn't spam. |
40+
| `PING FTRP <ver>` | Liveness + identity beacon, emitted ~every 2s. The magic `FTRP` marks the board as running Flytrap firmware (detection keys off *this*, not "any bytes", so a board flashed with something else isn't adopted), and `<ver>` is the wire-protocol version — the Flipper prompts an update when it's behind `FLYTRAP_FW_VERSION`. Also drives unplug detection (no beacon/traffic for 5s ⇒ link lost). Dropped from the console so it doesn't spam. |
4141

4242
## Handshake
4343

@@ -57,15 +57,20 @@ Flipper ESP
5757
| <---- IP mac=... ip=...(DHCP leases it an address)
5858
| <---- CRED ip=...&... (a form is submitted)
5959
| <---- BYE mac=... (the phone leaves)
60-
| <---- PING (~every 2s, all the while)
60+
| <---- PING FTRP <ver> (~every 2s, all the while)
6161
| |
6262
| stop\n / reset\n ----> | (tear down / reboot)
6363
```
6464

65-
If the ESP emits `STATUS boot` mid-session (it rebooted), the Flipper re-sends
66-
`sethtml` automatically to recover. If `PING` (and all other traffic) stops for
67-
5s, the Flipper flags the link as lost and the dashboard shows **Board
68-
disconnected** until the board returns.
65+
Before the handshake the Flipper first **detects** the board by waiting for a
66+
magic `PING FTRP` beacon: none ⇒ *"Firmware needed"* (install prompt); magic but
67+
`<ver>` behind ⇒ *"Update firmware?"*; current ⇒ it proceeds. If the ESP emits
68+
`STATUS boot` mid-session (it rebooted), the Flipper re-sends `sethtml`
69+
automatically to recover. If the beacon (and all other traffic) stops for 5s, the
70+
Flipper flags the link as lost and the dashboard shows **Board disconnected**
71+
until the board returns. A start that detects the board but never reaches
72+
`portal_up` within ~8s is treated as a stalled handshake and drops back to the
73+
install prompt.
6974

7075
## Notes
7176

esp32/flytrap-fw/flytrap-fw.ino

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
// BYE mac=<mac> (a station left the AP)
1212
// IP mac=<mac> ip=<ip> (DHCP assigned a station its IP)
1313
// CRED <urlencoded> (all submitted form fields, one line per submission)
14-
// PING (~every 2s liveness beacon so the Flipper detects unplug)
14+
// PING FTRP <ver> (~every 2s liveness beacon; the magic "FTRP" identifies THIS
15+
// firmware to the Flipper and <ver> drives its update prompt)
1516
//
1617
// Authorized testing only.
1718

@@ -25,6 +26,12 @@
2526
#define MAX_HTML_SIZE 48000
2627
#define MAX_SSID 32
2728

29+
// Beacon identity: the magic marks a PING as coming from THIS firmware (so the
30+
// Flipper won't adopt some other board), and the version drives its update
31+
// prompt. KEEP IN SYNC with flipper/flytrap/flytrap_i.h.
32+
#define FW_MAGIC "FTRP"
33+
#define FW_VERSION 2
34+
2835
static char index_html[MAX_HTML_SIZE + 1] =
2936
"<html><body><h1>Flytrap</h1><p>Set a portal from the Flipper.</p></body></html>";
3037
static size_t index_html_len = 0;
@@ -129,15 +136,26 @@ static void onStaDisconnect(WiFiEvent_t event, WiFiEventInfo_t info) {
129136
emitLine(String("BYE mac=") + mac); // a station left the AP
130137
}
131138

132-
// DHCP handed a station an IP. The event only carries the IP (no MAC on this
133-
// IDF), so look the MAC up in the soft-AP station table by matching the IP, and
134-
// report the pair so the Flipper can attach the IP to the right client. Falls
135-
// back to an IP-only line if the table lookup can't resolve it.
139+
// DHCP handed a station an IP. Report the IP, and the MAC when we can resolve it,
140+
// so the Flipper can attach the IP to the right client. How the MAC is obtained
141+
// differs by core version (see the #if below), so this stays portable across the
142+
// 2.0.x core the S2/WROOM build against and the 3.x core the C5 needs.
136143
static void onStaIp(WiFiEvent_t event, WiFiEventInfo_t info) {
137144
(void)event;
138145
uint32_t evip = info.wifi_ap_staipassigned.ip.addr;
139146
IPAddress ip(evip);
140147

148+
#if ESP_ARDUINO_VERSION_MAJOR >= 3
149+
// esp-idf 5+ (Arduino core 3.x, e.g. the C5): the assign event carries the
150+
// station's MAC directly, so report the pair with no table lookup. The old
151+
// esp_netif_get_sta_list() path this used on 2.0.x was removed in the 3.x core.
152+
char mac[18];
153+
formatMac(info.wifi_ap_staipassigned.mac, mac);
154+
emitLine(String("IP mac=") + mac + " ip=" + ip.toString());
155+
#else
156+
// esp-idf 4.x (Arduino core 2.0.x, the S2/WROOM): the staipassigned event has
157+
// no MAC field, so look the MAC up in the soft-AP station table by matching the
158+
// IP. Falls back to an IP-only line if the table lookup can't resolve it.
141159
wifi_sta_list_t wifi_list;
142160
esp_netif_sta_list_t netif_list;
143161
if(esp_wifi_ap_get_sta_list(&wifi_list) == ESP_OK &&
@@ -152,6 +170,7 @@ static void onStaIp(WiFiEvent_t event, WiFiEventInfo_t info) {
152170
}
153171
}
154172
emitLine(String("IP ip=") + ip.toString()); // MAC unknown; Flipper pairs by recency
173+
#endif
155174
}
156175

157176
static void startPortal() {
@@ -261,6 +280,6 @@ void loop() {
261280
uint32_t now = millis();
262281
if(now - lastPing >= 2000) {
263282
lastPing = now;
264-
emitLine("PING");
283+
emitLine(String("PING " FW_MAGIC " ") + FW_VERSION);
265284
}
266285
}

0 commit comments

Comments
 (0)