Skip to content

Repository files navigation

Lilyshark

Lilyshark

Wireshark for mesh radio, built for the LILYGO T-Deck.

Status: developer alpha Target: LILYGO T-Deck LVGL 9.3 License: GPL-3.0

Flash a T-Deck from the browser · Watch the demo · Open the analyzer

The guided first run on a T-Deck Plus, the web analyzer open behind it The About screen on hardware: wordmark, version, protocols, license The Traffic screen streaming frames in simulate mode
The guided first run, in hand About, on the 320×240 panel Traffic streaming, first field session

Lilyshark turns a T-Deck into a handheld LoRa traffic and RF analyzer. It captures frames from the SX1262, keeps the raw bytes and radio measurements together, interprets supported mesh headers, tracks what the receiver has heard, surveys the band, and saves evidence to a microSD card.

Meshtastic, MeshCore, and Reticulum-compatible RNode traffic share one capture engine and one interface. Each protocol decoder adds the meaning it can prove from the frame. Unknown or protected data remains available as raw bytes with frequency, bandwidth, spreading factor, coding rate, RSSI, SNR, CRC state, frequency error, and airtime.

A single T-Deck is enough for field surveys, packet inspection, radio-profile checks, interference hunting, and capture. Lilyshark listens to traffic already on the air and gives that traffic a diagnostic interface you can carry.

