An ESP32 2.4 GHz-only Wi-Fi bridge (AP + client + NAT) with an experimental ARINC 834 ADBP position feed for bench and interoperability testing.
The onboard flight display (ESP32-S3 T-Display) live in flight-test: tail & flight number,
trip-progress bar, route, position & altitude, distance to go, steady ETA, UTC and local time at the
aircraft position — plus Wi-Fi / internet / satellite / feed status icons.
The same flight view rendered on a LilyGO T3-S3's 128×64 monochrome OLED — one firmware,
per-board panel and layout drivers selected automatically from the eFuse MAC.
Wired GNSS as a position source. Left: a u-blox MAX-M8Q on a Raspberry Pi HAT-format
carrier, jumper-wired to an ESP32-S3 devkit (RX 16 / TX 12 / PPS 21). Right: a NEO-M8N-marked
breakout beside a LilyGO T-Display-S3. Either feeds NMEA 0183 at 9600 baud — no HAT, Pi or shield
electronics required.
AIDlink is a tiny ESP32 project that does three things at once, on the bench:
- A 2.4 GHz-only Wi-Fi access point. The ESP32 radio is 2.4 GHz only, so devices that join it stay off 5 GHz — handy when you specifically want a 2.4 GHz-only hotspot.
- An experimental position feed. It implements the EFB-facing side of an ARINC 834 ADBP interface and streams aircraft position to an EFB client, sourced from an HTTP endpoint that returns position data (see Position sources). Intended for development and interoperability testing only.
- Connection sharing (NAT). It joins an existing upstream Wi-Fi as a client and re-shares that internet to everything on its own AP — one uplink, many devices.
All three run on a single ~$5 ESP32, configured from a built-in web page.
⚠️ EXPERIMENTAL — NOT CERTIFIED, NOT FOR OPERATIONAL USE. The position output has no integrity guarantee, lags reality, and must never be used for navigation of any kind. This is an independent hobby/educational interoperability project, not affiliated with or endorsed by any aircraft, avionics, EFB, airline, or connectivity provider. See DISCLAIMER and TRADEMARKS.
[EFB / tablet] --WiFi WPA2 (2.4GHz)--> [ESP32 SoftAP 172.20.1.1/26] --NAT--> [upstream WiFi] --> Internet
(your AP, e.g. "AIDlink") | ADBP server (ARINC 834): TCP 24000 + Web API HTTP 80 + mDNS
| config/status web UI on HTTP 80
| polls position from a Source URL every N seconds
v
ownship pushed to the EFB's data-stream port
- STA (uplink): joins an existing Wi-Fi as a client (DHCP or static).
- SoftAP (downlink): a WPA2, 2.4 GHz access point at
172.20.1.1/26(the ESP32 radio is 2.4 GHz only). - NAPT:
WiFi.AP.enableNAPT(true)— AP clients reach the internet through the uplink. - ADBP server: answers the EFB Web API (for detection) and ADBP subscriptions, then streams ownship.
- Web config + status:
http://172.20.1.1/orhttp://aidlink.local/.
Current firmware: v1.
Any common ESP32 dev board works — a ~$5 ESP32-DevKitC / NodeMCU-32S / WROOM-32 clone is exactly what this was built and tested on. No extra components, shields, or wiring are needed; the board on its own provides the radio, the config web server, and the NAT router.
| Spec | Requirement | Notes |
|---|---|---|
| SoC | ESP32 (Xtensa dual-core LX6, e.g. ESP32-D0WD) | Original ESP32 family; not required to be S2/S3/C3, but any ESP32 with Wi-Fi works |
| Wi-Fi | 802.11 b/g/n, 2.4 GHz only | The ESP32 radio is 2.4 GHz only by design — that's the "2.4 GHz-only AP" feature |
| Flash | ≥ 4 MB | Firmware is ~1.2 MB (~92% of the default 1.3 MB app partition on a 4 MB board) |
| RAM | 520 KB SRAM (on-chip) | Runs comfortably; ~250 KB free heap at idle |
| USB-serial | CH340, CP2102, or native USB | For flashing + optional serial logging; 115200 baud |
| Buttons | EN (reset) and BOOT (GPIO0) | Standard on dev boards; used only for manual reset / bootloader entry |
| Power | 5 V via USB (or 3.3 V regulated) | ~80–260 mA depending on Wi-Fi activity (AP + STA + NAT) |
| Antenna | PCB or external 2.4 GHz | On-board PCB antenna is fine for bench use |
- ESP32-DevKitC v4 or any ESP32-WROOM-32 / WROOM-32E dev board, 4 MB flash, USB-C or micro-USB.
- Confirmed working: generic WROOM-32 clone with a CH340 USB-serial bridge.
- RAM/PSRAM: external PSRAM is not required.
- Flash size: 8/16 MB boards also work (more headroom); 2 MB boards are too small.
- Some CH340 boards don't auto-run firmware after flashing (esptool prints "Hard resetting" but the app
doesn't start) — tap EN, power-cycle, or reboot from the host with the RTS toggle in
tools/reboot.py. - Serial console is 115200 baud, 8N1.
Toolchain (offline-friendly): arduino-cli + esp32:esp32 core 3.3.10, bundled esptool, Python 3 +
pyserial. FQBN esp32:esp32:esp32. The sketch folder name must match the .ino (firmware/aidlink/aidlink.ino).
arduino-cli core install esp32:esp32 # one-time
arduino-cli compile --fqbn esp32:esp32:esp32 firmware/aidlink
arduino-cli upload --fqbn esp32:esp32:esp32 -p /dev/cu.usbserial-0001 firmware/aidlinktools/flash.sh wraps this and auto-increments the build version. The firmware stamps its build via
#define FW_BUILD ("vNN " __DATE__ " " __TIME__), shown in the web header and logged at boot.
macOS note: esptool's config scan can hit a TCC permission error when run from under ~/Desktop —
build/flash from a directory outside ~/Desktop and set ESPTOOL_CFGFILE to an empty file (tools/flash.sh
does this).
All settings persist in NVS. Key groups:
- ① Uplink Wi-Fi — SSID/pass, DHCP toggle (shows live DHCP values read-only; uncheck for static). A Scan button lists nearby networks (UTF-8 SSIDs supported) to pick from.
- ② Cockpit AP — SSID/pass, hidden flag (2.4 GHz, WPA2).
- 🆔 Aircraft identity — tail/registration, type, Web API version (placeholders by default).
- ③ Network · DHCP · AP radio — AP IP/mask, DHCP pool, lease, client DNS, channel, max clients, ADBP port (24000), data-stream port, NAT toggle, device name (→ mDNS host).
- ④ Position source — a provider selector (Viasat / Panasonic / Custom·test URL), poll-interval slider (0.25–20 s), stale→NCD timeout. Viasat and Panasonic use their official on-board endpoints automatically; Custom points at your own server (Viasat JSON shape). See Position sources.
- ⑤ Emulator — fixed test position — when enabled, the source feed is discarded and the EFB receives a fixed position at the configured lat/lon (for bench testing).
- 🛰 GNSS (wired receiver) — on boards with a serial GNSS attached, selects it as a position source and sets which source is preferred. The choice is a preference, not a lock: whichever source is actually live feeds the EFB, so neither a GNSS dropout nor a feed stall blanks the position. Appears only once a receiver has answered. See the firmware README.
- 🪪 Aircraft identity (fallback) — tail, flight, origin, destination used only for fields the live feed leaves empty; the feed always wins. GNSS carries no identity, so without these a GPS-only unit would show no route, distance-to-go or ETA.
- 🔒 Security — enable/disable the settings login, set username, change password.
- 📡 Device traffic log — auto-refreshing, with a live capture on/off toggle, Copy, and raw
/log.
Defaults: uplink SSID blank (set it), AP AIDlink, AP IP 172.20.1.1/26, ADBP 24000,
data-stream 51000, poll 10 s, source Viasat, settings login admin / password (change it).
The web UI is protected by a clean login page (default admin / password — change it in the Security
card). Credentials are stored as a salted SHA‑256 hash (never plaintext); a random HttpOnly session
cookie is issued, with a "Stay connected" option for a persistent (~30‑day) cookie that survives reboots.
The EFB/ADBP endpoints are not gated so the aircraft interface keeps working without login. This is a
plain‑HTTP device — the password is protected by the WPA2 link, not TLS. Forgot it? Re‑flash or erase NVS to
reset to admin/password.
ADBP (Avionics Data Broadcast Protocol) is one of the protocols defined by ARINC 834 (Aircraft Data Interface Function) for exchanging avionics parameters with an EFB over IP. AIDlink implements the EFB-facing side an Aircraft Interface Device would provide.
The EFB first probes GET /getAPIVersion; without a valid reply it reports "not detected".
Wrapper: <Response commandName="X" returnCode="0" timestamp="ISO8601"> … </Response>. Implemented:
/getAPIVersion, /getWiFiAPStatus, /getAoIPStatus, /getAcarsStatus, /cmdReboot.
- Request/response:
getAvionicParameters(one socket per request). - Continuous subscription:
subscribeAvionicParameterswith<publishport>+<refreshperiod>; on-event variant;unSubscribeby publishport. - Streaming model: the AID is the TCP client — it connects to the EFB on the advertised
publishportand streamspublishAvionicParametersover one persistent socket atrefreshperiod. - Multiple EFBs: subscriptions are deduplicated per client IP;
unSubscribematches IP+port (clients commonly share one publishport). Up to 6 concurrent.
- validity: 0 ND · 1 valid · 2 NCD (no computed data) · 3 FT · 4 NF.
- type: 0 = Float64 · 3 = Float32 · 5 = Signed32 · 6 = Bool · 7 = String · 8 = Date (
YYYY/MM/DD) · 9 = Time (HH:MM:SS). - time = ms since 1970-01-01 UTC, from SNTP when synced.
<?xml version="1.0" encoding="UTF-8"?><method name="publishAvionicParameters" length="N"><parameters>
<parameter name="GPSLATP" validity="1" type="0" value="43.9692" time="1782692413468"/> …
</parameters></method>length = byte count of the <method>…</method> element only (excludes the prolog); the prolog is sent
on every frame; no trailing delimiter.
Discovered by testing against a real EFB and reading its integration log. These are the difference between "detected but no ownship" and a working moving-map dot:
- GPS position is a COARSE+FINE pair and both must share validity. Latitude is reconstructed from
GPSLATP(coarse) +GPSLATPF(fine); longitude fromGPSLONGP+GPSLONGPF. If the fine field is NCD while coarse is valid, the EFB rejects the fix as a "Coarse and Fine status mismatch" and discards every position. → Emit the fine fields asvalidity="1" type="0" value="0.000000"(coarse already carries full precision). Never NCD the fine fields. - Track/heading must be −180…+180, not 0…360. Values > 180 are rejected as out-of-range. → Normalize (subtract 360 when > 180).
- City pair (
FROMTO) is the combined departure+arrival field — there's no separate departure parameter; emit it as<origin><destination>(e.g.LFPGVTBS).
Other observations: unsupported (NCD) parameters log harmlessly as "not supported"; ground speed should be internally consistent with the position motion (AIDlink derives GS from successive fixes and clamps it to a sane range, then dead-reckons the pushed position at 1 Hz so it advances smoothly between source updates).
On boards with a screen (LilyGO T-Display-S3, auto-detected by MAC), the ESP-IDF firmware drives a 320×170 flight display fed entirely by the live position source:
| Row | Shows |
|---|---|
| Top | Tail registration (cyan) · aircraft type (yellow — the resolved performance-profile code, never the raw feed string) · flight number. Until the first fix arrives, an AIDlink · build · IP splash row shows instead. |
| Progress | Trip-completion bar (cyan→green gradient) with the ➤ aircraft marker riding the fill, and percentage — remaining vs. the dep→arr great-circle. |
| Route | Departure ➤ arrival, IATA preferred (NOU ➤ NRT), ICAO fallback · ETA in the destination's local time pinned right (23:50L, green L), DST-aware via the embedded timezone grid. |
| Data | Live coordinates (green) · altitude with the tens/units dimmed so the flight level reads at a glance (310 00 ft). |
| To go | Distance to destination (NM) left · centered ↘ top-of-descent and ◎ estimated arrival times (↘11:42z ◎12:50z) — theoretical-profile based (see below), falling back to the made-good-speed estimator when no aircraft profile is selected. |
| Bottom | UTC (magenta) · status icons (Wi-Fi signal bars · internet globe · feed-activity blip) · UTC offset and local time at the aircraft position (embedded IANA-derived timezone grid, DST-aware, works fully offline). |
Display-less boards show the equivalent state on the onboard RGB LED (red = no Wi-Fi, orange = scanning or no internet, yellow/green = internet with weak/strong signal, magenta blip = location received).
A naive now + distance/GS ETA is undisplayable — and even a windowed made-good estimator can only
react. Like a real FMS, AIDlink instead predicts the whole remaining flight from an aircraft
performance profile and only nudges that prediction with small measured deviations:
- Aircraft profiles (30 types) and a seasonal ERA5 wind climatology (250 hPa, 5°×60° boxes) are
generated at build time from the Offto app's database — read-only, single source of truth
(
tools/gen_perfdb.py→perfdb_data.c, ~5 KB flash). - Profile: 3-segment climb from DB minutes (280/380/Mach·593.7 kt) · cruise at DB TAS with a per-segment wind-triangle over the climatology · descent distance = ceiling/300 NM integrated over the 290 → 250 kt @10 000 ft-above-airport → 180 → 140 IAS schedule (airport-elevation aware) · staged approach over the last 60 NM.
- The only adaptive term: measured made-good speed vs. predicted cruise speed, EMA'd (τ 600 s), clamped ±10 %, applied to the remaining cruise scaled by the fraction of cruise already flown — zero at top of climb, full (still slight) by end of cruise.
- Configured in the portal's ✈ ETA — aircraft performance card (manufacturer → type, statistical-winds switch); a type reported by the position feed auto-selects the matching profile. With no profile selected, the plain made-good estimator remains.
Host-simulated result: 0 displayed-minute drift across an entire cruise under ±25 kt speed oscillation, arrival within seconds of truth; the winter Pacific jet correctly costs ~+21 min NOU→NRT.
Selectable in settings (④). Two on-board moving-map providers are built in, plus a custom URL:
- Viasat —
https://wifi.inflight.viasat.com/ac/flight/info. Nested JSON: each field is"latitude": {..., "value": "-7.98"}. Recognized:latitude, longitude, altitude(ft), groundSpeed(kt), flightNumber, tail_number, originCode, destinationCode, current_time. (No ground speed/track in the feed → derived from successive fixes.) - Panasonic —
http://services.inflightpanasonic.aero/inflight/services/flightdata/v1/flightdata. Flat JSON withtd_id_*keys; lat/lon are 8-digit strings =degrees×1000(values ≥ 80000000 are negative). Provides ground speed and true heading directly. - Custom / test URL — point at your own server, which must return the Viasat JSON shape.
See
examples/flight_info.json.
The device picks the endpoint and parser automatically from the selected provider; the poller derives track, clamps/derives ground speed, and dead-reckons position at the push rate for smooth motion.
A serial GNSS receiver can be attached as an independent position source — no cabin Wi-Fi, no internet, no moving-map provider. Any receiver that emits NMEA 0183 at 9600 baud works; the parser is pure and covered by host tests built from a real capture.
| Receiver | ESP32 | board_t field |
|---|---|---|
TXD (module out) |
GPIO 16 | gps_rx |
RXD (module in) |
GPIO 12 | gps_tx |
PPS (optional) |
GPIO 21 | gps_pps |
GND |
GND | — |
VCC |
3V3 or 5V per module | — |
GPIO 16 and 12 are the only pins free across every board in the fleet — on the classic ESP32-WROVER,
16/17 are the PSRAM lines, so opening a UART there blind is destructive. Pins live in the per-model profile
in firmware-idf/main/board.c, keyed by eFuse MAC, so a board with no receiver stays inert rather than
driving pins blind. Adding a receiver to a board is one line in its profile.
On the GNSS path, ground speed and track are taken verbatim from the RMC sentence (f[7] = SOG knots,
f[8] = COG °true) and passed straight through — no smoothing, no derivation. u-blox computes these from
carrier Doppler rather than position differencing, which is why the data sheet quotes 0.05 m/s velocity and
0.3° heading accuracy.
This differs from the Wi-Fi feed path, which runs derive.c to reconstruct track from successive positions
(EMA-smoothed great-circle bearing) and ground speed from distance/time when the provider omits them:
| Source | Ground speed & track |
|---|---|
| GNSS | RMC fields verbatim — the receiver's own solution |
| Wi-Fi feed | provider's values when present; derived from position deltas when not |
The GNSS setting is a preference, not a lock. Whichever source is actually live feeds the EFB, so neither a GNSS dropout nor a feed stall blanks the position. Because GNSS carries no identity, the manual identity fallback (tail, flight, origin, destination) supplies only the fields the live feed leaves empty — without it, a GPS-only unit would show no route, distance-to-go or ETA.
Fix quality drives the satellite indicator colour, and /status exposes the full receiver state
(present, fix, sats_used, sats_view, hdop, per-constellation counts, pps_us, csum_err).
- u-blox MAX-M8Q on a Pi HAT-format carrier — genuine M8 (
ROM CORE 3.01,hwVersion 00080000,PROTVER 18.00). L1 only, 3 concurrent constellations, 2.5 m CEP, TCXO, PPS at 30 ns RMS. - A NEO-M8N-marked breakout that actually reports M10-class firmware (
hwVersion 000A0000,PROTVER 34.10,ROM SPG 5.10). Consequence: legacyUBX-CFG-NAV5does not exist on it and all configuration must go throughCFG-VALSET. CheckUBX-MON-VERbefore trusting a silkscreen. - Antenna, not receiver, is the usual fault. Active antennas need bias the module may not supply
(MAX-M8Q has
LNA_ENbut noV_ANT), and patch antennas are directional — the labelled face is the base.UBX-MON-HWis the diagnostic that matters: low AGC = too much input power, high AGC = no signal reaching the front end, and a frozen AGC across an antenna swap means the RF path is open.aStatus=OKproves nothing unlessCFG-ANTshows open-detect enabled.
Full receiver specification, wiring detail and diagnostics: GPS-HARDWARE.md.
- The firmware mirrors a 90-line ring buffer to USB serial and to the web
/log. Verbose detail (full frames, per-fix derivation) goes to serial only. A Live capture toggle andlogEnablesetting gate it. - Serial commands:
L= dump ring buffer,C= clear. tools/tee_serial.pyopens the USB port once and tees the live serial stream toaidlink_serial.logfor read-on-demand debugging without rebooting the board.- Success signal: the per-connection push counter climbs past
#10with nounSubscribe/ client close.
firmware/aidlink/aidlink.ino # Arduino firmware (proven/shipping): WiFi/NAT, poller, ADBP, web UI
firmware/aidlink/defs.h # Config / PosState / Sub structs
firmware-idf/ # ESP-IDF rewrite — dual-target (esp32 + esp32s3) + USB-cable networking
firmware-idf/README.md # build/flash, USB-NCM cable, source layout, host tests
tools/flash.sh # Arduino: compile + upload (auto-version; macOS TCC-safe)
tools/tee_serial.py # background serial logger
tools/reboot.py # reboot the board from the host (RTS toggle)
examples/flight_info.json # sample position-source response
GPS-HARDWARE.md # GNSS receiver specs, wiring, and RF fault isolation
ESP32-BOARDS.md # per-board silicon specs and USB-networking feasibility
LICENSE # Apache-2.0
Two firmwares:
firmware/aidlink/is the original Arduino sketch (the proven build).firmware-idf/is the ESP-IDF rewrite that additionally turns an ESP32-S3's USB-C cable into a network link (DHCP + NAT to the uplink, coexisting with the Wi-Fi AP) — seefirmware-idf/README.md.
Newest first. Dates are the working dates in this repository's history.
- Wired GNSS brought up on the ESP32-S3 devkit (Board 2):
PROF_S3_DEVKITgivengps_rx=16 / gps_tx=12 / gps_pps=21. Data path verified end-to-end —gps.present=truewithcsum_err=0, which validates jumper routing, TX/RX orientation and baud in a single check. - Uplink Wi-Fi and cockpit AP passwords are now shown in clear text in the settings page. Both values were already sent to the browser; masking only hid them visually, and made typos undetectable when entering a passphrase off a placard. The portal login password stays masked.
- New
GPS-HARDWARE.md— full MAX-M8Q specification, carrier-board pinout, jumper routing, ESP32 wiring, and an RF fault-isolation procedure built fromUBX-MON-HW/CFG-ANT. - Flashing note: Board 2 cannot be flashed over native USB —
/dfureturns 200 but the ROM downloader never enumerates a CDC port. Use its second USB port (WCH CH343); esptool auto-reset works there with no BOOT+RST, and the stub is safe.
- Pure NMEA 0183 parser with host tests built from a real capture.
- UART1 receiver task, PPS edge counter, per-board pin profiles.
- Source arbitration with fallback and identity precedence — preference, not lock.
- Portal controls for the GNSS source and the manual identity fallback; persisted to NVS.
- Satellite indicator coloured by fix quality; per-constellation "used" counts and settings tiles.
- User-key gestures — hold swaps source, tap shows GPS detail.
- Fix dimension gated on GGA quality; PPS health tightened.
- Flashing:
--no-stubis mandatory on the USB-OTG ROM — the stub does not survive it. Hands-off reflash script added for the native-USB S3 boards.
- Raised the lwIP socket pool. ADBP push was being starved, and the EFB lost position in flight.
- Registered a second T3-S3; split per-model profiles from the MAC map.
- LilyGO T3-S3 support with segregated panel/layout drivers (SSD1306 OLED alongside the ST7789 LCD).
- Stable Viasat poll: keep-alive TLS, octal PSRAM, fetch diagnostics.
- Internet probe via HTTP
generate_204instead of a blocked port-53 handshake.
- Orthodromic + vertical-profile rework; FMS-steady ETA display; measured A20N profile.
- Real-flight replay tooling — see
docs/eta-estimator.md. - Display polish: local-ETA badge, estimator palette, cloud icon for internet reachability.
Licensed under the Apache License 2.0 — see LICENSE and NOTICE. You may use, modify, and redistribute it, including commercially, under the terms of that license, which also includes an explicit patent grant. Apache-2.0 does not grant trademark rights — see TRADEMARKS.
This project is provided for educational and experimental purposes, as-is and without warranty of any kind (see the Apache-2.0 "Disclaimer of Warranty"). It is not an approved, certified, or airworthy device, and is not affiliated with, endorsed by, or derived from any aircraft, avionics, EFB, airline, or connectivity manufacturer or their documentation. It targets ARINC 834 ADBP, an industry interoperability standard, purely as a descriptive reference — no claim of conformance or certification is made. The position output is non-authoritative and must never be relied upon for navigation. You are responsible for complying with all applicable laws and regulations and with the operating rules of any network or aircraft you connect it to.