LakeShark · Volume 9

LakeShark Volume 9 — Flashing, Building & the C6 Dance

Web installer, two-slot OTA, the ESP-IDF source build, and the one-time C6 UART reflash


9.1 About this Volume

This is the operations volume: how to get LakeShark onto hardware, keep it updated, build it from source, and recover the one component that cannot be flashed over USB. Three audiences are served in order of effort: someone who wants a running board in five minutes (the web installer, §2), someone who wants to track the near-daily releases (OTA, §4), and someone who wants to build and modify the firmware themselves (ESP-IDF, §8).

One section — the C6 co-processor reflash (§7) — is genuinely fiddly and deserves its reputation. It is the only step that needs an external USB-UART adapter, a button dance, and correct knowledge of two different sets of flash offsets. It is also a one-time operation for most owners, after which the wireless half of the board simply works. The volume treats it in full because getting it wrong leaves Wi-Fi/BT (and therefore OTA, LINK, and DETECTOR’s beacon listening) dead.

All offsets, commands and version numbers here are drawn from the repo’s docs/TDP4_FIRST_FLASH.md and docs/HACKRF_BRINGUP.md at the v2.15.0 (2026-10-15) state. Version churn is real — see Vol 10 — so every claim is dated and the repo is the tiebreaker over anything in this volume. Vol 2 carries the canonical “where this firmware runs” supported-hardware table; the four build targets in §8 are the authoritative “what can I flash this on” list in terms of the firmware’s own build profiles.

Warning. Two power pitfalls (§6) can disrupt a flashing session or reboot- loop the board. Read §6 before connecting anything. The short version: flash only over the right-hand USB-C, and never back-feed 5 V from the MIX-RF add-on.


9.2 The easy path: the terminalbay.com web installer

For almost everyone, flashing LakeShark is a browser operation. The author hosts a WebSerial installer that writes (and later updates) the board over USB with no toolchain installed.

 Web installer
 URL: terminalbay.com/?m=lakeshark&board=tdp4
 Browser: Chrome or Edge (WebSerial required; Firefox/Safari will not work)
 Port: the RIGHT-hand USB-C, labelled "P4U" (CH343 USB-UART)

The flow:

  1. Open the URL in Chrome or Edge.
  2. Connect the board by its right-hand USB-C port (“P4U”) — this is the P4’s flash/serial path through the CH343 bridge. The left-hand USB-C is charge- only and will not flash (§6).
  3. Let the WebSerial installer enumerate the port and write the images. It can also update an already-flashed board over USB, which is the fallback when OTA (§4) is not an option.

That is the whole happy path for the display. It does not flash the C6 co-processor — the C6 is on no USB port and needs the separate procedure in §7 — but a factory board already has some C6 firmware; the C6 reflash is only needed to put the ESP-Hosted build LakeShark requires in place of incompatible factory firmware.

First boot, in brief. After a successful flash the board comes up at the launcher (ls-home.png’s layout). The two things worth doing immediately are connecting Wi-Fi in LINK (which both proves the C6 link is up and enables OTA, §4) and inserting a formatted microSD card (the bulk store, §5). If LINK reports no wireless at all, the C6 is on incompatible firmware and the §7 reflash is the fix — this is the single most common “it flashed but half of it does not work” outcome, and it is expected on a fresh factory board.

Tip. WebSerial is a Chromium feature. If the installer never sees a port, the browser is the first suspect (use Chrome/Edge), the cable is the second (a charge-only cable presents no data lines), and the port is the third (must be the right-hand P4U, not the left charge port).

A quick triage table for the web-installer step, since three of the four common failures are not the firmware’s fault:

Table 1 — failures are not the firmware's fault

SymptomMost likely causeFix
No serial port offeredNon-Chromium browserUse Chrome or Edge
Port offered, no deviceCharge-only cableUse a data cable
Enumerates then dropsWrong USB-C portUse the right-hand P4U port
Flashes, then no wirelessC6 on factory firmwareDo the C6 reflash (§7)

The web installer is also the designated recovery tool: a board that will not boot after a bad from-source flash, or one that needs to migrate from a pre-v2.9.0 layout, is recovered by a full USB install through the same page. There is no separate “unbrick” procedure for the P4 — the P4 is always reachable over P4U, so a full reinstall is the recovery. (The C6 is the one part that can need the UART-adapter recovery of §7; the P4 never does.)