The firmware is also a complete device shell, not a loose collection of graphs. It boots into the Lily Pink (#FF4F9D) wordmark, explains its packet/RF/network/capture tools, guides a first-time user through network and radio-profile selection, teaches the controls, checks the available hardware, then opens a Home screen with clear routes to diagnostics and settings. Capture, storage, radio, display, keyboard, optional GPS, Help, About, and setup reset are all controllable on the T-Deck.

A banner only works if somebody is looking at the screen, and in the field the deck is usually in a pocket. A message arriving raises a status-bar banner for twelve seconds and plays a rising two-tone through the speaker; a node heard for the first time announces itself and plays two quieter blips. The nodes list colours how long ago each neighbour was heard -- green inside five minutes, amber to thirty, red beyond -- and reports how far away it is, which is the question the screen exists to answer. Conversations are saved to NVS and survive a power cycle.

The diagnostic views change as the radio environment changes. Frames enter the Traffic feed, protocol totals roll forward, the spectrum history advances, node signals develop over time, surveys accumulate observations, and the synchronized Timeline shows packet rate, SNR, CRC failures, and events on one clock.

Note

The current firmware is a developer alpha, and it runs on real hardware. Two T-Deck Plus units have been exercised together in the field: display, keyboard, trackball, GT911 touch, GPS (fix, up to 12 satellites via baud autodetect), battery readout, USB analyzer link, and the full screen set. The SX1262 receives and decodes live over-air Meshtastic traffic between the two units -- position and NodeInfo beacons at -65 dBm with zero CRC errors -- and direct messages pass between them in both directions.

Still untested on hardware: microSD capture writes, touch calibration against a known reference, spectrum-scan recovery, and any live MeshCore or Reticulum/RNode traffic, for which no second implementation has been on the air here. Treat those paths as unverified until they get the same treatment.

Contents

Three surfaces: the device, the analyzer, and the app

Lilyshark is firmware, a web analyzer that reads what the firmware records, and a native app that carries the same mesh onto a phone, a Mac and a watch.

Where What it does
Firmware src/, include/ Captures LoRa frames off the SX1262 with their radio measurements. Writes .lscap and LoRaTap PCAP to microSD.
Analyzer webapp/ — live at lilyshark.com A terminal-style web app. TRAFFIC opens captures Wireshark-style — frame list, decoded RF metadata, hex dump, statistics, inline Shelby-pointer decode. SNIFFER adds a protocol dissection tree whose rows highlight the bytes they came from, and exports the capture as LoRaTap PCAP, CSV or JSON. SPECTRUM draws a live waterfall from sweeps the deck streams up the cable. CHAT/NODES/MAP/MESH/TELEMETRY drive a real radio over USB or Bluetooth. It installs as an offline app: once loaded, it opens with no internet at all, which is the point for an instrument whose network does not need one. Without a radio attached it seeds an explicitly synthetic demo mesh, labelled as such.
App ios/ A native iOS, macOS and watchOS client, built on PommeCore (Apache 2.0, by Michael P. Bedworth) with a MeshtasticKit package added beside its MeshCore one. It exists because Apple does not permit Web Bluetooth, so lilyshark.com can never reach a deck from an iPhone — and it brings a real macOS client with it. Build it with ./scripts/build_ios.sh.

Try it without a radio: open lilyshark.com, press SAMPLE on the TRAFFIC screen, and select frame 9 — it carries a Shelby pointer, decoded inline.

INTRO — the device, cycling all 38 real firmware screens PAPER — the 63-page whitepaper, pre-rendered and scrollable
INTRO — the device, cycling all 38 real firmware screens PAPER — the 63-page whitepaper, pre-rendered and scrollable
TRAFFIC — synthetic demo frames marked not OTA, with frame 9 carrying a Shelby pointer SHELBY — why an 82-byte pointer, with demo airtime derived from synthetic sample metadata
TRAFFIC — synthetic demo frames marked NOT OTA; frame 9 carries a Shelby pointer SHELBY — demo airtime derived from synthetic sample metadata
CHAT — channel traffic with per-message SNR and hop count NODES — roster with signal, battery, position and last-seen
CHAT — channel traffic with per-message SNR and hop count NODES — roster with signal, battery, position and last-seen
MAP — the demo mesh across the Bay Area MESH — topology summary, hop histogram and link graph
MAP — the demo mesh across the Bay Area MESH — topology summary, hop histogram and link graph
TELEMETRY — per-node metrics charted over 6h to 30d CONFIG — device, radio, alerts and backup settings
TELEMETRY — per-node metrics charted over 6h to 30d CONFIG — device, radio, alerts and backup settings
DEBUG — the serial console
DEBUG — the serial console

What Lilyshark shows

  • A live frame feed with capture time, protocol, source and destination when the protocol exposes them, packet type, route or hop data, and SNR
  • A five-tab packet inspector for route/header facts, RF measurements, decoder state, paged HEX, and capture provenance
  • A composable Traffic Filter for protocol, decoded/opaque/malformed state, and CRC condition; capture files remain complete while the view is filtered
  • Rolling 60-second protocol volume and decode health for Meshtastic, MeshCore, Reticulum/RNode, and unknown LoRa frames
  • Per-protocol detail with activity history, traffic share, CRC validity, mean SNR when attributable, and one-action handoff into filtered Traffic
  • Protocol-aware node activity and short signal histories when a stable node identity is available
  • A color spectrum view built from the SX1262 spectral-scan histogram
  • Channel activity, observed airtime, packet rate, CRC failures, and recent utilization
  • A synchronized 60-second Timeline for packet rate, SNR trend, CRC failures, interference, node changes, high utilization, and survey completion
  • A timed 60-second field survey with frames captured, unique sources, best SNR, and CRC errors
  • Local GPS state and position when a compatible receiver is attached
  • Operational events for radio state, active profile, capture files, PCAP limits, and screenshots
  • Native .lscap, LoRaTap PCAP, and 24-bit BMP output on microSD

Every screen, rendered by the firmware

These are not mockups: all 38 frames below are the simulator's own pixel-locked --render-test output at the device's exact 320x240 layout, the same images the test suite hashes on every build. The intro at lilyshark.com cycles through every one of them on the device.

Analyzer views

Live Traffic — frame feed with protocol, route, SNR Traffic filter — protocol and field filtering Protocols — per-protocol totals Protocol detail
Live Traffic — frame feed with protocol, route, SNR Traffic filter — protocol and field filtering Protocols — per-protocol totals Protocol detail
Spectrum — band scan Nodes — roster with signal history Node detail — SNR and RSSI histories with frame and CRC counts Packet detail
Spectrum — band scan Nodes — roster with signal history Node detail — SNR/RSSI histories, frames, and CRC errors Packet detail
Map — spatial view Survey — coverage capture Channel utilization Timeline — rate, SNR, CRC on one clock
Map — spatial view Survey — coverage capture Channel utilization Timeline — rate, SNR, CRC on one clock
Events — device history Messages — every decoded text message, newest first
Events — device history Messages — every text message heard

Device shell and onboarding

Splash First run — welcome First run — what it does First run — network
Splash First run — welcome First run — what it does First run — network
First run — radio profile First run — controls First run — ready Home
First run — radio profile First run — controls First run — ready Home
Radio profile Settings Capture & storage Device status
Radio profile Settings Capture & storage Device status
Display & input Help About Spectrum scan warning
Display & input Help About Spectrum scan warning
Reset setup
Reset setup

Packet inspector

PKT tab RF tab DEC tab HEX page 1
PKT tab RF tab DEC tab HEX page 1
HEX page 2 HEX page 3 RAW tab Event detail
HEX page 2 HEX page 3 RAW tab Event detail

Interface

The 320x240 interface gives the display to telemetry. It uses condensed labels, monospaced values, one-pixel rules, compact plots, and high-contrast selection. Lime marks live or healthy data, cyan marks navigation and information, amber marks warnings, and coral marks faults.

These images come from the working LVGL simulator and use the exact 320x240 device layout. Each screenshot freezes one moment from an interface that continues updating while the simulator runs:

Deterministic synthetic Lilyshark telemetry moving through Traffic, Protocols, Protocol Detail, Spectrum, Nodes, Survey, Airtime, Timeline, and Events

Deterministic synthetic RF telemetry: packet arrivals, protocol health, a deep-band spectrum sweep, node histories, survey progress, airtime, Timeline, and Events. Packet-derived views pause during the sweep because the T-Deck has one SX1262.

The animation is generated from 118 verified 320x240 simulator frames with scripts/generate_readme_media.sh. The complete 87-step recording tour also visits every setup, menu, tool, detail, confirmation, and analyzer route through the real input handlers.

Pink Lilyshark wordmark on the firmware splash screen Lilyshark onboarding screen explaining packet, radio, network, and capture diagnostics
First visible frame What the firmware can diagnose
Lilyshark Home screen with analyzer and Settings routes Lilyshark Settings menu
Home Settings
Traffic Filter with protocol, decode-state, and CRC predicates Rolling protocol health for Meshtastic, MeshCore, Reticulum, custom, unknown, and malformed frames
Traffic Filter Protocols
Five-tab packet inspector showing decoded packet facts Synchronized RF Timeline for packet rate, signal, CRC failures, and events
Packet inspector RF Timeline
Lilyshark traffic screen running in the LVGL simulator Lilyshark spectrum screen running in the LVGL simulator
Live traffic Spectrum

The early hardware mockups below established the compact visual language. They are historical composition studies, not a promise that every sketched field is available from received frames; the exact simulator frames above are the current implementation contract.

Early Lilyshark spectrum composition study shown on a LILYGO T-Deck

Early Lilyshark live packet traffic composition study Early Lilyshark node detail composition study
Dense frame feed with a clear focused row Signal-history density study; current firmware shows only attributable SNR/RSSI activity

All ten historical references and their relationship to the implemented routes live in design/references.

Product shell

On a fresh install, Lilyshark follows this path:

  1. Show the antialiased pink wordmark before the backlight reveals the application UI.
  2. Explain the packet, RF, network-health, and capture tools before asking for configuration.
  3. Ask which mesh family the operator intends to inspect.
  4. Choose and apply a matching radio preset.
  5. Teach trackball, keyboard, Back, Home, profile, and screenshot controls.
  6. Report input, microSD, optional GPS, radio, and capture readiness honestly.
  7. Save first-run completion only after the settings write succeeds.
  8. Open Home, where every live view and Settings is reachable without memorizing keys.

Returning users can start at Home or resume the last live view. Settings exposes the active radio profile, capture and storage state, device status, display brightness, keyboard light, optional GPS polling, startup behavior, Help, About, and a guarded setup reset. Failed persistence rolls the visible value back instead of pretending it was saved.

Analyzer routes

Route View Current device behavior
1 Traffic Shows captured frames from the bounded in-memory store. Up/Down selects a row, Enter opens it, and X opens Traffic Filter.
Traffic → X Traffic Filter Filters the visible frame list by protocol, decoder outcome, and CRC state without filtering either capture file.
9 Protocols Rolls the last 60 seconds into stable per-class counts, traffic share, decoded share, and CRC failures. Enter opens the focused class.
Protocols → Enter Protocol detail Shows attributable activity, CRC, and SNR health. Enter applies that class to Traffic Filter.
2 Spectrum Offers a fast active-channel scan or a deeper regional sweep, grows the measured histogram as bins complete, and reports partial/cancel/restore state.
3 Nodes Lists observed protocol identities and signal history. Up/Down selects a row; Enter opens it.
Traffic → Enter Packet detail Uses PKT, RF, DEC, HEX, and RAW tabs; HEX exposes every captured byte in bounded 40-byte pages.
Nodes → Enter Node detail Summarizes recent frames, SNR, RSSI, and activity for an observed source.
4 Map Shows the local optional-GPS fix and distinguishes GPS Off, missing hardware, search, and fix states.
5 Survey Captures a 60-second diagnostic sample and reports observed results.
6 Airtime Summarizes observed airtime, frame rate, CRC failures, and recent activity.
T Timeline Places packet rate, SNR trend, CRC failures, and diagnostic event markers on one synchronized 60-second axis.
7 Events Reports radio, profile, capture, PCAP, screenshot, settings, and hardware state in a scrollable history.

8 opens Settings. M or 0 opens Home. Left and Right move through the primary diagnostic views; reaching either end returns to Home instead of wrapping invisibly.

The simulator drives these views with deterministic synthetic RF telemetry. A fixed seed and simulation clock reproduce the same packet arrivals, progressive spectrum sweeps, signal drift, survey totals, protocol mix, utilization changes, and Timeline markers for demos and tests. Simulator values are synthetic. On a T-Deck, the same interface uses live SX1262 captures and hardware state, or shows an explicit unavailable state when a measurement cannot be collected.

Device controls

Control Action
Left/Right or horizontal touch swipe Move through primary analyzer views, Packet Detail tabs, or a selected setting where the screen says so
Up/Down or vertical touch swipe Move menu and table focus; page Packet Detail HEX bytes and the Events history
Trackball press or Enter Open the focused route or record, apply a choice, start a survey, or start/cancel a spectrum sweep
Backspace Go back through the product shell or return from a detail view
M or 0 Open Home; press again to return to the analyzer
1 through 7 Open Traffic, Spectrum, Nodes, Map, Survey, Airtime, or Events directly
8 Open Settings
9 Open Protocols
T Open Timeline
X from Traffic Open Traffic Filter; use Up/Down to choose a predicate, Left/Right to change it, R to reset, and Enter to apply
P Open the five-preset radio-profile picker with the active preset focused
Trackball on Map Roll to pan. Press to step the zoom inward through neighbourhood, town, streets, houses, then back out wide. Return to your own position with the on-screen MAP chip, which appears once you have panned
+ / - on Map Zoom, from z8 (about 150 km across) in to z23
I / D / G on Map Satellite imagery, dark road map, or the drawn field chart. The on-screen chip names the layer you are on and moves to the next when tapped
- / + Move the active profile down or up one bandwidth-sized frequency step within its region
B Cycle 62.5, 125, 250, and 500 kHz bandwidths
F Cycle spreading factors 7 through 12
R Cycle coding rates 4/5 through 4/8
C Open Chat; from a node's detail screen, open Chat addressed to that node
E Open Messages — the readable text pulled out of captured traffic, including LXMF
S Save a BMP screenshot to microSD
? Open Help without bypassing unfinished onboarding or destructive confirmations
Touch tap Select the tapped menu/table row or invoke the exact left/right action shown in the footer

The touchscreen probes the GT911 at both T-Deck addresses and reports absence instead of blocking startup. The trackball and keyboard remain available when touch is missing.

Pair a phone

The deck advertises Meshtastic's client Bluetooth service, so the official Meshtastic app (iOS or Android) finds it in a scan as Lilyshark followed by the deck's short name. Pairing gets the app the real conversation, not a shim: the deck answers the app's configuration handshake with its node identity, hardware model, the node list as currently heard (live SNR included), the primary LongFast channel, and the LoRa region settings, then keeps the picture current — a node heard for the first time after pairing is pushed to the phone the same moment the deck itself raises its new-node banner.

Texts flow both ways. A message the deck hears on the air appears in the app's conversation view; a message typed in the app goes out through the same transmit path as one typed on the deck's own keyboard — same packet ids, same want-ack request on direct messages, and it lands in the deck's chat log alongside everything else, where the peer's acknowledgement marks it DELIVERED.

Positions travel too: nodes with a known location arrive in the app with coordinates at pairing, and a position heard afterwards is forwarded as the same POSITION_APP packet stock firmware would send, so the app's map places nodes the way the deck's own map does. What pairing does not yet do, so nobody discovers it the hard way: settings changed in the app are read back but not applied — the deck's own Settings screen remains the way to change the radio.

Private messages, and borrowed channels

Two kinds of privacy, kept honestly apart.

A direct message to a node whose public key the deck has heard is sealed to that node with Curve25519 and AES-256-CCM, the way Meshtastic 2.5 does it. The deck mints its identity once from the hardware random generator and keeps it in NVS forever, because an identity regenerated each boot goes stale in every peer's memory and private replies quietly stop arriving. The public half rides our NodeInfo, which is the only way anyone can address us privately. Anything that cannot be sealed still goes out under the channel key — a message nobody can open would be worse than one the channel can read.

A channel key somebody gives you is different: it makes their traffic readable, and it must not make you loud. So a frame opened with a stored key is never acknowledged on the public channel (the acknowledgement would be sealed with the default key, announcing both that you hold the key and which packet you just read), it is not forwarded to a paired phone (that API has no field to say which key opened it, so the message would arrive looking public), and it appears on MESSAGES rather than in CHAT — because CHAT has a Send button and a reply would go out publicly into a conversation you believed was private.

That last rule holds until the deck can transmit sealed with a stored key. The comment in the code says so, and names itself as the thing that changes when it can.

Protocol coverage

Decoding is profile-gated because these LoRa protocols do not all carry an unambiguous magic value. Press P on the T-Deck to open the radio-profile picker. Use -/+, B, F, and C to tune its frequency, bandwidth, spreading factor, and coding rate for the network in front of you. The active preset and tuned values are saved across restarts. Lilyshark reconfigures the SX1262 transactionally and uses the matching structural decoder.

Protocol Included profiles Fields decoded today Current boundary
Meshtastic US LongFast, 906.875 MHz, 250 kHz, SF11, CR 4/5 Outer header, source, destination, packet ID, channel hash/hint, hop limit/start, next hop, relay byte, broadcast/ACK/MQTT flags Protobuf payload stays opaque. The outer header alone does not prove whether it is clear or encrypted; channel keys and payload decryption are not implemented.
MeshCore Current US recommendation at 910.525 MHz/62.5 kHz/SF7; legacy 915 MHz/250 kHz/SF10 Version 1 route type, payload type, encoded path shape, transport codes, group channel, ACK checksum, structural length validation Protected direct, group, and anonymous payloads stay opaque. Advertisement bodies are not expanded into contacts.
Reticulum / RNode Documented EU example at 867.2 MHz plus a tunable 915 MHz US starting point, both 125 kHz/SF8 RNode shim, split marker, Reticulum header type, packet and destination type, context, hops, hash prefixes, outer-header protection marker. LXMF messages that were never encrypted are read out of the payload: destination and source hashes, timestamp, title, body, and a count of structured fields RNode PHY settings are deployment-defined. IFAC-marked content stays opaque and unverified without an interface key. LXMF to a SINGLE destination is encrypted end to end and stays that way. The included profiles are starting points, not universal Reticulum channels.
Unknown LoRa User code can add RadioProfile entries Raw frame, integrity state, and all RF metadata supplied by the radio No protocol labels are invented. The frame is still inspectable and exportable.

The decoder API preserves uncertainty. MeshCore transport codes are not presented as node IDs, and Reticulum's 32-bit hash prefixes are not presented as complete identities.

No decoder here attacks a cipher. Two payload formats are readable because of what they are, not because anything was broken: a Meshtastic channel using the published default key, which every radio ships with, and an LXMF message sent to a destination type that carries no encryption. Both give up the moment the bytes stop parsing, because noise must never be presented as a message. A real PSK, and an LXMF message to a SINGLE destination, stay opaque -- exactly as they should.

Built-in PHY profiles

ID Name in firmware Center frequency Bandwidth SF CR Sync word Preamble
1 MESHTASTIC US LF 906.875 MHz 250 kHz 11 4/5 0x2B 16 symbols
2 MESHCORE US 910.525 MHz 62.5 kHz 7 4/5 0x1424 32 symbols
3 MESHCORE LEGACY 915.000 MHz 250 kHz 10 4/5 0x1424 16 symbols
4 MESHTASTIC BAY MF 913.125 MHz 250 kHz 9 4/5 0x2B 16 symbols
5 RNODE EXAMPLE US 915.000 MHz 125 kHz 8 4/5 0x1424 18 symbols

These are explicit starting profiles, not automatic protocol detection. Choose settings that match the network and comply with the rules for your location before capturing traffic.

Architecture: off-grid data over Shelby

A flooded LoRa mesh pays for every payload byte many times over. Measured against Meshtastic's discrete-event simulator, delivery costs R = 7.36 transmissions per delivered message at realistic density — against the 3 to 5 usually assumed — and reach falls from 68.6% to 25.8% as nodes are added, because every relay consumes shared channel time. Airtime is the scarce resource, and it shrinks as the network grows.

Lilyshark therefore never asks the mesh to carry content. It carries an 82-byte ShelbyPointer: a fixed-layout reference to a blob stored on the Shelby storage protocol. The pointer rides inside a normal Meshtastic, MeshCore, or Reticulum payload — it is a payload convention, not a new link layer — so the same encoding works on all three protocols and unmodified nodes forward it untouched. Any node with an IP path, such as a phone or a base station, acts as a gateway: it resolves the pointer and moves the actual bytes over IP, where bandwidth is not shared with the mesh.

Off-grid data flow: the 82-byte Shelby pointer crosses the LoRa mesh, a gateway resolves it, and the full blob moves over IP between the gateway and Shelby storage

The 82 bytes hold a magic, version, flags, a 32-byte blob commitment, a 32-byte owner account address, blob size, expiry, and chunk index/count, which leaves headroom inside one LoRa frame (~200 practical payload bytes in Meshtastic) for the enclosing protocol's own header. The exact layout, encoder, decoder, and payload detector are in include/lilyshark/shelby/shelby_pointer.h.

ShelbyPointer 82-byte wire format: magic SHLB, version, flags, 32-byte blob commitment, 32-byte owner account address, blob size, expiry, chunk index, and chunk count, all little-endian

Shelby, built on Aptos, provides the storage side: persistent, paid, verifiable blob reads and writes that a mesh with no IP path can still reference. The commitment identifies the content and the owner address identifies the paying account, so a gateway can fetch or post the blob without trusting the mesh. The Lilyshark webapp resolves pointers seen in captures and displays both the capture data and the state of the Shelby network.

Open stack from LILYGO T-Deck hardware through open radio protocols, Lilyshark firmware, the Shelby pointer convention, and Shelby storage on Aptos to the Lilyshark webapp

An interactive 3D walkthrough of this flow — the T-Deck, the mesh, the pointer in flight, and the Shelby resolution — ships with the webapp and is served at /demo on its deployment, from webapp/public/demo. It includes a scripted record mode; docs/DEMO.md is the matching video production guide.

Radio capture and spectrum scanning

The T-Deck target configures the onboard SX1262 for one active profile at a time. Its receive path records valid frames and CRC mismatches, then immediately returns the radio to receive mode. The in-memory UI store holds the newest 64 records while microSD capture keeps writing beyond that window.

Spectrum mode uses the SX1262 spectral-scan patch and reads a 33-bin power histogram at each frequency step. Fast Narrow inspects 11 steps around the active channel with 64 samples per step. Deep Band uses 512 samples per step across the configured region; the built-in US request covers 902 through 928 MHz in 200 kHz steps, while EU and custom profiles use their own ranges. The plot grows only as measurements complete. The scan owns the single radio while it runs, so packet reception, Traffic, and capture pause visibly; Lilyshark then fully reapplies the active receive profile before capture resumes.

This scan facility is marked experimental in the firmware. Semtech and RadioLib describe it as experimental, and Lilyshark's restore path has not yet been exercised on a physical T-Deck. The interface reports partial progress, cancellation, timeouts, scan failures, and receive-restoration failures instead of hiding them.

Capture files and screenshots

Insert a writable microSD card before boot. Lilyshark mounts it, creates /lilyshark, and opens unique capture files automatically.

Output Path Use
Lilyshark capture /lilyshark/capture-####.lscap Protocol-neutral format that preserves every recorded RF field and supports settings such as 62.5 kHz bandwidth. Version 1.1 marks each synthetic frame in metadata bit 2. The layout is documented in docs/lilyshark-capture-format.md.
LoRaTap PCAP /lilyshark/capture-####.pcap Standard PCAP with DLT 270 LoRaTap records for Wireshark and compatible tools. Synthetic frames are skipped because LoRaTap v0 cannot carry their provenance.
Screenshot /lilyshark/screenshot-####.bmp Pixel-exact 320x240, uncompressed 24-bit BMP captured from the device display when S is pressed.

LoRaTap v0 cannot represent every bandwidth exactly. The included MeshCore 62.5 kHz profile therefore continues recording .lscap while the Events view reports the PCAP bandwidth limit. Profiles at 125 kHz or exact multiples can produce LoRaTap records. Capture sinks flush every five seconds during normal operation. A short write or a post-flush size mismatch closes that file and latches a storage error. After restoring writable storage, open Settings → Capture & Storage and retry capture to create new unique files; reboot if the card itself was reinserted after the SPI stack reported a mount failure.

Native captures keep per-frame provenance. Metadata flag bit 2 means the frame was generated by simulate mode. A clear bit in a legacy version 1.0 file does not prove over-the-air origin. The LoRaTap writer returns a distinct SyntheticFrame result without writing a packet, which prevents generated traffic from entering a PCAP that could be mistaken for an RF capture.

Capture timestamps are monotonic microseconds since boot. That preserves order and intervals, but the original T-Deck has no dependable real-time clock, so classic PCAP cannot provide trustworthy wall time yet. Wireshark may display a 1970-era absolute date; use its relative-time columns for these captures.

Validate a capture on the host before processing it:

python3 scripts/lscap.py validate capture-0001.lscap

Dump the file header followed by one JSON object per frame, which works well with streaming tools such as jq:

python3 scripts/lscap.py dump capture-0001.lscap

Use --pretty when you want one indented JSON document with a records array:

python3 scripts/lscap.py dump --pretty capture-0001.lscap

The reader keeps signed RF values as signed JSON numbers. It also includes the raw enum and flag values, readable names for known values, unknown flag bits, reserved bytes, header extensions, and the payload as hexadecimal.

Saving a BMP uses the display and microSD on the shared SPI bus. Reception stays armed, but polling pauses while the image is written, so a busy channel can lose frames. The Events view reports the measured screenshot capture gap.

Shelby: storage for captures, and a pointer that fits one LoRa frame

A capture is only worth something if you can prove it is the same bytes the radio heard, so captures belong in content-addressed storage rather than on a card that can be edited. Lilyshark uses Shelby for that, and the analyzer can fetch a capture by Shelby blob name and parse it in the browser.

Reaching Shelby from off-grid needed a design rather than a wire. A LoRa node has no IP path, so it cannot call Shelby. It does not need to: the mesh moves ~200 usable bytes per frame, far too small for a payload and far larger than a blob reference. So the radio carries an 82-byte Shelby pointer and a node with connectivity moves the bytes.

include/lilyshark/shelby/shelby_pointer.h    82-byte wire format, magic "SHLB"
src/shelby/shelby_pointer.cpp                encode / decode / locate
src/shelby/shelby_pointer_decoder.cpp        finds pointers inside captured frames
webapp/src/lib/lscap.ts                      the same format, read in TypeScript

The pointer carries a blob commitment, owner account, size, expiry, and chunk position. It is deliberately not a new link layer — it is an application payload convention, which is why one encoding works inside Meshtastic, MeshCore, and Reticulum alike, and why a node running stock firmware relays it without knowing what it is. Decoding rejects inconsistent chunk state, so a receiver can never mistake one part of a split blob for a complete one. The C++ encoder and the TypeScript decoder are cross-checked byte for byte.

Keeping the over-the-air object small is a measured decision, not a stylistic one: against Meshtastic's own discrete-event simulator a flooded mesh spends R = 7.36 transmissions per delivered message at realistic density, and reach falls from 68.6% to 25.8% as nodes are added. Airtime is the scarce resource, so send a reference and let a connected node carry the payload.

The loop is live, not planned. A field capture is stored on shelbynet right now — object captures/field-capture-0846.lscap, owner 0x34946d19fb18115046c807b8f48845a515efe107892bb9cc49c6f197a6998728, commitment 0x6ab9566563ba70a73965f89a46edf3d49978c5091b8da8786e8cb58a449a32c9 — uploaded with webapp/scripts/shelby-put.ts through the official Shelby SDK (commitment generation, on-chain registration, chunkset upload, commit ack). The bundled sample's frame 9 carries that blob's real coordinates, and the analyzer's RESOLVE button walks the whole path in the browser with no Lilyshark server in between: commitment → object name on the shelbynet indexer → bytes from the Shelby RPC → the capture opens — or click https://lilyshark.com/#resolve to watch it run: the link opens the analyzer, loads the sample, and resolves the pointer by itself. Fetch it yourself:

curl https://shelby.shelbynet.shelby.xyz/shelby/v1/blobs/0x34946d19fb18115046c807b8f48845a515efe107892bb9cc49c6f197a6998728/captures/field-capture-0846.lscap

On-chain anchor: the capture registry

The blob and the pointer answer where the bytes are; a Move contract on the same network answers who vouched for them and when. contracts/capture-registry/ publishes lilyshark::capture_registry, deployed at

0x34946d19fb18115046c807b8f48845a515efe107892bb9cc49c6f197a6998728::capture_registry

Registering a capture records the publisher, the 32-byte blob commitment, the claimed size, and the lease expiry, and emits a CaptureRegistered event. The field capture above is anchored as entry 0 — same commitment on the radio, in the blob store, and on the chain — so the chain of custody reads RF capture → Shelby blob → on-chain anchor, each step checkable against the last. Query it against the live network:

aptos move view \
  --url https://api.shelbynet.aptoslabs.com/v1 \
  --function-id 0x34946d19fb18115046c807b8f48845a515efe107892bb9cc49c6f197a6998728::capture_registry::capture_at \
  --args address:0x34946d19fb18115046c807b8f48845a515efe107892bb9cc49c6f197a6998728 u64:0

Deployment tx 0x09dab1e8f99df0feed757503c8e89179d80db2bd861ce8d81348c137b81ec904, anchor tx 0x5c56d7bfce7c45a7d16c242a45e9d7f9711511fd4b3fd8f1f152dfaac1a73aee.

Status: the pointer codec, the cross-protocol decoder, the capture format, upload via the Shelby SDK, in-browser resolution, and the on-chain registry are built, tested (ASan/UBSan on the C++ side), and running against shelbynet. The remaining piece is on-device: firmware invoking the upload directly from the T-Deck's companion gateway.

Download

The public GitHub Releases page contains the current T-Deck factory image, application-only image, debug symbols, and SHA-256 checksums. For a fresh install, use the prebuilt-release steps; a local firmware build is not required. Do not flash the .elf file.

Flash a T-Deck

Adding a new deck to an existing mesh? docs/join-the-mesh.md is the whole path — clone, flash, verify it hears, and put your own location's imagery on a microSD card.

The fast path needs no toolchain at all: lilyshark.com/flash installs the current factory image from Chrome or Edge over Web Serial — plug the T-Deck in, click install, pick the port. The page shows the image's SHA-256 and the recovery steps.

For a scripted install, the guarded flash script accepts an explicit serial device, verifies the factory image against SHA256SUMS, pins esptool 4.11.0, and writes the merged image at 0x0.

./scripts/build_release.sh
./scripts/flash_tdeck.sh /dev/cu.usbmodem1101   # macOS example
# ./scripts/flash_tdeck.sh /dev/ttyACM0         # Linux example

./scripts/flash_tdeck.sh --auto proceeds only when it finds exactly one eligible USB modem or ACM serial device. The script stops when the port, artifact, or checksum is ambiguous. After flashing, scripts/smoke_tdeck.py records a bounded 115200-baud startup log and checks the display, touch, storage, radio, and UI milestones without sending data to the device.

Use the factory image for a fresh install. The application-only image at 0x10000 is for updates on a T-Deck that already has the matching Lilyshark bootloader and partition table. Full instructions, serial monitoring, expected files, and recovery boundaries are in docs/FLASHING.md.

Build and test

The tool versions are pinned in the repository. Install uv so uvx can create isolated PlatformIO and esptool environments.

Run every host test and build both targets

./scripts/test_all.sh

Run the sanitizer-backed C++ tests without PlatformIO builds:

./scripts/test_all.sh --host-only

Build release artifacts

./scripts/build_release.sh

The release build pins compiler date and time macros to the current Git commit. To prove determinism on the same host with the pinned toolchain, run two forced-clean builds and compare every output byte:

./scripts/verify_reproducible_release.sh

GitHub Actions is the canonical release environment (ubuntu-24.04). A build on another operating system can embed different tool paths, so use the tagged release assets when you need the published checksums exactly.

The script runs the pinned T-Deck build and writes:

dist/lilyshark-tdeck.factory.bin  Complete image for address 0x0
dist/lilyshark-tdeck.bin          Application image for address 0x10000
dist/lilyshark-tdeck.elf          Symbols for debugging
dist/SHA256SUMS                   SHA-256 checksums

The equivalent direct firmware build is:

SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)" \
  uvx --with pip==25.2 --with intelhex==2.3.0 --from platformio==6.1.19 \
  platformio run -e t-deck

