+
It flies itself out of the box. A scripted circuit drives the attitude so the whole
+ layout is judgeable without any sensor at all. Drag anywhere on the display to fly it
+ by hand instead. The annunciator on the attitude indicator always names which source is driving it.
+
+
If “Enable phone sensors” does nothing
+
That is expected here, and the diagnostics panel above says why. This page runs inside an iframe,
+ and deviceorientation is gated by Permissions Policy: in a cross-origin frame it silently
+ never fires unless the parent sets allow="gyroscope; accelerometer; magnetometer", which a
+ page cannot grant itself. Desktop Chrome has no motion sensors to begin with.
+
Three ways to get real attitude:
+
— Desktop Chrome: DevTools → ⋮ → More tools → Sensors, then set
+ a custom orientation. It drives deviceorientation and this page will pick it up and flip
+ the annunciator to PHONE AHRS.
+ — Your phone, properly: save this page as an .html file and serve it
+ over HTTPS or from localhost as a top-level page, not framed.
+ — Or just don't. For judging layout, the demo flight is better than real sensors
+ anyway, because it exercises bank angles and climb rates you would not get waving a phone around.
+
+
What is real when sensors do work
+
Attitude, heading, track, ground speed, GPS altitude and position come from the device.
+ The attitude is low-pass filtered, because raw phone fusion is too jittery to look at even sitting
+ still on a desk — which is an early and not especially encouraging data point for a question the
+ PRD lists as open.
+
+
What's simulated
+
Everything on the engine strip, plus indicated airspeed, pressure altitude and vertical speed.
+ Those are node channels and no node exists yet. Values are plausible PM-2 cruise numbers.
+
+
Where design rule 8 went
+
Not twenty small tags. Real avionics annunciate a degraded or alternate source on the instrument
+ itself, so that's what this does: PHONE
+ AHRS · ADVISORY sits on the attitude indicator,
+ cyan labels mark node-plumbed channels, and
+ magenta marks GPS-derived data, which is already
+ the convention every pilot reads without thinking. Same rule, native idiom.
+
+
Worth saying plainly
+
A full PFD makes phone attitude look authoritative, and it isn't. PRD section 19 rejects phone
+ attitude for a control loop because OS fusion is tuned for handheld use and drifts in sustained turns,
+ and section 15 still lists “phone attitude is stable enough on a vibrating airframe to be
+ worth showing” as an open question. That question is exactly what this layout is for —
+ put it on the aircraft, run the engine, and see whether the horizon is steady enough to keep.
+ Design rule 2 stands either way: the mechanical gauges stay installed.
+
+
Layout mockup only. Not an instrument, not airworthy, not the Android client.
+
+
+
+
+
+
+
+
diff --git a/app/mockup/manifest.webmanifest b/app/mockup/manifest.webmanifest
new file mode 100644
index 0000000..f74dccc
--- /dev/null
+++ b/app/mockup/manifest.webmanifest
@@ -0,0 +1,33 @@
+{
+ "name": "Junco PFD Layout Mockup",
+ "short_name": "Junco PFD",
+ "description": "Primary flight display layout mockup for the Junco open flight computer. Not an instrument.",
+ "start_url": "./index.html",
+ "scope": "./",
+ "display": "fullscreen",
+ "display_override": ["fullscreen", "standalone"],
+ "orientation": "landscape",
+ "background_color": "#07090B",
+ "theme_color": "#07090B",
+ "categories": ["utilities"],
+ "icons": [
+ {
+ "src": "./icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "./icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "./icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/app/mockup/sw.js b/app/mockup/sw.js
new file mode 100644
index 0000000..73df329
--- /dev/null
+++ b/app/mockup/sw.js
@@ -0,0 +1,60 @@
+/* Junco PFD layout mockup — service worker.
+ *
+ * The only job here is offline. A flight instrument that stops working when the
+ * cell signal drops is not an instrument, and at 800 feet over a field the
+ * signal drops routinely. Cache the shell on install, serve it from cache
+ * first, and never depend on the network in the air.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+var CACHE = "junco-pfd-v1";
+
+var SHELL = [
+ "./",
+ "./index.html",
+ "./manifest.webmanifest",
+ "./icon-192.png",
+ "./icon-512.png"
+];
+
+self.addEventListener("install", function (e) {
+ e.waitUntil(
+ caches.open(CACHE).then(function (c) {
+ return c.addAll(SHELL);
+ }).then(function () {
+ return self.skipWaiting();
+ })
+ );
+});
+
+self.addEventListener("activate", function (e) {
+ e.waitUntil(
+ caches.keys().then(function (keys) {
+ return Promise.all(keys.map(function (k) {
+ return k === CACHE ? null : caches.delete(k);
+ }));
+ }).then(function () {
+ return self.clients.claim();
+ })
+ );
+});
+
+self.addEventListener("fetch", function (e) {
+ if (e.request.method !== "GET") { return; }
+ e.respondWith(
+ caches.match(e.request).then(function (hit) {
+ if (hit) { return hit; }
+ return fetch(e.request).then(function (res) {
+ // Cache same-origin successes so a first online run primes everything.
+ if (res && res.status === 200 && res.type === "basic") {
+ var copy = res.clone();
+ caches.open(CACHE).then(function (c) { c.put(e.request, copy); });
+ }
+ return res;
+ }).catch(function () {
+ return caches.match("./index.html");
+ });
+ })
+ );
+});
diff --git a/docs/open-questions.md b/docs/open-questions.md
new file mode 100644
index 0000000..88d19a9
--- /dev/null
+++ b/docs/open-questions.md
@@ -0,0 +1,118 @@
+# Open questions
+
+Everything not yet decided, in one place. Previously this was scattered across
+five documents, which meant nobody could tell how much was actually open.
+
+**The rule for this file:** a question leaves it by being answered in the
+document it belongs to, not by being answered here. This is an index of open
+work, not a place decisions live.
+
+Last reviewed: August 2026, PRD revision 5.
+
+---
+
+## Needs a measurement
+
+These cannot be answered at a desk. PRD section 15 holds the full table with the
+risk attached to each; this is the summary.
+
+**Nine of the twelve close in phases 0 and 1, and phase 0 needs no firmware at
+all.** It is an ESP32, one barometer, a breadboard, and ground runs at every RPM.
+
+The cheapest instrumentation available: add one thermocouple and a logging
+laptop to the EMI ground runs already planned, and four questions close at once.
+
+| Question | Phase |
+|---|---|
+| BLE link survives twin CDI ignition at the mount point | 0 |
+| I2C survives the same environment | 0 |
+| Enclosure temperature stays in range on the engine cage | 0, plus a summer |
+| Ground idle does not heat-soak the enclosure before takeoff | 0 |
+| A printed static plenum gives usable vario data in prop blast | 1 |
+| A cooling inlet near the pitot does not couple into the plenum | 1 |
+| Phone holds BLE and headset audio simultaneously | 1 |
+| A dash-mounted phone does not thermally shut down in summer sun | 1 |
+| Phone attitude is stable enough on a vibrating airframe to show | 1 |
+| Magnetic float fuel sensing survives vibration and slosh | 3 |
+| A pitot can be placed usefully on a powered parachute | 4 |
+| Phone holds BLE, USB OTG to an SDR, and headset audio at once | Traffic work |
+
+The Pi-class build in PRD section 23 lives or dies on the two thermal rows.
+
+---
+
+## Blocked on a prior decision
+
+Answering these before the thing they depend on is wasted work.
+
+| Question | Blocked on | Where |
+|---|---|---|
+| Two engines: separate node IDs or an engine index field? | The bus choice | `spec/dronecan-engine-extension.md` |
+| Does fuel endurance belong in the engine namespace or its own? | The bus choice | `spec/dronecan-engine-extension.md` |
+
+**The bus choice itself** — DroneCAN or CAN-FIX — is deliberately deferred to
+v2, because v1 has no bus and no second node. PRD section 24 records the
+argument and leans CAN-FIX: its consumers are experimental aircraft panels
+rather than autopilots, and its specification is Creative Commons.
+
+---
+
+## Open, decidable, not urgent
+
+| Question | Where | Note |
+|---|---|---|
+| Full node status payload beyond transition counts | `spec/ble-telemetry.md` | Uptime, supply voltage, SD state, free space are candidates |
+| What the BLE advertisement carries | `spec/ble-telemetry.md` | Device name convention, whether build class is visible before connecting |
+| Rate class payload exceeding the negotiated MTU | `spec/ble-telemetry.md` | Reachable by a many-cylinder aircraft. The PM-2 does not reach it |
+| Configuration characteristic read semantics | `spec/ble-telemetry.md` | Current hash, last validation result, or both |
+| TOML key names and file section structure | `spec/aircraft-profile.md` | |
+| Binary profile field layout | `spec/aircraft-profile.md` | Shared with `ble-telemetry.md` |
+| Behavior with no valid profile at all | `spec/aircraft-profile.md` | A freshly built unit that has never been configured |
+| CRC polynomial and width | `spec/log-format.md` | |
+| Magic header value and record alignment | `spec/log-format.md` | The recovery scanner depends on both |
+| CSV and GPX export mapping | `spec/log-format.md` | Deferred deliberately. A `tools/` concern that cannot cost field hardware |
+
+---
+
+## Needs the maintainer
+
+| Question | Note |
+|---|---|
+| `ESP32-S31` | Appears three times in `docs/prd.md`, sections 7, 19, and 25. There is no such Espressif part. Section 19 says it "shipped two months after the S3 was selected" and section 7 credits it with Bluetooth Classic support, which narrows it, but the intent is not recoverable from the text |
+
+---
+
+## Stewardship
+
+From `MAINTAINERS.md`, in the order worth doing them.
+
+1. **Name a co-maintainer.** This is the one that makes the others matter. A
+ single-maintainer project is a project with a scheduled end date, and every
+ other item on this list assumes someone is there to act on it
+2. **Move to a GitHub organization with two owners.** Currently a personal
+ account, which PRD section 17 already says is insufficient
+3. **Mirror to a second forge**
+4. **Zenodo DOI on a tagged release.** Cheap, and can wait for something to tag
+5. **OSHWA certification.** Free and self-certified
+6. **Fiscal host for donations.** Only if donations actually appear
+
+---
+
+## Recently closed
+
+Kept briefly so the next reader can see the trajectory rather than assuming
+these were never considered. Full rationale lives in the specs.
+
+**Irreversible, decided in revision 5:** the BLE UUID base is generated and
+frozen; the log is a preallocated file on FAT32 rather than a raw partition; the
+profile hash is SHA-256 over the stored bytes rather than a re-serialization.
+
+**Also closed in revision 5:** integer SI channel encoding with no floats and no
+display units on the wire; whole-profile atomic configuration writes; "in
+flight" defined as the log file being open; node-side channel transition counts;
+bonding required to write but not to subscribe; two concurrent clients with
+first-clock-write-wins; profile chunking and encoding; TOML source plus a
+compiled binary form; schema versioning that refuses rather than guesses;
+validation rules; `uint8` record type allocation; ArduPilot-style format
+descriptors; log payloads byte-identical to BLE payloads; and seizure precursors
+publishing a raw rate rather than a computed judgment.
diff --git a/docs/prd.md b/docs/prd.md
index f50eaa9..6c62e70 100644
--- a/docs/prd.md
+++ b/docs/prd.md
@@ -2,11 +2,17 @@
**Open engine and air data node for Part 103 and experimental aircraft**
-Status: Draft, revision 2
+Status: Draft, revision 5
Target aircraft: ParaPlane PM-2 (twin engine powered parachute)
Target publish: AirVenture 2027
-Revision 2 moves position and attitude sensing onto the pilot's phone, moves the link to Bluetooth Low Energy, and reduces the node to engine and air data only. Section 22 records what changed and why.
+Revision 2 moved position and attitude sensing onto the pilot's phone, moved the link to Bluetooth Low Energy, and reduced the node to engine and air data only.
+
+Revision 3 adds traffic as a pluggable app-side channel supplied by hardware the pilot already owns, and opens the compute platform to a Linux single-board variant.
+
+Revision 4 relicenses to copyleft, drops the commercial roadmap, and positions Junco as the engine and air data front end for the MakerPlane stack rather than a parallel instrument system.
+
+Revision 5 closes the open specification questions, adds design rule 9, and moves the remaining ones into `docs/open-questions.md`. Section 25 records what changed and why.
---
@@ -32,6 +38,7 @@ These constrain the design. They are not disclaimers.
6. **No claim of crash survivability.** It is a flight data logger, not a black box.
7. **A unit always declares what it is.** Firmware reports build class, meaning self-built, kit-built, or factory-qualified, along with hardware revision and calibration date. Assurance differs enormously across those and the name sits on all of them.
8. **Every channel declares its source.** A value derived from the phone's barometer and a value derived from a plumbed static plenum are not interchangeable, and the display, the log, and the protocol must all say which one produced a given reading. New in revision 2, and load-bearing: the architecture now mixes two sensor platforms of very different quality.
+9. **Advisory-only data never raises an alert.** A channel whose coverage or latency cannot be relied on may be displayed, marked as what it is, but may not drive audio, the annunciator, or any advisory. Internet-sourced traffic is the case this rule was written for, and section 22 explains why that data looks authoritative and is not. New in revision 5.
---
@@ -47,7 +54,7 @@ These constrain the design. They are not disclaimers.
- Custom PCB
- Distributed multi-node CAN network
-- ADS-B receive
+- **A Junco-built ADS-B receiver.** Junco does not demodulate ADS-B and is not planned to. Displaying traffic from a receiver the pilot already owns is in scope as an optional channel. See section 22
- GDL90 output. Demoted to phase 2, see section 14
- iOS native application. The protocol supports it, the app does not exist yet
- Angle of attack
@@ -136,6 +143,8 @@ No custom board. An off the shelf ESP32-S3 development board on a screw terminal
**Compute is specified by requirement, not part number.** The module must provide: two cores, PSRAM, Bluetooth Low Energy, a native CAN 2.0B controller, SD, and at least 20 usable GPIO. Wi-Fi is required for the on-demand AP mode but is not on the flight-critical path. The ESP32-S3 is v1's reference implementation, not a dependency.
+**A Linux single-board variant is a supported second build**, not a fork. It changes what the node can do and what the enclosure has to do. Section 23 records the full trade and the requirements a Pi-class build must meet.
+
**Bluetooth Classic is not required.** Revision 1 noted the ESP32-S31's Classic BR/EDR support as a reason to prefer it. Choosing BLE removes that reason. See section 14.
**Power:** node draws roughly 120 to 160 mA at 5V with BLE rather than Wi-Fi as the active radio. The phone draws far more. One bank powers both.
@@ -205,6 +214,8 @@ Fuel is the highest risk channel and the one most likely to differ per aircraft.
**Reference client:** Android native, open source, APK published on GitHub releases and F-Droid. No store dependency, no developer account, no expiry.
+**Panel display:** pyEFIS, reached through a FIX-Gateway plugin, for builds that have a panel and a Pi. See section 24. The Android app stays the reference client and the only one required in v1, because the primary aircraft has no panel.
+
**iOS:** the protocol supports it because BLE is available to third-party iOS apps. No app exists in v1, and iOS is not on the critical path of a project meant to outlive its maintainer.
### Audio
@@ -279,6 +290,8 @@ Contents:
**The profile lives on the node**, not on the phone, so a borrowed phone or a replacement tablet inherits the correct configuration by connecting. The app reads it over BLE on connect and caches it against the profile hash.
+**The profile describes the aircraft and nothing else.** Configuration that describes the pilot's own equipment stays app-side, on the phone. Traffic backend selection in section 22 is the current example: it is a property of what is in the flight bag, it changes without the aircraft changing, and the node is not in that data path. Putting it in the profile would make the node authoritative over something it cannot see.
+
---
## 12. Logging
@@ -325,7 +338,8 @@ The architecture change helps here. Position and time now come from the phone, w
| Junco BLE GATT telemetry | Node to phone | **Required in v1. Separate specification document** |
| Junco log record format | On card and on phone | Required in v1. Published with recovery tool |
| Wi-Fi AP, on demand | Bidirectional | Required in v1 for log pull, config, and firmware only. Never in flight |
-| GDL90 over UDP 4000 | Node to EFB | **Phase 2.** Arrives with ADS-B |
+| GDL90 over UDP 4000, outbound | Node to EFB | **Phase 2.** Junco publishing its own channels to a third-party EFB |
+| GDL90 over UDP 4000, inbound | Receiver to phone | **Optional channel.** Traffic and weather from a receiver the pilot owns. See section 22 |
| DroneCAN engine and fuel extension | Bus | Separate specification document. Not implemented in v1 |
### Why BLE and not Bluetooth Classic
@@ -340,7 +354,17 @@ BLE is available to third-party apps on both platforms, carries telemetry comfor
**GDL90 to a third-party EFB.** GDL90 is UDP over Wi-Fi, and the node cannot usefully hold a BLE link and a Wi-Fi AP in flight on one 2.4 GHz radio. Revision 1 treated free display in ForeFlight and Avare as a major benefit. That is given up.
-It costs less than it appears. The main thing GDL90 delivered was position into the EFB, and the phone now has its own position, so an EFB works normally alongside the Junco app. What GDL90 still uniquely delivers is ADS-B traffic and weather, and that arrives in phase 2 with the receiver, at which point the Wi-Fi path is worth turning on.
+It costs less than it appears. The main thing GDL90 delivered was position into the EFB, and the phone now has its own position, so an EFB works normally alongside the Junco app. What GDL90 still uniquely delivers outbound is Junco's own channels reaching a third-party display, and that stays in phase 2.
+
+### Consuming GDL90 is not the same as emitting it
+
+These are two different features that share a format, and conflating them is what kept traffic looking expensive.
+
+**Emitting** GDL90 requires the node to run a Wi-Fi AP in flight, which it cannot usefully do while holding BLE. That is the phase 2 problem described above.
+
+**Consuming** GDL90 requires nothing of the node at all. The receiver is the pilot's, the transport is the receiver's, and the listener is a UDP socket in the app. The node is not in the path, is not aware of it, and cannot be affected by it. Design rule 1 is preserved without any argument, because there is nothing to argue about.
+
+That asymmetry is why traffic display does not have to wait for phase 2 and does not have to wait for a Junco receiver that is never going to be built.
### BLE requirements
@@ -367,6 +391,10 @@ These are assumptions until a phase closes them.
| Phone attitude is stable enough on a vibrating airframe to be worth showing | Phase 1 | Drop the attitude display rather than show a bad one |
| Magnetic float fuel sensing survives vibration and slosh | Phase 3 | Fall back to burn integration only |
| A pitot can be placed usefully on a powered parachute | Phase 4 | Ship without airspeed, document why |
+| A ram-air-cooled enclosure holds a Pi-class board inside its range on an engine cage | Phase 0, plus a summer season | Pi build restricted to a cockpit mount, or dropped |
+| Ground idle on a hot day does not heat-soak the enclosure before takeoff | Phase 0 ground runs | Thermal mass, a shroud, or a documented warm-up limit |
+| A cooling inlet near the pitot does not couple into the static plenum | Phase 1 | Altitude and vertical speed corrupted by cooling airflow |
+| The phone holds BLE to the node, USB OTG to an SDR, and audio to a headset at once | Traffic work | Traffic falls back to a separate receiver over Wi-Fi |
---
@@ -385,17 +413,25 @@ These are assumptions until a phase closes them.
Phase 0 gains a test. BLE link stability under twin CDI ignition is now flight-relevant in a way an I2C bus alone was not, because the link carries every value the pilot sees.
+Phase 0 gains thermal instrumentation as well. If the Pi-class build is going to be viable on an engine cage, the enclosure temperature during ground runs and after a hot shutdown is the measurement that decides it, and it costs one logged channel to collect while the EMI runs are happening anyway.
+
+**Traffic is not a phase.** It is an optional app-side channel that can be built whenever someone wants it, because it blocks on nothing in this table and touches no node hardware. It should not be allowed to displace phases 1 through 4, which are the ones that produce an instrument.
+
---
## 17. Licensing and stewardship
| Artifact | License |
|---|---|
-| Firmware | MIT or Apache 2.0 |
-| Android app | MIT or Apache 2.0 |
-| Board files, STLs | CERN-OHL-P-2.0 |
+| Firmware | GPL-2.0-or-later |
+| Android app | GPL-2.0-or-later |
+| Board files, STLs | CERN-OHL-S-2.0 |
| Documentation and specs | CC-BY-4.0 |
+Code and hardware are copyleft. Specifications are not, deliberately: they are meant to be implemented by anyone in anything, and a protocol that cannot be adopted freely does not outlive its implementation.
+
+GPL v2 **or later** matches MakerPlane, so code moves in both directions between Junco and FIX-Gateway or pyEFIS without relicensing. See section 24.
+
Stewardship requirements, driven by the goal of outliving the maintainer:
- GitHub organization with at least two owners, not a personal account
@@ -426,8 +462,13 @@ Successors break what they do not understand the reason for. Every rejection bel
| Considered | Rejected because |
|---|---|
| Fork XCVario or GNUVario | Firmware is coupled to their dedicated hardware, their differential pressure parts are kilopascal-class against our sub-100 Pa dynamic pressure, and no board has thermocouple or isolated pulse inputs. Reuse their protocols, not their codebase |
-| Build on ArduPilot | GPL-3.0 rewrites the licensing plan, the parameter surface runs to hundreds of entries, it is architected for control rather than instrumentation, and the community is uneasy about manned use. Steal the parameter model and the self-describing log format instead |
-| PWA as the primary client | Cannot bind a socket to a specific network, and cannot reach BLE on iOS at all. Native is required by the transport, not by preference |
+| Build v1 on the ArduPilot flight stack | The parameter surface runs to hundreds of entries, it is architected for control rather than instrumentation, and the community is uneasy about manned use. Steal the parameter model and the self-describing log format instead. Note this rejects the flight stack, which is not the same artifact as the row below |
+| Build v1 on ArduPilot AP_Periph | AP_Periph defeats half of the objection above, being a genuinely publish-only DroneCAN sensor node, and it is a real candidate for the v2 bus stage. It fails v1 on four other grounds: no BLE at all, STM32 only so neither of our compute paths qualifies, no thermocouple or ignition-pulse tach support, and its EFI backends talk to an ECU over serial, which a two-stroke on CDI does not have |
+| Rebuilding what MakerPlane already has | FIX-Gateway already brokers avionics data from arbitrary sources, pyEFIS already displays it, and plugins already exist for ADS-B, recording, annunciation, and multi-source voting. Junco writes the two-stroke engine front end nobody has and plugs into the rest. See section 24 |
+| Absorbing Junco into MakerPlane entirely | The engine and air data work needs its own hardware, specs, and test program, and a Part 103 powered parachute is a narrow enough target that it would be a poor fit for a general E-AB project's roadmap. Stay separate, contribute the plugin upstream |
+| CAN-FIX as a v1 requirement | v1 has no bus and no second node. CAN-FIX becomes the leading v2 candidate over DroneCAN, because its consumers are experimental aircraft panels rather than autopilots, and its specification is Creative Commons so implementing it costs nothing legally |
+| PWA as the primary client | Narrower than it first looked, and still correct. Web Bluetooth reaches the node on Android and would genuinely work there, so the transport objection now applies to iOS only. What decides it is duration: a PWA needs a user gesture per connection, has no background operation, and drops the link when the page is suspended. Holding a link for a two hour flight is what native satisfies and a web page does not. The socket binding objection now scopes to the on-demand Wi-Fi AP alone, since revision 2 moved the in-flight link to BLE |
+| A PWA as a prototyping vehicle | **Not rejected. Adopted for exactly that**, in `app/mockup`. It installs in seconds with no store account, signing key, or toolchain, which is what makes a layout question answerable the same afternoon it is asked. Nothing in it is on the path to the shipping client except the layout decisions it settles |
| Bluetooth Classic SPP | iOS blocks it for third-party apps without MFi. Locks the protocol to Android, not just the app |
| Wi-Fi as the in-flight link | Joining the node's AP costs the phone its cellular data, and the node cannot usefully run AP and BLE together in flight |
| Phone barometer as the vario source | Its port vents into the phone case in the slipstream, so vertical speed would report throttle position |
@@ -442,25 +483,32 @@ Successors break what they do not understand the reason for. Every rejection bel
| Onboard lithium cell | A cell in a vibrating enclosure next to gasoline, to replace a USB-C input from a bank the phone requires anyway |
| Hard-specifying the compute module | The ESP32-S31 shipped two months after the S3 was selected. Specify by requirement, name a reference implementation |
| Unified stale-data handling | Holding the last value is correct for a display and dangerous on a link. Two consumers, two behaviors, deliberately |
+| Decoding ADS-B on the ESP32-S3 node | Its USB OTG is full speed, 12 Mbps. An RTL-SDR at 2.4 MSPS is roughly 38 Mbps of raw I/Q, and practical full-speed bulk throughput is nearer 8 Mbps. The bus is short by a factor of four before any demodulation, and a 240 MHz Xtensa could not demodulate it anyway |
+| Building a Junco ADS-B receiver | A Pi-class board and a dongle is Stratux, which already exists, is open, is documented, and costs about $50. Rebuilding it spends the budget twice and inherits a maintenance burden someone else is already carrying |
+| ForeFlight Sentry as a supported receiver | It does not emit GDL90 to third-party apps and is iOS only. A supported receiver has to speak an open protocol, which is the same standard we hold ourselves to |
+| Internet-sourced traffic as an alerting source | Crowdsourced ground receivers are line of sight and thin below 1000 to 2000 ft AGL, which is exactly where this aircraft lives, and 5 to 15 seconds of latency displaces a target by roughly a third of a mile |
+| Rejecting a Linux single-board node on its 0 to 50C rating | That is an enclosure and airflow problem, on an aircraft already routing a pitot line to the same location. Solve it with insulation, self-heating, and ram air rather than with a different processor. See section 23 |
---
-## 20. Product roadmap beyond v1
+## 20. Roadmap beyond v1
-v1 is a DIY kit. It is not the end state. The architecture is chosen so later stages do not require redesign.
+**This project is not building a product line.** Its goal is to put a working, documented, reproducible engine and air data node into the world under a license that keeps it there. Revenue is not an objective and no stage below is a business plan.
-| Stage | Form | Who builds it |
+| Stage | Form | Who |
|---|---|---|
-| v1 | Breadboard node, phone as hub over BLE, published files, kits at cost | Allen, plus builders reproducing from documentation |
-| v2 | Custom carrier board, DroneCAN bus, separate annunciator and sensor nodes, ADS-B receive, GDL90 restored over Wi-Fi | Allen, plus anyone selling assembled units |
-| v3 | Boxed product. Assembled, calibrated, warrantied, harness included | A production partner |
-| v4 | Autopilot node subscribing to the Junco bus | A production partner with a test program |
+| v1 | Breadboard node, phone as hub over BLE, published files, kits at cost | This project |
+| v2 | Custom carrier board, CAN bus, FIX-Gateway plugin upstreamed, separate annunciator node | This project. The intended end point |
+| v3 | Boxed product. Assembled, calibrated, warrantied, harness included | Anyone who wants it. Not pursued here |
+| v4 | Autopilot node subscribing to the Junco bus | Anyone with a test program. Not pursued here |
-**Why the sequence is ordered this way.** Each stage removes a dependency on one person. Kits depend on documentation quality. A boxed product depends on manufacturing and support capacity, which is why it goes to a partner. An actuating product depends on a test program and product liability insurance, neither of which a hobby project can supply.
+**Why the project stops at v2.** A boxed product depends on manufacturing and support capacity. An actuating product depends on a test program and product liability insurance. Neither is something this project intends to acquire, and pretending otherwise is how a volunteer project takes on obligations it cannot meet.
+
+**Why v3 and v4 are still described.** The architecture should not foreclose them, and someone will eventually want them, so recording what they require is more useful than pretending they do not exist. Copyleft means anyone who takes those stages passes the result on under the same terms, which is the outcome this project wants from them anyway.
**What stands between v2 and an autopilot.** Junco has no attitude solution of its own, and the phone's is not one. A control loop needs reliable AHRS, and getting usable attitude off an IMU bolted to a two-stroke airframe is a real project: vibration isolation, filter design, and validation against a truth source. Assume AHRS is a full stage, not a checkbox on the autopilot stage. The revision 2 architecture makes this clearer rather than closer, because borrowing the phone's attitude for display deliberately does not produce an attitude source anything can be flown by.
-**What does not change across stages.** The BLE protocol, the log format, the configuration schema, and the licensing. Those are what let a partner take over production without the project forking.
+**What does not change across stages.** The BLE protocol, the log format, the configuration schema, and the licensing. Those are what let someone else take on a later stage without the project forking.
---
@@ -485,11 +533,11 @@ The target product is a single small enclosure that bolts to the frame, is calib
| Rung | What it means | When it applies |
|---|---|---|
| Nothing | No requirement at all | Part 103 and experimental amateur-built. The entire v1 and v2 market |
-| Environmental qualification | DO-160 style testing for temperature, altitude, vibration, humidity, and EMI, self-declared | The meaningful milestone. Real engineering credibility, and what a production partner will ask for |
+| Environmental qualification | DO-160 style testing for temperature, altitude, vibration, humidity, and EMI, self-declared | The meaningful milestone. Real engineering credibility, and the first thing anyone taking this to a product will be asked for |
| ASTM consensus standard | Compliance with the applicable LSA equipment standard | Only if an S-LSA manufacturer wants factory installation. Pursue on demand |
| TSO | Full FAA technical standard order, DO-178C software, DO-254 hardware | Certified aircraft. Not this market. Naming it as a goal is how projects like this die |
-**Target the second rung.**
+**Target the second rung**, and treat it as an engineering standard to build against rather than a credential to obtain. This project is unlikely to fund a full environmental campaign, but designing as though one were coming is what makes the difference between a node that survives a season and one that does not. It also leaves the work in a state where someone pursuing v3 starts from a real design rather than a rewrite.
### Two risk profiles, one name
@@ -499,7 +547,234 @@ A builder-assumes-risk notice works reasonably against the builder. It does not
---
-## 22. Revision history
+## 22. Traffic and ADS-B In
+
+Traffic is defined the way fuel is defined in section 9: **a channel with pluggable backends**. The app sees targets. It does not see a method.
+
+Unlike fuel, the selection is **app-side configuration and not part of the aircraft profile**, per section 11. A receiver lives in the flight bag rather than on the airframe, and the node cannot see it.
+
+**The node is never in the path.** Every backend below terminates at the phone. The node does not receive, decode, relay, or know about traffic. This is not a restriction that had to be negotiated, it is a consequence of the receiver being someone else's hardware, and it means design rule 1 holds without needing to be defended.
+
+### Backends
+
+| Backend | Pilot cost | Transport | Notes |
+|---|---|---|---|
+| USB SDR on the phone | $30 to $40 per band | USB OTG | Android only, permanently. Needs a powered OTG hub and a real antenna |
+| Portable receiver | $210 to $850 | GDL90 over Wi-Fi | Preserves a future iOS path. Stratux is the reference |
+| Internet feed | Free | Cellular | Advisory layer only. Never alerts |
+| OGN and FLARM | Free | Cellular | Only worth enabling near glider operations |
+
+**1090 ES and 978 UAT are separate radios.** Dual band means two dongles or a receiver that does both. In the United States, FIS-B weather and TIS-B traffic are carried on 978 only, so a 1090-only build gets direct traffic and nothing else.
+
+**ForeFlight Sentry is excluded** and the exclusion should be stated plainly in the documentation, because it is the most commonly recommended portable and it does not emit GDL90 to third-party apps.
+
+### Two rules
+
+1. **Every target carries its source tag**, per design rule 8. A target decoded from a local receiver and a target pulled from an internet feed are not the same kind of object and must never render identically.
+2. **Internet-sourced traffic never generates an alert**, per design rule 9. It is a map layer. It may not drive audio, may not drive the annunciator, and may not be the basis of any advisory. This was a local rule in revision 3 and was promoted in revision 5, because it generalises: any source whose coverage cannot be relied on is subject to it.
+
+### What ADS-B In does not show you
+
+This belongs in the requirements rather than in a footnote, because the failure mode is a pilot trusting a screen.
+
+**Part 103 aircraft are not required to carry ADS-B Out, and most carry none.** The traffic most likely to conflict with a powered parachute at 800 feet is other ultralights, gliders, banner tows, and ag aircraft, which is precisely the traffic least likely to appear on the display.
+
+**TIS-B is conditional.** It uplinks radar-derived traffic, including aircraft with no ADS-B Out, but only inside a service volume triggered by a properly equipped ADS-B Out aircraft. An aircraft without Out does not trigger its own. You receive it when someone equipped happens to be nearby, and not otherwise. Radar coverage at 500 to 1500 feet AGL is thin regardless.
+
+**Internet feeds have a coverage floor.** These networks are crowdsourced ground receivers operating line of sight. Below roughly 1000 to 2000 feet AGL in rural areas there is no coverage at all. Latency of 5 to 15 seconds displaces a target by roughly a third of a mile at closure speeds that matter.
+
+The consequence is a display requirement: **a traffic screen must not imply completeness.** A screen showing two airliners overhead and nothing else reads as an empty sky, and the sky is not empty. Traffic display is a supplement to looking outside, and the interface has to carry that rather than assume it.
+
+### Weather is the better use of the internet path
+
+`aviationweather.gov` publishes METAR and TAF as JSON, free, without a key. It is low rate, tolerant of latency, has no coverage floor, and creates no false confidence about collision risk.
+
+It also improves a feature that already exists. The density altitude advisory in section 10 currently compares against a profile limit. A real altimeter setting and temperature turn that from an estimate into a number.
+
+If effort goes to exactly one internet feed, it goes here and not to traffic.
+
+### What Junco implements
+
+**One GDL90 listener. Not a demodulator.**
+
+For a build that already runs FIX-Gateway, none of this is Junco's work at all: a Stratux ADS-B plugin already exists there. This section applies to the phone-only case, which is the primary one.
+
+GDL90 is the common interface. A single UDP listener serves the Wi-Fi receivers and any local application that emits the format, which means Junco never owns demodulation code, never owns a driver, and never inherits the maintenance burden of either. The SDR-on-the-phone path is then a documented configuration a builder wires up, not a component we ship.
+
+Check the dump1090 and dump978 licenses before bundling anything. The copyleft move in section 17 makes this easier rather than harder: a permissive upstream absorbs cleanly, and a GPLv3-only upstream would force the combined work to v3, which "or later" already permits.
+
+---
+
+## 23. Compute platform
+
+Section 7 specifies compute by requirement rather than part number. This section records the trade behind that requirement, because the choice looks obvious in both directions depending on which constraint is examined first.
+
+### Two reference builds
+
+| | ESP32-S3 build | Pi-class build |
+|---|---|---|
+| Mount | Engine cage | Engine cage or cockpit |
+| Boot | ~300 ms | 20 to 40 s |
+| Draw at 5V | 120 to 160 mA | 100 mA idle, several times that under load |
+| Temperature rating | −40 to +85C | 0 to +50C, commercial |
+| Boot medium | Soldered flash | microSD |
+| Power loss | Close the file. No OS to corrupt | Orderly stop, or a read-only root |
+| ADS-B, GDL90 out | Neither, ever | Both, natively |
+
+### What the Pi buys
+
+**One box instead of two.** A Pi-class board and a dongle is Stratux. A Pi-based node therefore does engine, air data, ADS-B In on both bands, FIS-B weather, GDL90 out, and the on-demand configuration AP in a single enclosure. It collapses section 22's optional receiver and section 14's phase 2 GDL90 output into the node itself.
+
+**Development and longevity.** Linux and plain C or Python, no cross-compile, no pinned toolchain. For a project whose stated goal is being buildable in 2040, that ages better than a specific ESP-IDF release. Production is committed through at least January 2030.
+
+**The real-time objection is weak and should not be repeated.** This workload peaks at 25 Hz on the pressure channels, and a tachometer at 10,000 RPM with one pulse per revolution is 167 Hz. Linux handles that comfortably. The ESP32's hardware pulse counters are cleaner, not necessary.
+
+### Temperature is an enclosure problem
+
+The 0 to 50C commercial rating is not a reason to reject the board. The aircraft already routes a pitot line to the node's location, so a ram air source is available at the same place, and the cold end is insulation plus a board that dissipates one to two watts into a small volume.
+
+Requirements that follow, for a Pi-class build:
+
+1. **Cooling is ducted ram air.** The inlet, the duct, and the outlet are enclosure design, not an afterthought.
+2. **The cooling path must not couple into the static plenum or disturb the pitot.** A cooling inlet is a pressure source. The rule in section 6 that keeps the phone barometer out of the vertical speed calculation exists because a pressure source that varies with airspeed makes a vario report throttle position, and a badly placed cooling duct reintroduces exactly that failure a foot from where it was designed out.
+3. **Ground idle is the sizing case, not cruise.** Ram air produces nothing on the ground, which is the same reason section 19 rejects the ram air turbine. The enclosure needs enough thermal mass to survive a hot-day taxi and runup, or the profile needs a documented temperature gate before takeoff.
+4. **Cold start is out of spec at t=0**, before self-heating. An insulated enclosure holds the board well above ambient within minutes but not at power-on. Either accept and publish a warm-up interval, or add a resistive heater.
+5. **A vented enclosure is not a shielded enclosure.** Section 7 requires shielding against CDI ignition. A ducted inlet needs screening or a labyrinth, and it will breathe moisture. Conformal coat the board.
+
+### What temperature does not solve
+
+Two distinctions survive the thermal work and are requirements on any Pi-class build rather than arguments against it:
+
+1. **The boot medium is the computer.** On the ESP32, firmware lives in soldered flash and the SD card carries only the log, so a card failure costs data and the instrument keeps running. On a Pi the card is the system, and a vibration-induced failure on a two-stroke airframe is a dead instrument in flight. Mitigation is mandatory: **read-only root with a RAM overlay, and the log on a separate card from the boot medium.**
+2. **An unclean stop can corrupt the root filesystem, not just the log.** Section 12 sizes a supercapacitor to close a file. A Pi-class build needs it sized for an orderly stop at that board's actual draw, which is several times the ESP32's. A read-only root reduces this from a bricking risk to a lost record, which is why requirement 1 is not optional.
+
+Boot time is the visible consequence: 20 to 40 seconds against 300 milliseconds. Publish it, and make sure a mid-flight brownout reboot cannot be mistaken for a working instrument during the interval when it is not one.
+
+### What does not change
+
+Both builds meet the same channel requirements in section 8, write the same self-describing log format in section 12, expose the same BLE protocol in section 14, and load the same aircraft profile in section 11. A log file does not record which processor produced it beyond the hardware revision required by design rule 7, and no consumer needs to care.
+
+---
+
+## 24. Interoperability with MakerPlane
+
+MakerPlane is the closest existing project to Junco's problem, and it is a better neighbour than a competitor. It is built for experimental aviation rather than adapted from drones, and it is alive.
+
+| Component | What it is | License |
+|---|---|---|
+| CAN-FIX | CANbus protocol designed for experimental aviation | Creative Commons |
+| FIX-Gateway | Plugin-based avionics data broker, Python | GPL-2.0-or-later |
+| pyEFIS | EFIS display, Python, runs on a Raspberry Pi | GPL-2.0-or-later |
+
+### The decision
+
+**Junco stays a separate project and contributes a plugin.** It keeps its own repository, specifications, hardware, and BLE protocol, and publishes into FIX-Gateway through a plugin offered upstream.
+
+Reasons, in order of weight:
+
+1. The engine and air data work needs its own hardware, its own test program, and its own specifications. None of that belongs inside a general E-AB avionics project.
+2. A Part 103 powered parachute with a twin two-stroke is a narrow target and a poor fit for somebody else's roadmap.
+3. It is reversible. A plugin that proves valuable can be pushed further upstream later. A dissolved project cannot be reconstituted.
+
+### The integration
+
+```
+ Junco node ---- BLE ----> Junco Android app in flight, no panel
+ |
+ +--------- BLE ----> FIX-Gateway plugin ----> pyEFIS panel, Pi build
+ |
+ +----> every other FIX-Gateway plugin
+```
+
+The plugin is small. FIX-Gateway is explicitly protocol-agnostic and brokers arbitrary sources into a single parameter namespace, so a Junco source is the exact shape it expects.
+
+### What this removes from Junco's scope
+
+Several things this document specifies as Junco work already exist in FIX-Gateway and should be consumed rather than rebuilt.
+
+| Specified here | Already exists |
+|---|---|
+| Section 22 traffic display | Stratux ADS-B plugin |
+| Section 12 second recording | Data recorder and playback plugin |
+| Section 10 annunciation | Annunciation plugin |
+| Section 10 panel display | pyEFIS |
+| Design rule 8 disagreeing sources | Multi-source voting plugin |
+| Pi-class baro and IMU | Raspberry Pi sensor plugins |
+
+**This does not delete the Junco app.** A Part 103 aircraft with no panel and no Pi is still the primary case, and the phone-as-hub architecture in section 6 stands unchanged. What it means is that the app becomes one client rather than the only one, which restores fallback paths revision 2 removed.
+
+### What stays Junco's
+
+Nothing in that stack reads a two-stroke with CDI ignition and no ECU. The engine plugins that exist are Grand Rapids EIS and MegaSquirt, and both assume an engine with a computer in it. A Part 103 aircraft does not have one.
+
+- Four thermocouples, cold junction compensated
+- Two opto-isolated ignition-pulse tachometer channels
+- Sub-100 Pa differential pressure, where the glider projects are kilopascal-class
+- A static plenum that works in prop blast
+- The fuel backends in section 9
+- The aircraft profile, the log format, and the BLE protocol
+
+That list is narrower than what this document described before, and it is the part nobody else has done.
+
+### CAN-FIX and the v2 bus
+
+`spec/dronecan-engine-extension.md` picks DroneCAN. That choice should be re-made rather than inherited.
+
+DroneCAN's consumers are autopilots, which is stage v4 and not pursued here. CAN-FIX's consumers are experimental aircraft panels, which is what this aircraft has. The spec's own argument, that custom types mean somebody has to write a driver and therefore nobody will, points at whichever bus the target community already runs.
+
+CAN-FIX is also Creative Commons, so implementing it carries no licensing consequence, unlike consuming a GPL implementation of it.
+
+Not decided in v1, which has no bus and no second node. Recorded so v2 decides it deliberately.
+
+### One honest caveat about design rule 1
+
+A Pi-class build per section 23 could run the node firmware, FIX-Gateway, and pyEFIS in one enclosure. The node function still publishes and the display still subscribes, so nothing influences a published value and design rule 1 holds in substance.
+
+But the boundary becomes a software boundary rather than a physical one, and software boundaries are weaker. If that build is pursued, the node process stays separable and independently testable, and imports nothing from the display side.
+
+---
+
+## 25. Revision history
+
+### Revision 5, August 2026: closing the open questions
+
+**What changed.** Fifteen specification questions that had been sitting in "not yet specified" lists were decided. Design rule 9 was added. The remaining open items were consolidated into `docs/open-questions.md` instead of being scattered across five files.
+
+**Why now.** They were blocking firmware, and none of them needed data that flying would produce. A question that can be answered at a desk and is instead left open becomes a decision someone makes accidentally while implementing.
+
+**The three that were irreversible** got decided first and deliberately: the BLE UUID base is generated and frozen, the log is a preallocated file on FAT32 rather than a raw partition, and the profile hash is taken over the stored bytes rather than a re-serialization. Each of those costs field hardware to change later.
+
+**Design rule 9, advisory-only data never raises an alert.** This was a local rule inside section 22 in revision 3. It was promoted because it generalises past traffic: any source whose coverage or latency cannot be relied on is subject to it, and a rule that only exists inside one section gets forgotten by the next section that needs it.
+
+**One decision worth calling out.** Log record payloads are now byte-identical to the BLE characteristic payloads, with the log adding only magic, type, and CRC. The node serializes each sample once rather than twice, which removes an entire category of defect where the link and the card disagree about what a flight contained.
+
+**What is deliberately still open.** The measurements in section 15, which need the aircraft. The bus choice between DroneCAN and CAN-FIX, which v1 does not have a bus for. CSV and GPX export, which is a tools concern that cannot cost hardware. And `ESP32-S31`, which appears three times in this document and refers to no Espressif part that exists.
+
+### Revision 4, August 2026: copyleft, and a neighbour instead of a competitor
+
+**What changed.** Code moved to GPL-2.0-or-later and hardware to CERN-OHL-S-2.0. Specifications stayed CC-BY. The commercial roadmap in section 20 stopped being the project's plan and became a description of what others may do. A new section 24 positions Junco as the engine and air data front end for the MakerPlane stack.
+
+**Why the license.** The project's purpose is to put this capability into the world permanently. A permissive license lets a better funded fork take the work closed and outrun the original, and copyleft prevents that at no cost to any use this project cares about. GPL v2 or later specifically, because it matches MakerPlane and lets code move both ways without relicensing.
+
+**Why the specifications stayed permissive.** They are meant to be implemented by anyone in anything. A protocol nobody may adopt freely does not outlive its implementation, which is the whole reason `spec/` exists. CAN-FIX is Creative Commons for the same reason.
+
+**What it cost.** Very little that this project wanted. GPL still permits building, selling, forking, and competing. What it forecloses is a closed derivative, and section 20 no longer has a stage that depends on offering one.
+
+**What got smaller, usefully.** Section 24's scope table removes six items from Junco's work because FIX-Gateway already has them. What remains is the two-stroke engine front end nobody has built, which is a sharper description of the project than any previous revision managed.
+
+**What is still open.** Whether the v2 bus is DroneCAN or CAN-FIX. Revision 4 records the argument and explicitly does not decide it, because v1 has no bus.
+
+### Revision 3, August 2026: traffic without a receiver, and a second compute path
+
+**What changed.** Traffic became a pluggable app-side channel with the node explicitly outside the path. The non-goal in section 4 was re-scoped from "ADS-B receive" to "a Junco-built ADS-B receiver," which is a narrower and more honest statement of the same position. A Linux single-board variant was admitted as a supported second build rather than a fork.
+
+**Why.** Two findings. First, consuming GDL90 costs the node nothing, because the receiver belongs to the pilot and the listener is a socket in the app, so the feature was never as expensive as bundling it with a receiver made it look. Second, the ESP32-S3 cannot decode ADS-B under any circumstances, its USB being short of the required bandwidth by a factor of four, which settles the node's role rather than constraining it.
+
+**On temperature.** The 0 to 50C rating was initially treated as disqualifying for a Pi-class board on an engine cage, by analogy with the e-paper rejection in section 10. That analogy does not hold. A display panel cannot be ducted and a circuit board can, and the aircraft is already routing pneumatic tubing to the same location. The rating became an enclosure requirement, recorded in section 23.
+
+**What is still open.** Whether a ram-air-cooled enclosure actually holds the range on an engine cage, and whether ground idle heat-soak defeats it, are measurements rather than arguments. Both went into section 15 and get instrumented during phase 0, where the hardware is already running for the EMI checks.
+
+**What it did not change.** The node stays publish-only. The phone stays the hub. No traffic backend, including the ones that run on the phone, may generate an alert from internet-sourced data, and no traffic display may imply that it is showing everything in the sky.
### Revision 2, August 2026: phone as hub
diff --git a/enclosure/README.md b/enclosure/README.md
index 917c0ca..ebc4da4 100644
--- a/enclosure/README.md
+++ b/enclosure/README.md
@@ -5,3 +5,5 @@ Printable parts. Nothing here yet.
Print in ASA, 1.5 mm walls, mounted on grommet or wire rope isolators. No PLA anywhere, and nothing printed within a foot of a cylinder.
The two parts worth real design effort are the pitot and the static plenum, since nobody sells them cheaply and the plenum geometry determines whether the vario works or just reports throttle position.
+
+A Pi-class build adds a third: a ducted ram air cooling path, which is what makes a 0 to 50C board viable on an engine cage. Its inlet is a pressure source, so it must not couple into the static plenum or disturb the pitot, and a vented enclosure still has to shield against CDI ignition. See docs/prd.md section 23.
diff --git a/firmware/README.md b/firmware/README.md
index 2ed81db..882e35b 100644
--- a/firmware/README.md
+++ b/firmware/README.md
@@ -1,5 +1,9 @@
# Node firmware
-ESP-IDF project for the sensor node. Nothing here yet.
+Nothing here yet.
-Phase 0 is an EMI reality check, not firmware: an ESP32-S3 and one baro sensor on a breadboard, ground runs at every RPM with the board where it will live, watching for I2C lockups, resets, and GNSS loss. Write firmware after that answers.
+Phase 0 is an EMI reality check, not firmware: an ESP32-S3 and one baro sensor on a breadboard, ground runs at every RPM with the board where it will live, watching for I2C lockups, resets, and BLE dropouts. Log enclosure temperature at the same time, since the hardware is already running and that measurement decides whether a Pi-class build is viable on the cage. Write firmware after that answers.
+
+Two compute paths are supported and both meet the same specifications. The ESP32-S3 build is an ESP-IDF project. A Pi-class build is Linux and carries extra requirements on its boot medium and shutdown path. See docs/prd.md section 23.
+
+`spec/ble-telemetry.md` is the document this code is written against. It is a draft with its field widths, UUID base, and configuration schema still open, so read its "Not yet specified" section before assuming anything is settled.
diff --git a/hardware/README.md b/hardware/README.md
index f3b5811..ae2775d 100644
--- a/hardware/README.md
+++ b/hardware/README.md
@@ -4,4 +4,4 @@ Schematics and board files. Nothing here yet.
v1 is deliberately breadboard only: an off-the-shelf ESP32-S3 dev board on a commercial screw terminal breakout. A custom carrier board is phase 5, after the harness requirements are known from actually flying it.
-Compute is specified by requirement, not part number. See docs/prd.md section 7.
+Compute is specified by requirement, not part number. See docs/prd.md section 7 for the requirement and section 23 for the trade between the ESP32-S3 and a Pi-class build.
diff --git a/spec/README.md b/spec/README.md
index d697bf3..8fa14d1 100644
--- a/spec/README.md
+++ b/spec/README.md
@@ -8,5 +8,21 @@ the firmware and are the artifacts most worth getting right.
|---|---|
| `log-format.md` | Draft. Required for v1 |
| `aircraft-profile.md` | Draft. Required for v1 |
-| `dronecan-engine-extension.md` | Draft. Not implemented in v1 |
-| `ble-telemetry.md` | Not started. Required for v1 |
+| `dronecan-engine-extension.md` | Draft. Not implemented in v1, and its choice of bus is reopened. See PRD section 24 |
+| `ble-telemetry.md` | Draft. Required for v1. The gating document |
+
+`ble-telemetry.md` gates firmware and app work. It carries every value the pilot
+sees, both reference clients are written against it, and PRD success criterion 4
+requires a stranger to write a second client from it without reading the Android
+source.
+
+The source tag enumeration required by design rule 8 is **defined in
+`ble-telemetry.md`** and referenced from `log-format.md`. Add a new source in one
+place only.
+
+The channel encoding table is likewise defined once, in `ble-telemetry.md`, and
+used by `log-format.md`, because log record payloads are byte-identical to the
+BLE characteristic payloads.
+
+What remains undecided across all of these is indexed in
+`../docs/open-questions.md`.
diff --git a/spec/aircraft-profile.md b/spec/aircraft-profile.md
index 97825e6..1c30b0e 100644
--- a/spec/aircraft-profile.md
+++ b/spec/aircraft-profile.md
@@ -38,9 +38,86 @@ km/h, Fahrenheit or Celsius, gallons or liters, inHg or hPa.
**Build class.** Self-built, kit-built, or factory-qualified. Written into
every log header.
+## Serialization
+
+**TOML as the source of truth, plus a compiled binary form. The node stores
+both.**
+
+This resolves a real tension. "Config, not code" requires a file a human can
+open in a text editor and understand. Flight firmware does not want a TOML
+parser on its critical path, and an ESP32 parsing text at boot is a failure mode
+nobody needs.
+
+So:
+
+- **TOML is authoritative.** Hand-editable, comments survive round trips, no
+ significant whitespace to get wrong, unambiguous types. It is what the owner
+ edits and what the web configuration tool emits.
+- **The binary form is derived.** Compiled from the TOML by the configuration
+ tool or by the node's Wi-Fi AP mode, never in flight. Fixed layout, no
+ parsing, directly usable.
+- **Both live on the node.** The TOML so the aircraft is self-documenting and a
+ replacement tool can read it back; the binary so nothing parses text at boot.
+- **BLE serves the binary form**, per `ble-telemetry.md`. A client needs the
+ values, not the comments.
+
+If the two ever disagree, the TOML wins and the binary is rebuilt. A node that
+finds a binary whose hash does not match its TOML refuses to arm and says so.
+
+## Versioning
+
+An integer `schema_version` at the top of the file.
+
+**The node refuses a version it does not recognise. It never guesses.** A node
+that half-understands a profile is a node that may be reading a cylinder head
+temperature limit from the wrong field, and there is no safe default for that.
+
+**Migration lives in the configuration tool, not in firmware.** A tool running
+on a laptop can be careful, can show a diff, and can be corrected. Migration
+logic embedded in flight firmware is how a limit gets silently reinterpreted
+three versions later.
+
+## Validation
+
+Validated in both places: the configuration tool rejects on save, and the node
+revalidates on load because it cannot assume the file arrived from the tool.
+
+**A channel that fails validation is not armed**, and the node reports it rather
+than substituting a default. Minimum rejections:
+
+- Engine count, cylinders per engine, and the channel map disagreeing
+- Usable fuel greater than capacity
+- Pulses per revolution of zero
+- Two channels claiming the same pin or address
+- A limit outside the range its sensor can represent
+- A fuel backend named in the channel map with no corresponding calibration
+
+## Profile hash
+
+**SHA-256 over the exact bytes stored on the node, truncated to 8 bytes.**
+
+"Exact bytes stored" and not a re-serialization. Hashing a re-serialized
+structure means any change to the serializer silently changes the hash of an
+unmodified profile, which invalidates every cached copy and every log header
+that referenced it. Hash the bytes on the card.
+
+The same 8 bytes appear in the log header and over BLE, so a log file and a live
+connection name the same profile identically.
+
+## What the profile does not contain
+
+The profile describes the aircraft, and it lives on the node so a borrowed phone
+or a replacement tablet inherits the right configuration by connecting.
+
+Anything describing the pilot's own equipment rather than the aircraft is
+app-side configuration and stays on the phone. Traffic backend selection is the
+current example: which receiver or feed a pilot uses is a property of what is in
+their flight bag, it changes without the aircraft changing, and the node is not
+in that data path at all. See PRD section 22.
+
## Not yet specified
-- Serialization format. Leading candidate is TOML for hand-editability
-- Schema versioning and migration when a field is added
-- Validation rules, particularly which combinations are rejected outright
-- How the profile hash is computed for the log header
+- The TOML key names and the file's section structure
+- The binary form's field layout, which is shared with `ble-telemetry.md`
+- What the node does when no valid profile exists at all, on a freshly built
+ unit that has never been configured
diff --git a/spec/ble-telemetry.md b/spec/ble-telemetry.md
new file mode 100644
index 0000000..d5e5000
--- /dev/null
+++ b/spec/ble-telemetry.md
@@ -0,0 +1,306 @@
+# Junco BLE telemetry
+
+**Status:** draft. Required for v1. This document blocks firmware and app work.
+
+## Requirement
+
+Every value the pilot sees crosses this link. A stranger must be able to write a
+working client from this document alone, without reading the Android source.
+That is PRD success criterion 4, and it is the reason this document exists
+separately from any implementation.
+
+Two reference clients are expected, which is a useful forcing function:
+
+- the Junco Android app
+- a FIX-Gateway plugin in Python, per PRD section 24
+
+If a rule below is only satisfiable by one of them, it is the wrong rule.
+
+## Design rules this document inherits
+
+From PRD section 2 and section 14:
+
+1. **Publish-only for flight data.** The only writable characteristics are
+ configuration and the clock, and neither influences a published value.
+2. **One notify characteristic per rate class**, not one per channel. Grouping
+ by rate keeps notification count low and packing efficient.
+3. **Every sample carries a source tag and a validity flag.** Invalid is
+ published as invalid, never as a held value.
+4. **The profile is readable over the link**, so a client configures itself from
+ the node.
+
+## Service and characteristic layout
+
+**These UUIDs are frozen.** Generated 2026-08-09 as a single random v4 UUID,
+with bytes 4 and 5 used as a 16-bit allocation slot. They are permanent. Once
+hardware exists in the field, changing them silently breaks every client that
+was written against them, so a future version of this document may allocate new
+slots but must never redefine an existing one.
+
+Base: `1761601a-XXXX-4e69-b9a1-b45cf63c7638`
+
+| Slot | Characteristic | UUID | Properties | Rate |
+|---|---|---|---|---|
+| `0001` | Junco service | `1761601a-0001-4e69-b9a1-b45cf63c7638` | — | — |
+| `0010` | Air data | `1761601a-0010-4e69-b9a1-b45cf63c7638` | notify | 25 Hz |
+| `0011` | Engine speed | `1761601a-0011-4e69-b9a1-b45cf63c7638` | notify | 5 Hz |
+| `0012` | Temperatures | `1761601a-0012-4e69-b9a1-b45cf63c7638` | notify | 2 Hz |
+| `0013` | Slow channels | `1761601a-0013-4e69-b9a1-b45cf63c7638` | notify | 1 Hz |
+| `0020` | Node status | `1761601a-0020-4e69-b9a1-b45cf63c7638` | notify, read | on change |
+| `0030` | Aircraft profile | `1761601a-0030-4e69-b9a1-b45cf63c7638` | read | on connect |
+| `0040` | Clock | `1761601a-0040-4e69-b9a1-b45cf63c7638` | write | on connect |
+| `0041` | Configuration | `1761601a-0041-4e69-b9a1-b45cf63c7638` | read, write | rare |
+
+Rate classes come from the channel table in PRD section 8. A channel's rate
+class is a property of the channel, not of the installation, so a client can
+rely on the grouping without reading the profile first.
+
+## Sample framing
+
+Every notification begins with a common header, so a client that does not
+recognise a characteristic can still discard it safely.
+
+| Field | Type | Notes |
+|---|---|---|
+| `format` | uint8 | Layout version for this characteristic. Increment on any change |
+| `t_ms` | uint32 | Node monotonic milliseconds. Not wall clock. See Time |
+
+Each channel in the payload is then a triple:
+
+| Field | Type | Notes |
+|---|---|---|
+| `value` | per channel | See the channel table |
+| `source` | uint8 | Source tag enumeration below |
+| `status` | uint8 | Bit 0 valid, bit 1 stale, bits 2-7 reserved and zero |
+
+**When bit 0 is clear the value field is undefined.** A client must ignore it
+and must not render it.
+
+**The status bit is the only validity signal.** Every value on this link is an
+integer, so there is no NaN to carry a second, redundant answer. The NaN
+convention in `dronecan-engine-extension.md` applies to DroneCAN's own float
+types and does not apply here. One signal, one place to check.
+
+**A node never holds a stale value on the link.** Bit 1 exists for the case
+where a channel is genuinely slower than its rate class, not as permission to
+republish an old reading. If a channel has failed, it is published invalid or
+not published at all. Holding is a display behavior and belongs in the client;
+see PRD section 10, which deliberately specifies different behavior for the link
+and for the display.
+
+## Channel encoding
+
+**Every value is an integer in SI units.** No floats and no display units.
+
+Integers because both reference clients decode them identically, because they
+pack smaller, and because a fixed scale is a decision recorded in this document
+rather than a floating point representation question deferred to a compiler.
+
+SI because unit selection is a display concern. PRD section 11 lets the owner
+pick feet or metres, gallons or litres, Fahrenheit or Celsius, and that choice
+lives in the profile and applies at render time. Gallons must never appear on
+the wire, or two clients will disagree about what a number means.
+
+| Channel | Type | Scale | Range covered |
+|---|---|---|---|
+| Static pressure | `uint32` | Pa × 100 | 0 to 1100 hPa with margin |
+| Differential pressure | `int32` | Pa × 100 | ±500 Pa, far finer than the sensor's 0.1 Pa zero accuracy |
+| Engine speed | `uint16` | 1 RPM | 0 to 10000 |
+| Cylinder head temp | `int16` | 0.1 °C | −273 to 3276 |
+| Exhaust gas temp | `int16` | 0.1 °C | Covers a two-stroke EGT to 1200 °C |
+| Outside air temp | `int16` | 0.1 °C | |
+| Fuel quantity | `uint32` | millilitres | |
+
+Scales are deliberately finer than the sensors warrant. Resolution costs nothing
+here and re-scaling a shipped protocol costs everything.
+
+## Source tag enumeration
+
+**This enumeration is shared with `log-format.md` and is defined here.** That
+document references these values rather than restating them, so there is exactly
+one place to add a source.
+
+Design rule 8 exists because a pressure altitude from a plumbed plenum and one
+from a phone barometer are not interchangeable. The tag is what makes that
+survivable across the link, the log, and the display.
+
+| Value | Source |
+|---|---|
+| `0x00` | Unknown. Never valid in a published sample |
+| `0x10` | Node, plumbed static plenum |
+| `0x11` | Node, pitot differential |
+| `0x12` | Node, type K thermocouple |
+| `0x13` | Node, isolated pulse counter |
+| `0x14` | Node, ambient temperature sensor |
+| `0x20` | Node fuel, burn integration |
+| `0x21` | Node fuel, magnetic float |
+| `0x22` | Node fuel, load cell |
+| `0x23` | Node fuel, ultrasonic |
+| `0x24` | Node fuel, capacitive |
+| `0x30` | Phone, GNSS |
+| `0x31` | Phone, barometer |
+| `0x32` | Phone, OS sensor fusion |
+| `0x40` | Derived, from node channels only |
+| `0x41` | Derived, mixing node and phone channels |
+
+`0x30` through `0x32` never appear on this link, because the node does not
+produce them. They are allocated here because the log format carries both sides
+and the enumeration must be single-valued across both.
+
+`0x41` is load-bearing. A derived value that mixes a node channel with a phone
+channel inherits the weaker assurance of the two, and a client that cannot tell
+`0x40` from `0x41` will present them identically. Vertical speed in particular
+must never be `0x41`; PRD section 8 requires it to come from plenum static only.
+
+## Payload composition
+
+**A rate class payload carries its channels in the order the profile declares
+them**, and the count comes from the profile. A twin publishes two engine speeds
+in the engine characteristic; a single publishes one. Nothing in the framing
+announces the count, because the profile already did.
+
+The consequence is a hard ordering requirement: **a client must read the profile
+before it can parse any rate class notification.** A client that subscribes
+first and reads later will mis-slice every payload it receives in between. Read
+the profile, then subscribe.
+
+This is the right trade for a link where the aircraft is knowable and bytes are
+scarce. It is stated explicitly because it is the single easiest way to write a
+broken second client, and success criterion 4 says a stranger has to get this
+right from the document alone.
+
+## Node status
+
+The status characteristic carries what the pilot needs after the flight rather
+than during it.
+
+**Channel transition counts live on the node, not in the client.** PRD section
+10 requires a post-flight summary naming each channel that failed and how many
+times it transitioned, and a client that connected late or dropped out would
+count wrong. The node is the only party that observed the whole flight, so the
+node counts and the client displays.
+
+The same reasoning puts them in the log, which is authoritative and survives the
+phone being lost.
+
+## Connection parameters
+
+- **Connection interval: 15 ms or better.** 25 Hz air data must arrive without
+ aggregation delay. A client requests this; a node does not assume it was
+ granted and must not silently drop samples if it was not.
+- **ATT MTU:** negotiate upward on connect. Every characteristic payload defined
+ here fits in the 23-byte default MTU so a client that fails negotiation still
+ works, at the cost of more packets.
+- **Notifications, not indications.** Flight data is a stream. A lost sample is
+ replaced 40 ms later by a better one, and the acknowledgement round trip costs
+ more than the sample is worth.
+
+## Time
+
+The node has no real-time clock and must never be assumed to have one.
+
+1. The node timestamps everything with `t_ms`, a monotonic counter from boot.
+2. The client writes its wall clock to the Clock characteristic on connect.
+3. The node records the offset into the log as a record, per `log-format.md`.
+
+The node's published timestamps do not change after a clock write. Correcting
+them would make the stream discontinuous mid-flight and would break any client
+that had already recorded samples. Alignment is a post-processing operation
+against the recorded offset, not a live correction.
+
+## Profile transfer
+
+The aircraft profile lives on the node, per PRD section 11, so a borrowed phone
+or a replacement tablet inherits the right configuration by connecting.
+
+The client reads the profile on connect and caches it against the profile hash.
+
+**Transfer:** the node serves the profile from a single read characteristic in
+sequential chunks, each framed as `[uint16 offset][uint16 total][bytes]`. The
+client reads from offset zero, learns `total` from the first chunk, and
+continues until it has that many bytes. No separate length characteristic and no
+state machine on the node beyond the offset the client asks for.
+
+**Encoding:** the compiled binary form of the profile, per
+`aircraft-profile.md`. The TOML source is also stored on the node and is
+retrievable over the Wi-Fi AP for editing, but it is not sent over BLE. A client
+needs the values, not the comments.
+
+**Hash:** SHA-256 over the exact bytes the node stores, truncated to 8 bytes.
+Truncated to the same 8 bytes in the log header, so a log and a live connection
+name the same profile identically.
+
+The profile describes the aircraft only. Client-side configuration, such as the
+traffic backend selection in PRD section 22, is not carried here and the node
+has no knowledge of it.
+
+## Writable surface
+
+Exactly two characteristics accept writes: Clock and Configuration.
+
+**Neither may influence a published flight value.** A configuration write that
+changed a calibration constant mid-flight would violate design rule 1 by making
+the node's outputs a function of something it subscribed to.
+
+**Configuration writes are rejected whenever the log file is open.** That is the
+whole definition of "in flight" for this purpose. It is a single condition the
+node already tracks, it needs no RPM threshold to tune, and it fails safe: if
+the node is recording, it is not reconfigurable.
+
+**A configuration write replaces the entire profile atomically.** There is no
+field-level write. The node validates the whole profile, recomputes the hash,
+and either accepts or rejects it as a unit. Partial writes are how a node ends
+up in a state that matches no file anywhere, which is unrecoverable by anyone
+trying to reproduce a flight from the log.
+
+**Writes require a bond. Subscribing does not.** Flight data is publish-only
+advisory data, and requiring a pairing to read it would mean a lost bond costs
+the pilot their instruments in flight. Writes change what the node is, so they
+require an established relationship. This puts the security boundary exactly on
+the design rule 1 line: the unauthenticated surface is the one that cannot
+influence anything.
+
+A client is not required to write anything. A read-only client that never writes
+the clock is valid and gets usable telemetry with node-relative timestamps. The
+FIX-Gateway plugin is expected to be exactly that.
+
+## Concurrent clients
+
+**The node supports two simultaneous connections** and publishes identically to
+both. The phone-plus-FIX-Gateway case in PRD section 24 makes this ordinary
+rather than exotic.
+
+Two, not unlimited. Each connection consumes radio time at the 15 ms interval,
+and a documented limit that clients can rely on is worth more than an
+undocumented one they discover in flight. A third connection attempt is refused,
+not silently accepted and starved.
+
+**The first clock write of a session wins.** Subsequent writes from any client
+are acknowledged and ignored. Two clients with slightly different wall clocks
+must not be able to move the time base underneath a log that is already being
+written.
+
+## Versioning
+
+The `format` byte is per characteristic, not global. A node may increment the
+air data layout without touching the temperature layout.
+
+A client that sees a `format` it does not recognise must discard that
+characteristic's notifications and continue operating on the ones it does
+recognise. It must say so plainly rather than showing an empty field, per the
+stale-data rules in PRD section 10.
+
+Adding a channel to the end of an existing payload is a `format` increment. It
+is not backward compatible and must not be treated as though it were, because a
+client sizing its parse from the old layout will mis-slice the new one.
+
+## Not yet specified
+
+- The full node status payload beyond the transition counts. Uptime, supply
+ voltage, SD state, and free space are candidates
+- What the advertisement carries: device name convention, whether the service
+ UUID is advertised, and whether build class is visible before connecting
+- Behavior when a rate class payload exceeds the negotiated MTU, which a
+ many-cylinder aircraft could reach even though the PM-2 does not
+- Whether the configuration characteristic's read returns the current profile
+ hash, the validation result of the last write, or both
diff --git a/spec/dronecan-engine-extension.md b/spec/dronecan-engine-extension.md
index b580272..76d8298 100644
--- a/spec/dronecan-engine-extension.md
+++ b/spec/dronecan-engine-extension.md
@@ -2,6 +2,22 @@
**Status:** draft, scope definition only. Not implemented in v1.
+## The bus protocol is not settled
+
+This document assumes DroneCAN. That assumption was inherited rather than
+decided, and PRD section 24 records the argument for re-making it.
+
+CAN-FIX is a CANbus protocol designed specifically for experimental aviation,
+its specification is Creative Commons, and its consumers are aircraft panels.
+DroneCAN's consumers are autopilots, which is a stage this project does not
+pursue.
+
+Everything below about standard types versus custom types applies to either bus
+and is worth keeping whichever one wins. Read this as "what the gap is" rather
+than "which bus fills it."
+
+Decide in v2, which is the first version that has a bus at all.
+
## Why this exists
DroneCAN is the primary CAN protocol used by ArduPilot and PX4. Publishing
@@ -42,16 +58,30 @@ This is the entire scope of this document.
4. **Fuel endurance modeling.** Time remaining and range remaining, with the
confidence of the estimate and which backend produced the underlying level.
-## Open questions
+## Resolved
+
+**Seizure precursors publish the raw rate, not a computed warning.** Design rule
+1 argues for publishing data rather than judgments, and the threshold that
+matters differs by engine, by jetting, and by ambient temperature. A node that
+publishes "warning" has embedded a policy that the consumer cannot see, cannot
+tune, and cannot disagree with. A node that publishes EGT rate of change lets
+the panel, the app, and the logbook each decide what it means.
+
+The alert in PRD section 10 is unaffected. That is the client applying policy to
+this data, which is exactly the split being described.
+
+## Blocked on the bus choice
+
+Neither of these can be answered before DroneCAN or CAN-FIX is chosen, because
+each protocol has its own conventions and answering in the wrong one is wasted
+work.
- How are two engines represented? Separate node IDs, or an engine index field
- within the message? Check what ArduPilot's consumer actually does before
- deciding, because the wrong answer here is invisible until someone tries it.
+ within the message? If DroneCAN, check what ArduPilot's consumer actually does
+ first, because the wrong answer is invisible until someone tries it. If
+ CAN-FIX, its own multi-instance convention replaces the question entirely.
- Does fuel endurance belong here or in a separate fuel namespace, given that
the level source is pluggable?
-- Should seizure precursor detection be published as a computed warning, or
- should the raw rate be published and the policy left to the consumer? Design
- rule 1 argues for publishing data rather than judgments.
## Rules for anything added here
diff --git a/spec/log-format.md b/spec/log-format.md
index c35c41e..41c1e20 100644
--- a/spec/log-format.md
+++ b/spec/log-format.md
@@ -44,9 +44,74 @@ Every file identifies the unit that wrote it, per PRD design rule 7.
A v1 self-build must be distinguishable from a later qualified unit using the
log file alone, with no external record.
+## Required per-record content
+
+Every sample carries the source that produced it, per PRD design rule 8. A
+pressure altitude derived from a plumbed static plenum and one derived from a
+phone barometer are different measurements, and a reader that cannot tell them
+apart will silently merge them.
+
+**The source tag enumeration is defined in `ble-telemetry.md`.** It is not
+restated here, because two copies of an enumeration diverge. That document
+allocates values for phone-supplied sources as well, which never cross the BLE
+link but do appear in this file.
+
+Invalid data is flagged invalid or omitted. It is never written at its last
+known value. Holding is a display behavior and has no place in a log.
+
+## Time
+
+There are two recordings and they are required to merge without manual
+alignment: the node log on the card, and the phone log that also carries
+position and attitude.
+
+The node has no real-time clock and must never be assumed to have one. It
+timestamps with a monotonic counter. The phone sends its wall clock on connect,
+and the node writes the offset into the file as a record, so a card recovered on
+its own can still be placed in real time.
+
+## Storage
+
+**A preallocated file on FAT32, not a raw partition.**
+
+Design rule 4 says the owner owns the data. A file on FAT means the owner pulls
+the card, puts it in any laptop, and the flight is there. A raw partition means
+the owner needs our tool to see anything at all, which is a worse position for
+them and a worse position for the project when the tool stops being maintained.
+
+The usual argument for a raw partition is surviving an interrupted write, and
+that argument does not apply here because it is already solved twice over:
+preallocation means the length is correct before the flight starts, and the
+magic header scan recovers records from a card whose filesystem is destroyed. A
+raw partition would add nothing and cost the owner a mount.
+
+## Record types
+
+**A `uint8` type identifier.**
+
+| Range | Use |
+|---|---|
+| `0x00` to `0x0F` | Structural: file header, format descriptors, clock offset |
+| `0x10` and above | Data, mirroring the rate classes in `ble-telemetry.md` |
+
+**Data record payloads are byte-identical to the corresponding BLE
+characteristic payloads.** The log wraps them in magic, type, and CRC and writes
+nothing else.
+
+This is worth more than it looks. The node serializes each sample once, not
+twice, so there is one layout to get right, one place a scaling error can hide,
+and no possibility of the log and the link disagreeing about what a flight
+contained. The channel encoding table in `ble-telemetry.md` is therefore also
+this document's channel encoding table, and it is not restated here.
+
+**Format descriptors follow ArduPilot's FMT model**, as PRD section 19 already
+resolved to do: a record giving the type identifier, a name, and a format string
+describing the field layout. It is proven, it is compact, and anyone who has
+opened a dataflash log already knows how to read it.
+
## Not yet specified
-- Record type identifiers and their allocation
-- Descriptor record encoding
-- Whether records are a raw partition or a file on FAT
-- Export mapping to CSV and GPX
+- Export mapping to CSV and GPX. Deferred deliberately: it is a `tools/` concern
+ and cannot be wrong in a way that costs field hardware
+- The CRC polynomial and width
+- Magic header value and record alignment, which the recovery scanner depends on