9.3 Flash offsets and what lives where

A full install from source (or a manual recovery) writes four images to the P4’s internal flash at fixed offsets. These are the P4 offsets; the C6 uses a different set (§7), and confusing the two is the classic reflash mistake.

Table 2 — 3. Flash offsets and what lives where

ImageOffsetRole
bootloader/bootloader.bin0x2000Second-stage bootloader
partition_table/partition-table.bin0x8000Partition map
lakeshark.bin0x10000The application
storage.bin0x910000On-board storage (settings, saved Wi-Fi)

From an ESP-IDF build tree, the full install is driven by the build’s own flasher args rather than by typing offsets by hand:

# Full install from a source build (writes all images + OTA data init)
idf.py -B build_tdp4 -D SDKCONFIG=build_tdp4/sdkconfig -p YOUR_PORT flash
# build_tdp4/flasher_args.json carries the offsets above + OTA-data init

Warning. A full install rewrites internal storage. Back up SD-card data and note your saved Wi-Fi before a full reinstall. A two-slot OTA update (§4) preserves settings and saved Wi-Fi; a from-scratch flash does not necessarily, and the storage partition at 0x910000 is the thing being overwritten.

Note the P4 bootloader lives at 0x2000 — remember this number, because the C6 bootloader is at 0x0, and §7 depends on keeping the two straight.


9.4 Two-slot signed OTA

LakeShark releases are near-daily, so updating matters, and since v2.9.0 the firmware updates itself over Wi-Fi with a fail-safe two-slot (A/B) scheme.

How it works:

  • The app partition is split into two slots. A new build is written to the inactive slot, verified, then booted. The old slot is retained.
  • Updates are signed. If a new build fails to boot, the board automatically reverts to the previous slot — a bad update cannot brick the device.
  • Because the running app can be in either slot, there is no fixed app offset for an update (unlike the one-shot install in §3). This is why OTA tooling must not assume 0x10000.

The operator flow:

 LINK → connect Wi-Fi (needs the C6 running ESP-Hosted, §7)
 HOME > SYSTEM > UPDATE → pull and apply the new build to the spare slot
 SET > DEVICE > Check for updates → Daily | Off

The two-slot design is worth appreciating as a reliability feature on a near-daily-release project. On a single-slot device, a bad update that fails to boot bricks the unit until it is re-flashed over USB; on LakeShark, the old slot is retained untouched and the signed new build is only committed once it boots, so a failed update is a silent no-op rather than a brick. That is precisely the safety margin a user needs to be comfortable running the Daily update cadence — the cost of staying current is near zero because the downside is bounded by the revert. It also means the “which slot is running” question has no fixed answer, which is why OTA tooling must never assume the app is at 0x10000 (that is the initial install offset only, §3).

The one prerequisite is the boundary condition worth flagging: boards on v2.8 or earlier do not have the two-slot layout, so they need one full USB or web install to lay down the new partition scheme before OTA is available. That first install rewrites internal storage but keeps settings and saved Wi-Fi. After it, every subsequent update can be OTA.

Table 3 — 4. Two-slot signed OTA

MechanismWhen to use
Web installer (§2)First flash; recovery; pre-v2.9.0 → v2.9.0+ migration
Two-slot OTAEvery routine update on v2.9.0+
From-source idf.py flash (§8)Developers / custom builds

The update cadence is set from the SET > DEVICE > Check for updates control, which offers Daily or Off. Given the near-daily release pace (Vol 10 §8), Daily is the way to stay current without manual checking; Off is the way to pin a known-good build and update deliberately. Either way the two-slot revert is the safety net — an update that does not boot falls back to the slot that did.

Note. OTA depends on Wi-Fi, Wi-Fi depends on the C6, and the C6 depends on the correct ESP-Hosted firmware (§7). A board whose C6 is on factory firmware cannot OTA — which is exactly the chicken-and-egg the C6 reflash resolves.


9.5 The SD card and its roles

A microSD card is LakeShark’s bulk store. The repo does not publish a size or format specification, but it names the roles the card plays, and they add up to “everything the internal flash is too small for”:

Table 4 — "everything the internal flash is too small for"