Run the simulator

The checked-in simulator environment currently targets x86_64 macOS and resolves sdl2-config from PATH.

uvx --from platformio==6.1.19 platformio run -e simulator
.pio/build/simulator/program

Open a named view at launch:

.pio/build/simulator/program --screen spectrum

Render all analyzer and product-shell routes without opening an SDL window and compare their exact framebuffers with the checked-in expectations:

.pio/build/simulator/program --render-test

Run the automatic presentation tour for a screen recording:

./scripts/run_ui_demo.sh

On macOS 14 or newer, ./scripts/run_ui_demo.sh --record binds a desktop-independent ScreenCaptureKit recorder to the dedicated Lilyshark simulator window, holds on the pink splash until capture is active, then drives one complete pass through the real UI navigation while deterministic synthetic telemetry continues to move. The Retina window capture remains intact if another app covers it or the user changes Spaces. Use --record-manual only when automatic window capture is unavailable. See the recording guide for the full shot list and output options.

Regenerate the exact README screenshots and the deterministic live GIF after an intentional UI change:

./scripts/generate_readme_media.sh

Run the simulator as one long-lived process that rebuilds every view once per second. With no argument, the runner stops after 24 hours and fails on an early exit or fatal diagnostic:

./scripts/soak_simulator.sh       # 24 hours
./scripts/soak_simulator.sh 60    # one-minute check