SD roleContent
Call / recording archiveP25 call archive, voice recordings
MusicMUSIC app media
Sub-GHz library.sub captures (Vol 8 §3)
NotesNOTES app Markdown files
C6 firmware imagesFor SD-based C6 updates after the first UART flash (§7)
DETECTOR rulesUser rules file (Vol 8 §7)
Map tilesOffline PMTiles (Vol 6)

The C6-images role is the one that saves future pain: once the C6 has been flashed over UART the first time (§7), later C6 updates can be applied from the SD card without the adapter and the button dance. So the SD card is not just a data store — it is the C6’s maintenance path after the initial bring-up.

The division of labour between internal flash and SD is the usual MCU pattern: the 16 MB internal flash holds the bootloader, two app slots and a small settings partition (§3), while everything large and user-generated — call archives, recordings, music, .sub captures, notes, map tiles — lives on SD. That means a full P4 reinstall (§3) loses settings/storage but leaves the SD-card content alone, and conversely reformatting the SD card loses captures and maps but not the firmware or its settings. Backing up the SD card before a from-scratch session is cheap insurance precisely because the two stores fail independently.

Note. Because no explicit format spec is published, a standard FAT/exFAT card of ordinary capacity is the safe assumption. Keep a backup of anything on the card before a full P4 reinstall (§3), since a from-scratch session is exactly when a card is most likely to be reformatted by mistake.


9.6 Power pitfalls — the part that bricks a session

Two power rules are documented as hardware hazards, not preferences. Both are easy to violate because the board has three USB connectors and an add-on with its own power output.

1. Flash only over the right-hand USB-C (P4U). The board has two USB-C ports. The right-hand one (“P4U”, CH343 bridge) carries flash and serial. The left-hand one is charge-only and presents no data path. A flashing session that will not enumerate is very often a cable plugged into the charge port.

2. Never back-feed 5 V from the MIX-RF add-on into the P4. The MIX-RF keyboard (Vol 8 §5) has a 5 V USB output. Routing it back into the P4 caused repeated power cycling (reboot loops) in testing. Power the P4 only from its own USB-C; let the add-on draw from the board, not push into it.

 T-Display P4 USB map
 ┌───────────────┐
 │ [L] USB-C │ charge only — will NOT flash
 │ [R] USB-C P4U│ flash + serial (CH343) ← use this
 │ USB-A host │ RTL-SDR dongle in here
 └───────────────┘
 MIX-RF 5V out ──╳──► NEVER into the P4 (reboot loop)
 C6 UART header ──────► external 3.3V USB-UART only (§7)

Warning. A related item is still in development: a shared battery pack for the P4 and the RTL-SDR is not finished. Powering a thirsty RTL-SDR and the P4 off one ill-designed supply invites brownouts; until the shared pack ships, power them cleanly and expect the RTL-SDR to be a real current draw.

The RTL-SDR’s draw is worth taking seriously for battery planning: an RTL2832U+R820T2 dongle pulls a few hundred milliamps continuously while streaming, on top of the P4, AMOLED and C6. That is why the shared-pack work exists and why, until it lands, a generous battery and a clean supply matter more than they would on a receive-only ESP32 toy. The BQ27220 fuel gauge (Vol 2) at least gives an honest state-of-charge readout while the power story is being sorted.


9.7 The C6 co-processor reflash — the fiddly bit

LakeShark’s wireless features — Wi-Fi, Bluetooth, OTA, LINK, DETECTOR’s beacon listening — all run on the onboard ESP32-C6, not on the P4 (the P4 has no radio of its own, Vol 2). The C6 must run a specific build: ESP-Hosted 2.12.9, the network_adapter firmware shipped in the repo’s c6_firmware/. Factory ESP-AT firmware and other ESP-Hosted versions do not work — a board on the wrong C6 firmware logs a transport timeout and has no wireless.

The complication: the C6 is on no USB port. It can only be flashed through its UART header with an external 3.3 V USB-to-UART adapter. This is the “C6 dance.”

Step 1 — prepare the P4. Flash LilyGO’s coprocessor_download_mode image over P4U (the right USB-C) and wait for the serial line:

 "Coprocessor preparation completed."

This temporarily overwrites LakeShark on the P4 (step 4 restores it) and puts the P4 into a mode that bridges to the C6 for flashing.

Step 2 — wire the adapter, RX and TX crossed. A 3.3 V USB-UART adapter on the C6 UART header:

 C6 RX ←── adapter TX (crossed)
 C6 TX ──→ adapter RX (crossed)
 C6 GND ←─→ adapter GND
 C6 3.3V NOT connected — the board powers the C6 itself

Warning. Do not connect the adapter’s 3.3 V to the C6. The board powers the C6; feeding a second 3.3 V source in is the C6-side equivalent of the MIX-RF 5 V back-feed in §6.

Step 3 — enter download mode and flash. Hold the C6 BOOT button, press and release RESET, then release BOOT. Flash the four C6 images with esptool.py — and note these are C6 offsets, different from the P4 offsets in §3 (C6 bootloader at 0x0, not 0x2000):

esptool.py -c esp32c6 -p YOUR_ADAPTER_PORT -b 460800 write_flash \
 0x0 c6_firmware/bootloader.bin \
 0x8000 c6_firmware/partition-table.bin \
 0xd000 c6_firmware/ota_data_initial.bin \
 0x10000 c6_firmware/network_adapter.bin

Note the -b 460800 baud and the explicit -c esp32c6 — the adapter talks to the C6 directly here, not through the P4, so the port in the command is the adapter’s serial port, not P4U. This is the one place in the whole flashing story where the port is not P4U, and getting that wrong is a common early stumble.

Step 4 — restore LakeShark on the P4. Flash the P4 again over P4U (web installer or idf.py flash), then power-cycle. Confirm the link came up in the serial log:

 I (....) headless: ESP-Hosted co-processor link: up (0)

Table 5 — 7. The C6 co-processor reflash — the fiddly bit

OffsetP4 (§3)C6 (this section)
bootloader0x20000x0
partition table0x80000x8000
ota_data(in flasher args)0xd000
app0x10000 (lakeshark.bin)0x10000 (network_adapter.bin)

A triage table for when step 3 or 4 does not go cleanly:

Table 6 — A triage table for when step 3 or 4 does not go cleanly

SymptomLikely causeFix
esptool cannot connect to C6Not in download modeRe-do BOOT-hold / tap RESET / release BOOT
esptool connects, wrong chip-c esp32c6 omitted / wrong portSet the chip and the adapter port
Flashes, but no wireless afterWrong offsets (used P4’s)C6 bootloader is 0x0, not 0x2000
No “link: up (0)” after restoreC6 image mismatchConfirm ESP-Hosted 2.12.9 images
Intermittent / brownout3.3V back-fed from adapterLeave adapter 3.3V disconnected

Note. This is a one-time operation. Once the C6 runs ESP-Hosted 2.12.9, later C6 updates can be pushed from the SD card (§5) without the adapter or the button dance. The UART path is still the recovery route if an SD update ever fails. See c6_firmware/README.md.

Tip. The single most common C6-reflash mistake is reusing the P4 offsets. They are not the same map: the C6 bootloader is at 0x0, the P4 bootloader is at 0x2000. Keep the two write_flash command lines in separate files so they never get crossed.


9.8 Building from source: ESP-IDF 5.5.4 and the per-board profiles

For developers, LakeShark builds against Espressif’s ESP-IDF 5.5.4, with six patches applied via bench/patch-idf.ps1 (documented in bench/IDF554.md). Both LoRa variants (SX1262 and LR2021) build from the same image — the firmware auto-detects the radio at boot (Vol 7 §2), so there is no separate build per radio.

There are four build targets, one per supported board/display:

Table 7 — There are four build targets, one per supported board/display

TargetBoard / displayBuild dirExtra defaults
TDP4 AMOLEDT-Display P4, RM69A10 568×1232build_tdp4boards/t_display_p4.defaults
TDP4 TFTT-Display P4, HI8561 540×1168build_tdp4_tft… + boards/t_display_p4_tft.defaults
Waveshare P4 headlessWaveshare ESP32-P4-NANO / WIFI6, no displaybuild_nanoboards/nano_headless_ble.defaults
Waveshare LCD-4.3Waveshare P4 with 4.3” LCD(per-board profile)(per-board defaults)