On Apple Silicon with Homebrew:

brew install sdl2 uv

Architecture

The firmware separates raw capture from protocol interpretation. A radio profile defines the LoRa PHY and suggests a decoder. Each received frame enters one protocol-neutral record, passes through the decoder registry, updates the bounded snapshot store, reaches the UI, and is written to the enabled capture sinks.

flowchart LR
    A[Active radio profile] --> B[SX1262 receive and scan service]
    B --> C[Raw frame plus RF metadata]
    C --> D[Decoder registry]
    D --> E[Protocol-aware packet]
    C --> F[LSCAP and LoRaTap writers]
    E --> G[64-record capture store]
    G --> H[Product shell and analyzer views]
    F --> I[microSD]
Loading

The scan state machine gives the SX1262 exclusive ownership during a sweep. It has bounded point counts, per-frequency and overall timeouts, cancellation, partial results, and a receive-profile restore step on every exit path.

Repository layout

include/lilyshark/core/       Protocol-neutral records, profiles, store, and spectrum model
include/lilyshark/protocols/  Meshtastic, MeshCore, and Reticulum decoder interfaces
include/lilyshark/device/     T-Deck radio, status, touch, screenshot, and SD services
include/lilyshark/export/     LSCAP and LoRaTap writer interfaces
include/lilyshark/shelby/     Shelby off-grid pointer format and its frame decoder
include/lilyshark/ui/         Packet presentation and derived-node policy
src/core/                     Decoder registry, profiles, decoders, and spectrum helpers
src/device/                   ESP32-S3 and T-Deck hardware implementations
src/export/                   Capture encoders
src/shelby/                   Shelby pointer encode, decode, and in-frame detection
webapp/                       Terminal-style web app: analyzer, mesh client, Shelby explorer (Vite + React)
webapp/src/lib/lscap.ts       .lscap and Shelby-pointer readers, matching the firmware
webapp/src/mesh/              Terminal analyzer: radio link, store, screens, theme
webapp/src/mesh/screens/      Terminal screens: TRAFFIC (analyzer), SHELBY, PAPER, mesh client
samples/                      Deterministic demo capture with a Shelby pointer at sequence 9
src/ui/                       Shared packet labels and UI admission rules
src/sim_main.cpp              Shared LVGL screens plus simulator and T-Deck shells
src/fonts/                    Generated LVGL font sources
test/                         Sanitizer-backed host tests
scripts/                      Test, release-build, factory-image, and safe-flash tooling
scripts/shelby_pointer.py     Shelby pointer reference tooling: emit, parse, scan, verify
assets/brand/                 Pink transparent SVG wordmark and monochrome variants
assets/device/                Embedded-wordmark generation notes and placement contract
design/previews/              Captures from the working simulator
design/references/            Ten target hardware mockups and their screen map
experiments/ios6/             Isolated 320×240 iOS 6 UI lab (not firmware)
docs/                         Quickstart, architecture, off-grid design, format specs, guides
.github/workflows/            Reproducible test and firmware artifact build