Each target differs in exactly two dimensions — display and whether there is one at all — which is why one codebase covers all four. The AMOLED and TFT builds are the same T-Display P4 hardware with different display controllers (RM69A10 vs HI8561) and resolutions; the two Waveshare targets drop the display entirely (headless) or carry a 4.3” LCD. The radio variant (SX1262 vs LR2021) is not a build dimension — both ship in the same image and are auto-detected at boot, so there is no “SX1262 build” to pick wrong.

The AMOLED build, as the canonical example:

# Build (AMOLED T-Display P4)
idf.py -B build_tdp4 \
 -D SDKCONFIG=build_tdp4/sdkconfig \
 -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;boards/t_display_p4.defaults" \
 build

# TFT variant: build dir build_tdp4_tft, and append boards/t_display_p4_tft.defaults

The four C6 images flashed in §7 correspond one-to-one to the ESP-Hosted layout: bootloader.bin (the C6’s second-stage boot), partition-table.bin (its map), ota_data_initial.bin (initial OTA-slot selector), and network_adapter.bin (the ESP-Hosted application itself — the part that makes the C6 behave as the P4’s radio). Mismatching any of them to a different ESP-Hosted version is the quiet way to end up with a C6 that enumerates but never brings the link up.

Building on Linux. The bench tooling (patch-idf.ps1, the wrap/build helpers) is PowerShell and Windows-first, which is a friction point on a Linux workstation. The patches those scripts apply are mandatory, but the underlying operations are ordinary IDF ones; the portable route is to install ESP-IDF 5.5.4, apply the equivalent patches described in bench/IDF554.md by hand (or run the .ps1 under PowerShell Core, which exists on Linux), and then drive the per-board idf.py invocations above directly. There is nothing Windows-specific in the firmware — only in the convenience wrappers.

Two practical flags for anyone cloning the repo:

  • The build tooling is PowerShell-oriented. The patch script (patch-idf.ps1) and bench tooling are .ps1 — Windows-first. On Linux/macOS the equivalent idf.py invocations above still work; the .ps1 wrappers are convenience, not a hard dependency, but the patches they apply are required.
  • There is a stale SDK in the tree. The repo carries a leftover sdk/esp-idf-5.4.3 alongside the required 5.5.4.Build against 5.5.4; the 5.4.3 directory appears to be a vestige and should not be used.

Tip. The per-board SDKCONFIG_DEFAULTS chain is the mechanism that keeps one codebase building four different physical products. The base sdkconfig.defaults sets the common config; each board’s .defaults layered after it overrides display, radio and feature settings. Do not edit a generated sdkconfig by hand — change the .defaults so the difference is tracked.

Why the IDF patches exist. LakeShark applies six patches to a stock ESP-IDF 5.5.4 via bench/patch-idf.ps1 (described in bench/IDF554.md). The reason is the same one that dominates the whole project: the P4’s USB-host stack and the real-time demands of streaming an RTL-SDR at 2 MSPS push on parts of IDF that a typical application never touches. The HackRF bring-up notes (see the HackRF discussion in Vol 3 and §3 here) record one concrete example of the class of problem being worked around — USB interface claims failing with ESP_ERR_NO_MEM because 512-byte-aligned DMA descriptors could not be allocated from PSRAM, fixed by reserving internal 512-byte slots managed by an atomic bitmap while keeping the IQ ring in PSRAM. Patches of that flavour are why a stock IDF checkout is not enough; apply the bench patches or the USB-host path will not behave.

A rough map of the internal flash a build lays down (the storage partition at 0x910000 is the one a full reinstall overwrites, §3):

 P4 internal flash (16 MB)
 0x002000 bootloader
 0x008000 partition table
 0x00d000 (ota data)
 0x010000 app slot A (lakeshark.bin) ─┐ two-slot OTA
 0x… app slot B ─┘ (running app in either)
 0x910000 storage (settings, saved Wi-Fi)

The DIAG app is the on-device counterpart to all of this: it reports radio presence, the C6 link state, sensor health and USB enumeration, and is the first place to look when a feature is silently absent. A dead wireless stack shows up in DIAG (and the serial log) as the missing “co-processor link: up (0)” line from §7; a missing RTL-SDR shows up as no USB radio enumerated. Reach for DIAG before assuming a flash failed.


9.9 Headless and Flipper-control-head modes

LakeShark does not require its display. Two “no-screen” modes exist, and they turn the firmware into something closer to a receiver module driven by another device.

Headless mode runs on the Waveshare ESP32-P4-NANO and ESP32-P4-WIFI6 boards — same receivers, no display — built with the build_nano target and boards/nano_headless_ble.defaults (§8). Control is through the serial console or over Bluetooth from a Flipper Zero. This is the right form for a fixed install (a monitoring node in a known location) or for anyone who wants the decode engine without paying for the AMOLED. Because the receiver stack is identical to the T-Display P4 build, a headless node loses nothing but the screen — it decodes the same P25/FM/ADS-B/POCSAG and hosts the same RTL-SDR; it simply presents the result over serial (or to a Flipper) instead of on glass.

Flipper control-head mode turns a Flipper Zero into LakeShark’s display and keypad. The companion LakeShark-Flipper app connects over Bluetooth (on the T-Display P4) or GPIO, and the radio advertises its connection and pairs automatically with no pairing code. The Flipper then shows LakeShark’s screens — the launcher, the offline aircraft map, ADS-B traffic, and the P25 radio view are the ones demonstrated.

 Control topologies
 ┌─ T-Display P4 ─┐ its own AMOLED/TFT + touch (default)
 ┌─ Waveshare P4 ─┐ headless: serial console OR BT → Flipper
 ┌─ any P4 board ─┐ LakeShark-Flipper app: Flipper = screen+keys (BT/GPIO)
Figure 1 — The LakeShark-Flipper control head showing the offline ADS-B aircraft map
Figure 1 — The LakeShark-Flipper control head showing the offline ADS-B aircraft map
*Image: SAMS0N1TE/LakeShark.*

The Flipper-as-control-head idea is one of the firmware’s more distinctive integration choices, and it ties LakeShark to the Flipper Zero deep dive elsewhere in this hub — the same .sub interchange (Vol 8 §3) and now a shared display role. For an owner who already carries a Flipper, a headless Waveshare P4 plus the Flipper app is a cheaper, pocketable way into the decode engine than the full AMOLED board.

A quick decision table for the three ways to drive the firmware:

Table 8 — A quick decision table for the three ways to drive the firmware

TopologyBoardDisplay / controlBest for
NativeT-Display P4Onboard AMOLED/TFT + touchThe default, all-in-one handheld
Headless + serialWaveshare P4-NANO/WIFI6Serial consoleFixed monitoring node, scripted
Headless + FlipperWaveshare P4 (or TDP4 over BT)Flipper Zero screen+keysCarrying a Flipper already; pocketable

The serial-console control of the headless build is also the plain-text way to drive the device for logging or automation: the TERMINAL/console is where commands like radios ble off (persisted in NVS) and the hackrf diagnostics (Vol 3) live, and a headless node on a serial tether can be scripted the way any console device can. That makes the Waveshare headless target the natural choice for an always-on receiver feeding a larger system, with the AMOLED board reserved for the walk-around use case.

Note. Automatic, codeless pairing is convenient but means the radio will accept a control-head connection without authentication. For a device that can key sub-GHz and LoRa transmitters (Vols 7–8), that is worth remembering in any threat model, even though in practice the BT range is short and the feature is aimed at the owner’s own Flipper.

The shape of the whole operations story, in one line each: the web installer is the five-minute path for almost everyone; two-slot OTA keeps a fast-moving device current without risk; the SD card holds everything large and fails independently of the firmware; the ESP-IDF source build is there for developers, four targets from one tree; and the C6 dance is the one genuinely fiddly step — a one-time UART reflash, after which the wireless half simply works and future C6 updates come from SD. Get the port and the bootloader offsets right (right-hand P4U; 0x2000 for the P4, 0x0 for the C6) and nothing here is difficult. Get them wrong and the symptoms are exactly the ones the triage tables above name.

Vol 10 closes the dive: how LakeShark compares to a PC-plus-RTL-SDR, a HackRF/PortaPack and other ESP32 SDRs; the open-source credit map; the legal and RF-listening envelope; the version-churn caveat; and a laminate-ready quick- reference card.

Comments (0)

  1. Loading…

Comments are held for moderation — nothing appears until approved.