Target hardware

  • LILYGO T-Deck with ESP32-S3 and SX1262
  • 320x240 ST7789 color display
  • Built-in keyboard, trackball, GT911 touch controller, and PSRAM
  • Writable microSD card for capture and screenshots
  • Optional GPS receiver on the T-Deck serial pins
  • An antenna suited to the frequencies you configure

The T-Deck is the first hardware target. The capture record, decoder registry, and export formats stay independent of its display and input services so future compatible LoRa hardware can reuse the analyzer core.

Project status

Area Evidence in this repository Physical T-Deck status
Product shell and analyzer UI Exact 320x240 framebuffer comparisons cover 14 analyzer routes, six onboarding stages, Home, menus, confirmations, all five packet tabs, extra HEX pages, and Event Detail. Interaction tests cover keyboard, trackball-equivalent navigation, mouse/touch hit targets, back-stack behavior, first-run persistence, filtering, and failure rollback. Running on two T-Deck Plus units
Embedded wordmark A generated 264x128 A8 mask keeps the SVG's antialiased edge detail, lives in flash, and is recolored Lily Pink at draw time. A source/payload hash test and device-shell framebuffer check protect the asset. Confirmed on the 320x240 panel
T-Deck hardware target Pinned PlatformIO builds app, factory, and ELF artifacts. A host test checks every command, data byte, and delay in the panel initialization sequence against LilyGO T-Deck commit 274ddaa. TFT_eSPI 2.5.43 is pinned with the upstream one-line SPI2 register fix at 880ec0e, and a compile-time guard rejects an invalid ESP32-S3 register base. The device shell also runs against host peripheral fakes. Boots and drives the panel on two units
SX1262 frame capture The real radio service runs against host fakes covering configure, IRQ/read/rearm order, CRC mismatch, retry, scan, restore, and recovery Receives live traffic (rx counter climbing, 0 CRC errors); long-run recovery pending
Meshtastic decoder Profile-gated outer-header tests, including malformed input Decoded live between two T-Decks at -65 dBm
MeshCore decoder Version 1 structural and malformed-frame tests, including the legal empty RAW_CUSTOM form Live over-air sample pending
Reticulum/RNode decoder Header-one/header-two, IFAC-marker, split, and malformed-frame tests Live RNode sample pending
Radio profile tuning Sanitizer tests cover regional frequency stepping, BW, SF, CR, and persisted-value validation Keyboard tuning and restart persistence pending
.lscap export Byte-exact writer tests and documented v1 layout microSD write test pending
LoRaTap PCAP Byte-exact writer tests for the DLT 270 record layout microSD and desktop-open test pending
BMP screenshots RGB565-to-BMP tests and unique-path device writer Display readback and microSD test pending
Touch, keyboard, and trackball Input services compile; host tests cover the touch transform and polling deadlines across the 32-bit millis() rollover On-device input, calibration, and interaction tests pending
Battery and optional GPS Battery model tests; TinyGPS++ hardware service compiles GPS fix observed (up to 12 satellites); ADC calibration against a reference pending
Spectrum scan Request/result tests plus radio restore state machine Experimental; complete hardware validation pending

The standalone C++ tests compile with warnings as errors and run under AddressSanitizer and UndefinedBehaviorSanitizer. The simulator renders every analyzer and product-shell route into a full 320x240 RGB565 buffer and checks exact pixels, content thresholds, and uniqueness. A separate region-based motion test proves that eleven live diagnostic views change through the production update path; packet selection remains a deliberate snapshot. The telemetry model has sanitizer-backed tests for deterministic replay, bounded state, rolling windows, scan-time capture pause, and changing measurements. The serial checker is fixture-tested and reads without writing to the port. Alpha.7 also executes the real device setup and loop under the sanitizers through host peripheral fakes, including first-frame/backlight ordering, six-stage onboarding persistence, missing-hardware recovery, menu navigation, settings rollback, a radio frame flowing into both capture formats and the UI, five packet tabs, HEX paging, Traffic Filter, Protocol Detail, and event-history detail. These checks do not replace the pending physical display and input smoke tests. The checked-in GitHub Actions workflow runs the same suite, builds both targets, and uploads firmware artifacts after a successful workflow run.

Roadmap to a stable release

  • Build the analyzer screen set, including Traffic Filter, Protocol Detail, five packet tabs, Protocols, and Timeline.
  • Add the pink device splash, six-stage onboarding, Home, Settings, Help, About, and guarded confirmations.
  • Persist first-run, brightness, capture, GPS, and startup preferences with CRC validation and failure rollback.
  • Add the ESP32-S3/T-Deck target and reproducible factory image.
  • Capture SX1262 frames and CRC mismatches with configured channel settings, measured signal metrics, and explicit-header coding-rate/CRC metadata.
  • Add profile-gated structural decoders for Meshtastic, MeshCore, and Reticulum/RNode.
  • Add persistent on-device frequency, bandwidth, spreading-factor, and coding-rate tuning.
  • Add .lscap, LoRaTap PCAP, and BMP output on microSD.
  • Add keyboard, trackball, GT911 touch, battery, and optional-GPS services.
  • Add an interruptible SX1262 spectral-scan state machine with receive restoration.
  • Add sanitizer-backed host tests and a two-target GitHub Actions build.
  • Boot the factory image on a physical T-Deck and verify the display, input, GPS, battery, and radio services.
  • Pass live traffic between two T-Decks and decode it, including direct messages.
  • Read LXMF messages out of Reticulum payloads.
  • Advertise Meshtastic's client BLE service and answer the app's configuration handshake with identity, node list, channel, and LoRa config.
  • Bridge texts both ways between a paired phone and the mesh, through the same transmit path as the deck's own keyboard.
  • Forward node positions to the paired phone, at pairing and live as they arrive.
  • Apply settings the phone app writes back.
  • Capture known MeshCore and RNode fixtures over the air and compare bytes with desktop captures.
  • Calibrate touch orientation, battery voltage, and spectral power against known references.
  • Exercise scan cancellation, SD removal, CRC bursts, missing peripherals, and radio recovery.
  • Run an overnight capture and spectrum endurance test on hardware.
  • Add sync-word/preamble editing, key management, deeper payload decoders, and more regional presets.
  • Publish a hardware-validated tagged release with checksums and a field-test report.

Documentation

License and project names

Lilyshark is licensed under GPL-3.0. Barlow Condensed and IBM Plex Mono are distributed under the SIL Open Font License; their license texts are included in assets/fonts.

The web terminal's JetBrains Mono build is distributed under the SIL Open Font License (webapp/src/mesh/assets/fonts/OFL.txt). See NOTICE.md.

Meshtastic, MeshCore, Reticulum, RNode, Wireshark, and LILYGO are referenced to describe compatibility, protocol coverage, and target hardware. Lilyshark is an independent project and is not an official release from those projects.

About

Wireshark for mesh radio, built for the LILYGO T-Deck.

Topics

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages