From f2dc3e71897a7cfe7c89e359c3da7b3fbb30ab2f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 19:36:25 +0000 Subject: [PATCH 1/7] PRD revision 3: traffic as a pluggable channel, second compute path Traffic becomes a channel with pluggable backends, following the pattern section 9 already uses for fuel. The node is explicitly outside the path for every backend, so design rule 1 holds without needing to be defended. The section 4 non-goal is re-scoped from "ADS-B receive" to "a Junco-built ADS-B receiver." Consuming GDL90 costs the node nothing, because the receiver belongs to the pilot and the listener is a socket in the app. That is a different feature from emitting it, which stays in phase 2. A Linux single-board variant is admitted as a supported second build. The 0 to 50C commercial rating is recorded as an enclosure and airflow requirement rather than a disqualifier: the aircraft already routes a pitot line to the node's location, so ram air is available there. The distinctions that survive the thermal work, the boot medium and the unclean-stop risk, become mandatory requirements on that build instead. New sections 22 and 23. Revision history renumbered to 24. Also recorded: - Why the ESP32-S3 cannot decode ADS-B, as bandwidth arithmetic - What ADS-B In does not show a Part 103 aircraft, as a display requirement - Internet-sourced traffic may never alert - Four new open items in section 15, instrumented during phase 0 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W8P8RZQELMR3NQ7Zacx79v --- docs/prd.md | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 6 deletions(-) diff --git a/docs/prd.md b/docs/prd.md index f50eaa9..2920a22 100644 --- a/docs/prd.md +++ b/docs/prd.md @@ -2,11 +2,13 @@ **Open engine and air data node for Part 103 and experimental aircraft** -Status: Draft, revision 2 +Status: Draft, revision 3 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. Section 24 records what changed and why. --- @@ -47,7 +49,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 +138,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. @@ -325,7 +329,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 +345,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 +382,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,6 +404,10 @@ 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 @@ -442,6 +465,11 @@ 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 | --- @@ -499,7 +527,124 @@ 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**, selected by the owner. The app sees targets. It does not see a method. + +**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.** It is a map layer. It may not drive audio, may not drive the annunciator, and may not be the basis of any advisory. + +### 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.** + +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 against the MIT plan in section 17 before bundling anything. + +--- + +## 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. Revision history + +### 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 From cada780476226a34b147467727a8fa605a466051 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 19:42:38 +0000 Subject: [PATCH 2/7] Resync docs to revision 3 and close spec gaps The README described the revision 1 product: GDL90 out as a headline feature, a $250 parts target, and seven design rules. Revision 2 demoted GDL90 to phase 2, cut the target to $180, and added the source-tagging rule. Fixed all three. app/README.md claimed iOS users get GDL90 into ForeFlight, which revision 2 gave up, and led with Wi-Fi socket binding as the reason the client is native. BLE reachability is the first reason now; the binding still matters for the on-demand AP but that AP is never used in flight. firmware/README.md had phase 0 watching for GNSS loss on a node that no longer has GNSS. Now watches BLE dropouts and logs enclosure temperature, since that measurement decides the Pi-class question and the hardware is already running for the EMI work. Spec gaps found while checking consistency: - log-format.md required design rule 7 header content but never required design rule 8 source tags on records, and said nothing about the clock offset that makes the node and phone logs merge - aircraft-profile.md did not say what stays out of the profile, which revision 3 made load-bearing: traffic backend selection is app-side because a receiver lives in the flight bag, not on the airframe - spec/README.md listed ble-telemetry.md as merely not started rather than as the document blocking implementation Also reconciled the PRD firmware license to MIT, matching LICENSE, the README, and MAINTAINERS.md. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W8P8RZQELMR3NQ7Zacx79v --- CONTRIBUTING.md | 2 ++ README.md | 5 +++-- app/README.md | 8 ++++++-- docs/prd.md | 10 +++++++--- enclosure/README.md | 2 ++ firmware/README.md | 8 ++++++-- hardware/README.md | 2 +- spec/README.md | 11 ++++++++++- spec/aircraft-profile.md | 11 +++++++++++ spec/log-format.md | 23 +++++++++++++++++++++++ 10 files changed, 71 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9932515..28c62e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,8 @@ be declined regardless of its merits elsewhere. - Logbook entries are proposed, never filed - Adapting to a new aircraft never requires a toolchain - A unit always declares its build class +- Every channel declares its source, and invalid data is published as invalid + rather than held at its last value ## Specifications before implementations diff --git a/README.md b/README.md index fd251d2..64e814c 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ **Open engine and air data node for Part 103 and experimental aircraft.** -Junco instruments an ultralight or experimental aircraft and streams flight and engine data to the pilot's phone or tablet. It logs everything to an SD card, emits GDL90 so existing electronic flight bag apps display it without any custom software, and generates draft logbook entries after each flight. +Junco instruments an ultralight or experimental aircraft and streams engine, fuel, and air data over Bluetooth Low Energy to an app on the pilot's phone or tablet. The phone supplies position, attitude, and time. The node supplies what a phone cannot measure from inside its own case. It logs to an SD card and generates draft logbook entries after each flight. -Target cost is under $250 in parts. Target build is one person with a soldering iron, no hot air station and no PCB order. +Target cost is under $180 in parts. Target build is one person with a soldering iron, no hot air station and no PCB order. --- @@ -27,6 +27,7 @@ These constrain the design. They are not disclaimers. 5. **Config, not code.** Adapting Junco to a different aircraft must never require a toolchain. 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, hardware revision, and calibration date. +8. **Every channel declares its source.** A value from the phone's barometer and a value from a plumbed static plenum are not interchangeable. The display, the log, and the protocol must all say which one produced a given reading. --- diff --git a/app/README.md b/app/README.md index 98eeefd..ef41fb1 100644 --- a/app/README.md +++ b/app/README.md @@ -4,6 +4,10 @@ Nothing here yet. Android native, open source, distributed as an APK on GitHub releases and via F-Droid. No store dependency, no developer account, no expiry. -The network binding is the reason this is native rather than a web app: ConnectivityManager.requestNetwork() with a WifiNetworkSpecifier attaches to the node's access point, and bindSocket() binds only this app's socket to it, so the rest of the tablet keeps using cellular. +The app is half the instrument system, not a display for the other half. It supplies position, attitude, and time from the phone's own sensors while the node supplies engine, fuel, and air data over BLE. Neither is a complete picture alone. -iOS is not on the critical path. iOS users get GDL90 into ForeFlight. +Native rather than a web app, for two reasons in order of weight: a PWA cannot reach BLE on iOS at all, and it cannot bind a socket to a specific network. The second still matters for the on-demand Wi-Fi AP used for log pull, config, and firmware, where ConnectivityManager.requestNetwork() with a WifiNetworkSpecifier plus bindSocket() keeps the rest of the tablet on cellular. That AP is never used in flight. + +iOS is not on the critical path for v1, but the protocol is deliberately reachable from it: BLE is available to third-party iOS apps, which is why the link is BLE rather than Bluetooth Classic SPP. There is no GDL90 output in v1, so an iOS user gets nothing from Junco until someone writes that client. + +Traffic display, if anyone builds it, is an app-side channel that never involves the node. See docs/prd.md section 22. diff --git a/docs/prd.md b/docs/prd.md index 2920a22..913d2e9 100644 --- a/docs/prd.md +++ b/docs/prd.md @@ -283,6 +283,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 @@ -414,8 +416,8 @@ Phase 0 gains thermal instrumentation as well. If the Pi-class build is going to | Artifact | License | |---|---| -| Firmware | MIT or Apache 2.0 | -| Android app | MIT or Apache 2.0 | +| Firmware | MIT | +| Android app | MIT | | Board files, STLs | CERN-OHL-P-2.0 | | Documentation and specs | CC-BY-4.0 | @@ -529,7 +531,9 @@ A builder-assumes-risk notice works reasonably against the builder. It does not ## 22. Traffic and ADS-B In -Traffic is defined the way fuel is defined in section 9: **a channel with pluggable backends**, selected by the owner. The app sees targets. It does not see a method. +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. 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..c56c42f 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. + +Nothing substantial can be written here before `spec/ble-telemetry.md` exists, because that link carries every value the pilot sees. 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..03327b5 100644 --- a/spec/README.md +++ b/spec/README.md @@ -9,4 +9,13 @@ 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 | +| `ble-telemetry.md` | **Not started. Required for v1, and the one that blocks code** | + +`ble-telemetry.md` is the gating document. It carries every value the pilot +sees, both the firmware and the app are written against it, and PRD success +criterion 4 requires a stranger to write a second client from it without reading +the Android source. Little of substance should be implemented before it exists. + +The source tag enumeration required by design rule 8 is shared between the BLE +protocol and the log format. Define it once, in one document, and reference it +from the other. diff --git a/spec/aircraft-profile.md b/spec/aircraft-profile.md index 97825e6..21516ca 100644 --- a/spec/aircraft-profile.md +++ b/spec/aircraft-profile.md @@ -38,6 +38,17 @@ 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. +## 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 diff --git a/spec/log-format.md b/spec/log-format.md index c35c41e..a76bbd7 100644 --- a/spec/log-format.md +++ b/spec/log-format.md @@ -44,9 +44,32 @@ 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. + +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. + ## Not yet specified - Record type identifiers and their allocation - Descriptor record encoding +- The source tag enumeration. This is shared with the BLE protocol and should be + defined once and referenced, not written twice - Whether records are a raw partition or a file on FAT - Export mapping to CSV and GPX From 62335c03a7c40ab2789221ea8e1447c03b68be88 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 20:32:57 +0000 Subject: [PATCH 3/7] PRD revision 4: copyleft, and MakerPlane as a neighbour Relicense code from MIT to GPL-2.0-or-later and hardware from CERN-OHL-P-2.0 to CERN-OHL-S-2.0. Specifications stay CC-BY. The purpose of the project is to put this capability into the world permanently, and a permissive license lets a better funded fork take the work closed and outrun the original. Copyleft prevents that at no cost to any use the project cares about: building, selling, forking and competing all remain permitted. GPL v2 or later specifically, because it matches MakerPlane. Their source headers read "version 2 of the License, or (at your option) any later version," so code moves in both directions between this project and FIX-Gateway or pyEFIS without relicensing, and GPLv3 stays reachable if ArduPilot code is ever wanted for the v2 bus. Specifications stay permissive on purpose. They are meant to be implemented by anyone in anything, and a protocol nobody may adopt freely does not outlive its implementation. CAN-FIX is Creative Commons for the same reason. New section 24 positions Junco as the engine and air data front end for the MakerPlane stack rather than a parallel instrument system. Junco stays a separate project and contributes a plugin: FIX-Gateway is explicitly protocol-agnostic, so a Junco source is the shape it already expects, and that reaches pyEFIS as a panel display. Six items leave Junco's scope because FIX-Gateway already has them, among them the traffic work from revision 3, the annunciator, and the second recording. What remains is the part nobody has built: a two-stroke with CDI ignition and no ECU. The existing open engine plugins are Grand Rapids EIS and MegaSquirt, both of which assume an engine with a computer in it. Section 20 stops being a business plan. The project ends at v2; a boxed product and an autopilot are described so the architecture does not foreclose them, and are explicitly left to anyone who wants them. Section 19 gains rows for AP_Periph, which defeats half the original ArduPilot rejection by being genuinely publish-only but fails v1 on BLE, STM32-only, and thermocouples; and the DroneCAN versus CAN-FIX question, recorded and deliberately not decided since v1 has no bus. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W8P8RZQELMR3NQ7Zacx79v --- CONTRIBUTING.md | 19 ++ LICENSE | 359 ++++++++++++++++++++++++++++-- LICENSE-HARDWARE | 198 +++++++++++----- MAINTAINERS.md | 25 ++- README.md | 32 ++- app/README.md | 2 + docs/prd.md | 144 ++++++++++-- spec/README.md | 2 +- spec/dronecan-engine-extension.md | 16 ++ 9 files changed, 693 insertions(+), 104 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28c62e4..6900d9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,6 +32,25 @@ be declined regardless of its merits elsewhere. - Every channel declares its source, and invalid data is published as invalid rather than held at its last value +## Licensing your contribution + +Code is GPL-2.0-or-later, hardware is CERN-OHL-S-2.0, documentation and +specifications are CC-BY-4.0. By opening a pull request you agree your +contribution ships under the license covering that artifact. + +Every source file starts with: + + SPDX-License-Identifier: GPL-2.0-or-later + +The "or later" is not optional and not decorative. It is what lets code move +between Junco and MakerPlane's FIX-Gateway and pyEFIS, which are GPL v2 or +later, and it keeps GPLv3 reachable if ArduPilot code is ever wanted. A file +contributed as GPL-2.0-only would break both. + +Do not paste in code from a permissively licensed project without checking that +the attribution requirements survive the relicensing, and do not paste in code +from a GPLv3-only project at all. + ## Specifications before implementations Changes to the log format, the aircraft profile schema, or the DroneCAN diff --git a/LICENSE b/LICENSE index ef3c65f..9efa6fb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,338 @@ -MIT License - -Copyright (c) 2026 Junco Project contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Moe Ghoul, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/LICENSE-HARDWARE b/LICENSE-HARDWARE index 4fe041e..0f10922 100644 --- a/LICENSE-HARDWARE +++ b/LICENSE-HARDWARE @@ -1,4 +1,4 @@ -CERN Open Hardware Licence Version 2 - Permissive +CERN Open Hardware Licence Version 2 - Strongly Reciprocal Preamble @@ -7,11 +7,11 @@ CERN has developed this licence to promote collaboration among hardware designers and to provide a legal tool which supports the freedom to use, study, modify, share and distribute hardware designs and products based on those designs. Version 2 of the CERN Open -Hardware Licence comes in three variants: this licence, CERN-OHL-P -(permissive); and two reciprocal licences: CERN-OHL-W (weakly -reciprocal) and CERN-OHL-S (strongly reciprocal). +Hardware Licence comes in three variants: CERN-OHL-P (permissive); and +two reciprocal licences: CERN-OHL-W (weakly reciprocal) and this +licence, CERN-OHL-S (strongly reciprocal). -The CERN-OHL-P is copyright CERN 2020. Anyone is welcome to use it, in +The CERN-OHL-S is copyright CERN 2020. Anyone is welcome to use it, in unmodified form only. Use of this Licence does not imply any endorsement by CERN of any @@ -21,39 +21,88 @@ their development. 1 Definitions - 1.1 'Licence' means this CERN-OHL-P. + 1.1 'Licence' means this CERN-OHL-S. - 1.2 'Source' means information such as design materials or digital + 1.2 'Compatible Licence' means + + a) any earlier version of the CERN Open Hardware licence, or + + b) any version of the CERN-OHL-S, or + + c) any licence which permits You to treat the Source to which + it applies as licensed under CERN-OHL-S provided that on + Conveyance of any such Source, or any associated Product You + treat the Source in question as being licensed under + CERN-OHL-S. + + 1.3 'Source' means information such as design materials or digital code which can be applied to Make or test a Product or to prepare a Product for use, Conveyance or sale, regardless of its medium or how it is expressed. It may include Notices. - 1.3 'Covered Source' means Source that is explicitly made available + 1.4 'Covered Source' means Source that is explicitly made available under this Licence. - 1.4 'Product' means any device, component, work or physical object, + 1.5 'Product' means any device, component, work or physical object, whether in finished or intermediate form, arising from the use, application or processing of Covered Source. - 1.5 'Make' means to create or configure something, whether by + 1.6 'Make' means to create or configure something, whether by manufacture, assembly, compiling, loading or applying Covered Source or another Product or otherwise. - 1.6 'Notice' means copyright, acknowledgement and trademark notices, - references to the location of any Notices, modification notices - (subsection 3.3(b)) and all notices that refer to this Licence - and to the disclaimer of warranties that are included in the - Covered Source. - - 1.7 'Licensee' or 'You' means any person exercising rights under + 1.7 'Available Component' means any part, sub-assembly, library or + code which: + + a) is licensed to You as Complete Source under a Compatible + Licence; or + + b) is available, at the time a Product or the Source containing + it is first Conveyed, to You and any other prospective + licensees + + i) as a physical part with sufficient rights and + information (including any configuration and + programming files and information about its + characteristics and interfaces) to enable it either to + be Made itself, or to be sourced and used to Make the + Product; or + ii) as part of the normal distribution of a tool used to + design or Make the Product. + + 1.8 'Complete Source' means the set of all Source necessary to Make + a Product, in the preferred form for making modifications, + including necessary installation and interfacing information + both for the Product, and for any included Available Components. + If the format is proprietary, it must also be made available in + a format (if the proprietary tool can create it) which is + viewable with a tool available to potential licensees and + licensed under a licence approved by the Free Software + Foundation or the Open Source Initiative. Complete Source need + not include the Source of any Available Component, provided that + You include in the Complete Source sufficient information to + enable a recipient to Make or source and use the Available + Component to Make the Product. + + 1.9 'Source Location' means a location where a Licensor has placed + Covered Source, and which that Licensor reasonably believes will + remain easily accessible for at least three years for anyone to + obtain a digital copy. + + 1.10 'Notice' means copyright, acknowledgement and trademark notices, + Source Location references, modification notices (subsection + 3.3(b)) and all notices that refer to this Licence and to the + disclaimer of warranties that are included in the Covered + Source. + + 1.11 'Licensee' or 'You' means any person exercising rights under this Licence. - 1.8 'Licensor' means a person who creates Source or modifies Covered - Source and subsequently Conveys the resulting Covered Source - under the terms and conditions of this Licence. A person may be - a Licensee and a Licensor at the same time. + 1.12 'Licensor' means a natural or legal person who creates or + modifies Covered Source. A person may be a Licensee and a + Licensor at the same time. - 1.9 'Convey' means to communicate to the public or distribute. + 1.13 'Convey' means to communicate to the public or distribute. 2 Applicability @@ -78,41 +127,69 @@ their development. 3.1 You may copy and Convey verbatim copies of Covered Source, in any medium, provided You retain all Notices. - 3.2 You may modify Covered Source, other than Notices. + 3.2 You may modify Covered Source, other than Notices, provided that + You irrevocably undertake to make that modified Covered Source + available from a Source Location should You Convey a Product in + circumstances where the recipient does not otherwise receive a + copy of the modified Covered Source. In each case subsection 3.3 + shall apply. You may only delete Notices if they are no longer applicable to the corresponding Covered Source as modified by You and You may add additional Notices applicable to Your modifications. + Including Covered Source in a larger work is modifying the + Covered Source, and the larger work becomes modified Covered + Source. 3.3 You may Convey modified Covered Source (with the effect that You shall also become a Licensor) provided that You: - a) retain Notices as required in subsection 3.2; and + a) retain Notices as required in subsection 3.2; b) add a Notice to the modified Covered Source stating that You have modified it, with the date and brief description of how - You have modified it. + You have modified it; - 3.4 You may Convey Covered Source or modified Covered Source under - licence terms which differ from the terms of this Licence - provided that You: + c) add a Source Location Notice for the modified Covered Source + if You Convey in circumstances where the recipient does not + otherwise receive a copy of the modified Covered Source; and - a) comply at all times with subsection 3.3; and - - b) provide a copy of this Licence to anyone to whom You - Convey Covered Source or modified Covered Source. + d) license the modified Covered Source under the terms and + conditions of this Licence (or, as set out in subsection + 8.3, a later version, if permitted by the licence of the + original Covered Source). Such modified Covered Source must + be licensed as a whole, but excluding Available Components + contained in it, which remain licensed under their own + applicable licences. 4 Making and Conveying Products -You may Make Products, and/or Convey them, provided that You ensure -that the recipient of the Product has access to any Notices applicable -to the Product. +You may Make Products, and/or Convey them, provided that You either +provide each recipient with a copy of the Complete Source or ensure +that each recipient is notified of the Source Location of the Complete +Source. That Complete Source is Covered Source, and You must +accordingly satisfy Your obligations set out in subsection 3.3. If +specified in a Notice, the Product must visibly and securely display +the Source Location on it or its packaging or documentation in the +manner specified in that Notice. + + +5 Research and Development + +You may Convey Covered Source, modified Covered Source or Products to +a legal entity carrying out development, testing or quality assurance +work on Your behalf provided that the work is performed on terms which +prevent the entity from both using the Source or Products for its own +internal purposes and Conveying the Source or Products or any +modifications to them to any person other than You. Any modifications +made by the entity shall be deemed to be made by You pursuant to +subsection 3.2. -5 DISCLAIMER AND LIABILITY +6 DISCLAIMER AND LIABILITY - 5.1 DISCLAIMER OF WARRANTY -- The Covered Source and any Products + 6.1 DISCLAIMER OF WARRANTY -- The Covered Source and any Products are provided 'as is' and any express or implied warranties, including, but not limited to, implied warranties of merchantability, of satisfactory quality, non-infringement of @@ -127,7 +204,7 @@ to the Product. of this Licence and a condition for the grant of any rights granted under this Licence. - 5.2 EXCLUSION AND LIMITATION OF LIABILITY -- The Licensor shall, to + 6.2 EXCLUSION AND LIMITATION OF LIABILITY -- The Licensor shall, to the maximum extent permitted by law, have no liability for direct, indirect, special, incidental, consequential, exemplary, punitive or other damages of any character including, without @@ -143,20 +220,19 @@ to the Product. in relation to such use. -6 Patents +7 Patents - 6.1 Subject to the terms and conditions of this Licence, each + 7.1 Subject to the terms and conditions of this Licence, each Licensor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as - stated in this section 6, or where terminated by the Licensor - for cause) patent licence to Make, have Made, use, offer to - sell, sell, import, and otherwise transfer the Covered Source - and Products, where such licence applies only to those patent - claims licensable by such Licensor that are necessarily - infringed by exercising rights under the Covered Source as - Conveyed by that Licensor. - - 6.2 If You institute patent litigation against any entity (including + stated in subsections 7.2 and 8.4) patent licence to Make, have + Made, use, offer to sell, sell, import, and otherwise transfer + the Covered Source and Products, where such licence applies only + to those patent claims licensable by such Licensor that are + necessarily infringed by exercising rights under the Covered + Source as Conveyed by that Licensor. + + 7.2 If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Covered Source or a Product constitutes direct or contributory patent infringement, or You seek any declaration that a patent @@ -165,13 +241,13 @@ to the Product. terminate as of the date such process is initiated. -7 General +8 General - 7.1 If any provisions of this Licence are or subsequently become + 8.1 If any provisions of this Licence are or subsequently become invalid or unenforceable for any reason, the remaining provisions shall remain effective. - 7.2 You shall not use any of the name (including acronyms and + 8.2 You shall not use any of the name (including acronyms and abbreviations), image, or logo by which the Licensor or CERN is known, except where needed to comply with section 3, or where the use is otherwise allowed by law. Any such permitted use @@ -179,7 +255,7 @@ to the Product. of endorsement or implication of involvement by the Licensor or its personnel. - 7.3 CERN may publish updated versions and variants of this Licence + 8.3 CERN may publish updated versions and variants of this Licence which it considers to be in the spirit of this version, but may differ in detail to address new problems or concerns. New versions will be published with a unique version number and a @@ -194,6 +270,20 @@ to the Product. may apply this or any later version of CERN-OHL with the same variant identifier published by CERN. - 7.4 This Licence shall not be enforceable except by a Licensor + 8.4 This Licence shall terminate with immediate effect if You fail + to comply with any of its terms and conditions. + + 8.5 However, if You cease all breaches of this Licence, then Your + Licence from any Licensor is reinstated unless such Licensor has + terminated this Licence by giving You, while You remain in + breach, a notice specifying the breach and requiring You to cure + it within 30 days, and You have failed to come into compliance + in all material respects by the end of the 30 day period. Should + You repeat the breach after receipt of a cure notice and + subsequent reinstatement, this Licence will terminate + immediately and permanently. Section 6 shall continue to apply + after any termination. + + 8.6 This Licence shall not be enforceable except by a Licensor acting as such, and third party beneficiary rights are specifically excluded. diff --git a/MAINTAINERS.md b/MAINTAINERS.md index e8aefa5..42cf755 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -37,7 +37,24 @@ open work, not aspirations. ## Why the licenses differ -Firmware is MIT, hardware is CERN-OHL-P-2.0, documentation is CC-BY-4.0. -MIT is drafted for source code and does not cleanly cover hardware design -artifacts. CERN-OHL-P is the permissive license actually written for hardware. -Neither is a copyleft choice: anyone may build, modify, sell, or fork Junco. +Firmware is GPL-2.0-or-later, hardware is CERN-OHL-S-2.0, documentation and +specifications are CC-BY-4.0. + +The code and the hardware are copyleft because the purpose of the project is to +put this capability into the world permanently, and a permissive license lets a +better funded fork take the work closed and outrun the original. Anyone may +still build, modify, sell, or fork Junco. What they may not do is stop passing +it on. + +GPL v2 **or later** matches MakerPlane, so code moves in both directions between +this project and FIX-Gateway or pyEFIS without relicensing. The "or later" also +keeps GPLv3 reachable if ArduPilot code is ever wanted for the v2 bus stage. + +CERN-OHL-S is the strongly reciprocal license actually written for hardware +design artifacts. A software license does not cleanly cover board files and +mechanical models. + +The specifications are the deliberate exception and stay CC-BY, so anyone can +implement them in anything, including a closed product. A protocol that cannot +be adopted freely does not outlive its implementation, which is the entire +reason `spec/` exists. diff --git a/README.md b/README.md index 64e814c..b17c146 100644 --- a/README.md +++ b/README.md @@ -55,17 +55,37 @@ If you feed Junco data into an autopilot, you have made Junco flight-critical fo --- +## Relationship to other projects + +Junco is an engine and air data front end for aircraft that existing open avionics do not cover. It is not a competing instrument system. + +| Project | What it does | How Junco relates | +|---|---|---| +| [MakerPlane](https://github.com/makerplane) | FIX-Gateway data broker, pyEFIS display, CAN-FIX bus protocol for experimental aircraft | Junco publishes into FIX-Gateway through a plugin, so pyEFIS can display Junco data. CAN-FIX is the bus candidate for v2 | +| [Stratux](https://github.com/cyoung/stratux) | Dual band ADS-B receiver on a Raspberry Pi | Junco does not receive ADS-B. Traffic comes from a receiver the pilot already owns | +| [AP_Periph](https://dronecan.github.io/Implementations/AP_Periph/) | ArduPilot's DroneCAN peripheral node firmware | A candidate for the v2 bus stage. Not used in v1: no BLE, STM32 only, and no thermocouple or ignition-pulse tach support | + +What none of them covers, and what Junco is actually for: a **two-stroke engine with CDI ignition and no ECU.** Four thermocouples, two opto-isolated ignition-pulse tachometers, sub-100 Pa differential pressure, and a static plenum that works in prop blast. The open engine monitor plugins that exist assume an engine with a computer in it. A Part 103 aircraft does not have one. + +--- + ## Licensing Different artifacts, different licenses. See [LICENSE](LICENSE), [LICENSE-HARDWARE](LICENSE-HARDWARE), and [LICENSE-DOCS](LICENSE-DOCS). -| Artifact | License | -|---|---| -| Firmware, app, tools | MIT | -| Board files, enclosure models | CERN-OHL-P-2.0 | -| Documentation and specifications | CC-BY-4.0 | +| Artifact | License | SPDX | +|---|---|---| +| Firmware, app, tools | GNU GPL v2 or later | `GPL-2.0-or-later` | +| Board files, enclosure models | CERN-OHL-S-2.0 | `CERN-OHL-S-2.0` | +| Documentation and specifications | CC-BY-4.0 | `CC-BY-4.0` | + +You may build Junco for yourself. You may sell assembled units. You may fork it, rename it, and compete with it. That is intentional. What you may not do is take it closed: improvements to the firmware and the hardware return to everyone under the same terms. + +Firmware is GPL v2 **or later**, which matches MakerPlane exactly, so code moves in both directions between Junco and FIX-Gateway or pyEFIS without friction. Every source file carries: + + SPDX-License-Identifier: GPL-2.0-or-later -You may build Junco for yourself. You may sell assembled units. You may fork it, rename it, and compete with it. That is intentional. +**The specifications stay permissive deliberately.** They are meant to be implemented by anyone, including in closed products, because a protocol nobody is allowed to adopt is a protocol nobody adopts. CAN-FIX is Creative Commons for the same reason. --- diff --git a/app/README.md b/app/README.md index ef41fb1..82fde4b 100644 --- a/app/README.md +++ b/app/README.md @@ -11,3 +11,5 @@ Native rather than a web app, for two reasons in order of weight: a PWA cannot r iOS is not on the critical path for v1, but the protocol is deliberately reachable from it: BLE is available to third-party iOS apps, which is why the link is BLE rather than Bluetooth Classic SPP. There is no GDL90 output in v1, so an iOS user gets nothing from Junco until someone writes that client. Traffic display, if anyone builds it, is an app-side channel that never involves the node. See docs/prd.md section 22. + +This app is the reference client and the only one required in v1, but it is no longer the only client. A build with a panel and a Pi can reach pyEFIS through a FIX-Gateway plugin instead. See docs/prd.md section 24. The primary aircraft has no panel, which is why the phone comes first. diff --git a/docs/prd.md b/docs/prd.md index 913d2e9..374daba 100644 --- a/docs/prd.md +++ b/docs/prd.md @@ -8,7 +8,9 @@ Target publish: AirVenture 2027 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. Section 24 records what changed and why. +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. Section 25 records what changed and why. --- @@ -209,6 +211,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 @@ -416,11 +420,15 @@ Phase 0 gains thermal instrumentation as well. If the Pi-class build is going to | Artifact | License | |---|---| -| Firmware | MIT | -| Android app | MIT | -| 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 @@ -451,7 +459,11 @@ 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 | +| 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 | 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 | | 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 | @@ -475,22 +487,24 @@ Successors break what they do not understand the reason for. Every rejection bel --- -## 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 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 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 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. --- @@ -515,11 +529,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 @@ -579,9 +593,11 @@ If effort goes to exactly one internet feed, it goes here and not to traffic. **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 against the MIT plan in section 17 before bundling anything. +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. --- @@ -636,7 +652,99 @@ Both builds meet the same channel requirements in section 8, write the same self --- -## 24. Revision history +## 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 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 diff --git a/spec/README.md b/spec/README.md index 03327b5..677759c 100644 --- a/spec/README.md +++ b/spec/README.md @@ -8,7 +8,7 @@ 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 | +| `dronecan-engine-extension.md` | Draft. Not implemented in v1, and its choice of bus is reopened. See PRD section 24 | | `ble-telemetry.md` | **Not started. Required for v1, and the one that blocks code** | `ble-telemetry.md` is the gating document. It carries every value the pilot diff --git a/spec/dronecan-engine-extension.md b/spec/dronecan-engine-extension.md index b580272..323f8db 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 From 3d2fa24669b1bf70d9efd2c560090dfe43f93943 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 00:21:58 +0000 Subject: [PATCH 4/7] Draft the BLE telemetry specification This is the document that was blocking firmware and app work. Every value the pilot sees crosses this link, and PRD success criterion 4 requires a stranger to write a second client from it without reading the Android source. Revision 4 made that requirement easier to hold honestly: there are now two expected reference clients, the Android app and a FIX-Gateway plugin in Python. A rule only one of them can satisfy is the wrong rule. Settled here: - Four notify characteristics, one per rate class, taken from the channel table in PRD section 8 rather than one characteristic per channel - A common header on every notification so an unrecognised characteristic can be discarded safely - Per-channel value, source, status triples, with the status bit authoritative for validity rather than the value - Connection interval, MTU negotiation, and notifications over indications - Monotonic node time with the client writing wall clock on connect, and published timestamps explicitly not corrected afterward - Exactly two writable characteristics, neither able to influence a published value, which is design rule 1 expressed in the protocol The source tag enumeration is defined here and referenced from log-format.md rather than restated, because two copies of an enumeration diverge. It allocates phone-side sources too, which never cross this link but do appear in the log. The mixed-derivation tag is load-bearing: a value combining node and phone channels inherits the weaker assurance of the two, and vertical speed may never carry it. Eight items are recorded as not yet specified, including the UUID base, which must be generated once and frozen before any unit ships, and the two-clients-at-once case that section 24 turned from hypothetical into real. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W8P8RZQELMR3NQ7Zacx79v --- firmware/README.md | 2 +- spec/README.md | 16 ++-- spec/ble-telemetry.md | 200 ++++++++++++++++++++++++++++++++++++++++++ spec/log-format.md | 7 +- 4 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 spec/ble-telemetry.md diff --git a/firmware/README.md b/firmware/README.md index c56c42f..882e35b 100644 --- a/firmware/README.md +++ b/firmware/README.md @@ -6,4 +6,4 @@ Phase 0 is an EMI reality check, not firmware: an ESP32-S3 and one baro sensor o 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. -Nothing substantial can be written here before `spec/ble-telemetry.md` exists, because that link carries every value the pilot sees. +`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/spec/README.md b/spec/README.md index 677759c..685472d 100644 --- a/spec/README.md +++ b/spec/README.md @@ -9,13 +9,13 @@ 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, and its choice of bus is reopened. See PRD section 24 | -| `ble-telemetry.md` | **Not started. Required for v1, and the one that blocks code** | +| `ble-telemetry.md` | Draft. Required for v1. The gating document | -`ble-telemetry.md` is the gating document. It carries every value the pilot -sees, both the firmware and the app are written against it, and PRD success -criterion 4 requires a stranger to write a second client from it without reading -the Android source. Little of substance should be implemented before it exists. +`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 shared between the BLE -protocol and the log format. Define it once, in one document, and reference it -from the other. +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. diff --git a/spec/ble-telemetry.md b/spec/ble-telemetry.md new file mode 100644 index 0000000..73644fe --- /dev/null +++ b/spec/ble-telemetry.md @@ -0,0 +1,200 @@ +# 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 provisional.** They must be fixed before any unit ships, +because once hardware is in the field they are permanent. Regenerate once, +record the decision here, and never change them again. + +Base: `f5a2c1e0-XXXX-4b7a-9c3d-1e6f8a2b4d70` + +| Slot | Characteristic | Properties | Rate | +|---|---|---|---| +| `0001` | Junco service | — | — | +| `0010` | Air data | notify | 25 Hz | +| `0011` | Engine speed | notify | 5 Hz | +| `0012` | Temperatures | notify | 2 Hz | +| `0013` | Slow channels | notify | 1 Hz | +| `0020` | Node status | notify, read | on change | +| `0030` | Aircraft profile | read | on connect | +| `0040` | Clock | write | on connect | +| `0041` | Configuration | 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. Float-typed values additionally carry NaN when invalid, +matching the convention already used in `dronecan-engine-extension.md`, but the +status bit is authoritative and a client must not infer validity from the value. + +**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. + +## 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. + +## 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. +A profile larger than one MTU is read in sequential chunks; the encoding is +listed as not yet specified below. + +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 therefore rejected while a flight is in progress, and the definition +of "in progress" is listed as not yet specified below. + +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. + +## 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 UUID base, which must be generated once and then frozen +- Exact field widths and scaling for each channel, per rate class +- Profile encoding and chunking over the read characteristic +- Configuration characteristic contents and its write schema +- What "flight in progress" means for rejecting configuration writes +- Whether node status carries channel transition counts, or whether the client + derives them from the validity bits it has already seen +- Pairing and bonding, and whether an unpaired client may subscribe at all +- Behavior when two clients subscribe at once, which the phone-plus-FIX-Gateway + case in PRD section 24 makes a real scenario rather than a hypothetical diff --git a/spec/log-format.md b/spec/log-format.md index a76bbd7..ea9e276 100644 --- a/spec/log-format.md +++ b/spec/log-format.md @@ -51,6 +51,11 @@ 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. @@ -69,7 +74,5 @@ its own can still be placed in real time. - Record type identifiers and their allocation - Descriptor record encoding -- The source tag enumeration. This is shared with the BLE protocol and should be - defined once and referenced, not written twice - Whether records are a raw partition or a file on FAT - Export mapping to CSV and GPX From 6bbba8792f89106fc42bd34facf6e14d9917d67f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 01:01:34 +0000 Subject: [PATCH 5/7] PRD revision 5: close the open specification questions Fifteen questions sitting in "not yet specified" lists were decidable at a desk and were blocking firmware. A question that can be answered without data, and is instead left open, becomes a decision someone makes accidentally while implementing. The three irreversible ones first, since each costs field hardware to change later: - BLE UUID base generated and frozen, with the allocation slot scheme recorded so future versions can add characteristics but never redefine an existing one - Log is a preallocated file on FAT32, not a raw partition. Design rule 4 says the owner owns the data, and a file means they pull the card and read it on any laptop. Raw-sector recovery already handles the interrupted-write case that usually argues for a partition - Profile hash is SHA-256 over the bytes actually stored, not over a re-serialization, so changing the serializer cannot silently invalidate every cached copy and log header referencing an unmodified profile Also closed: integer SI channel encoding with no floats and no display units on the wire, which drops the redundant NaN convention since the status bit is now the only validity signal; whole-profile atomic configuration writes; "in flight" defined as the log file being open, which is one condition the node already tracks; node-side transition counts, because a client that connected late would count wrong; bonding required to write but not to subscribe, putting the security boundary on the design rule 1 line; two concurrent clients with first-clock-write- wins; TOML source plus a compiled binary form, which resolves the tension between hand-editability and not parsing text in flight firmware; schema versioning that refuses an unknown version rather than guessing; validation rules; record type allocation; ArduPilot-style descriptors; and seizure precursors publishing a raw rate rather than a judgment. Log record payloads are now byte-identical to BLE characteristic payloads, with the log adding only magic, type and CRC. One serializer instead of two removes the category of defect where the link and the card disagree about what a flight contained. Design rule 9 added: advisory-only data never raises an alert. This was local to section 22 in revision 3 and generalises past traffic, and a rule living inside one section gets forgotten by the next section that needs it. New docs/open-questions.md indexes what remains, separated into what needs a measurement, what is blocked on a prior decision, and what is merely not urgent. Spec open items drop from 19 to 10. Also fixes the status line, which was left at revision 3 when revision 4 landed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01W8P8RZQELMR3NQ7Zacx79v --- MAINTAINERS.md | 4 +- README.md | 3 + docs/open-questions.md | 118 ++++++++++++++++++++ docs/prd.md | 23 +++- spec/README.md | 7 ++ spec/aircraft-profile.md | 74 ++++++++++++- spec/ble-telemetry.md | 178 ++++++++++++++++++++++++------ spec/dronecan-engine-extension.md | 26 ++++- spec/log-format.md | 47 +++++++- 9 files changed, 426 insertions(+), 54 deletions(-) create mode 100644 docs/open-questions.md diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 42cf755..904a3a8 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -24,7 +24,9 @@ continue existing. ## Stewardship checklist Status of the things that make the project survivable. Unchecked items are -open work, not aspirations. +open work, not aspirations. `docs/open-questions.md` records the order worth +doing them in; naming a co-maintainer comes first, because every other item +assumes someone is there to act on it. - [ ] GitHub organization with at least two owners (not a personal account) - [ ] Mirror to a second forge diff --git a/README.md b/README.md index b17c146..4a7a67c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Target cost is under $180 in parts. Target build is one person with a soldering See [docs/prd.md](docs/prd.md) section 16 for the phase plan and section 15 for the assumptions that only flight testing can resolve. +[docs/open-questions.md](docs/open-questions.md) lists everything still undecided, in one place, separated into what needs a measurement and what needs a decision. + --- ## Design rules @@ -28,6 +30,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, hardware revision, and calibration date. 8. **Every channel declares its source.** A value from the phone's barometer and a value from a plumbed static plenum are not interchangeable. The display, the log, and the protocol must all say which one produced a given reading. +9. **Advisory-only data never raises an alert.** Data whose coverage or latency cannot be relied on may be displayed, marked as what it is, but may not drive audio or any advisory. --- 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 374daba..5a66a30 100644 --- a/docs/prd.md +++ b/docs/prd.md @@ -2,7 +2,7 @@ **Open engine and air data node for Part 103 and experimental aircraft** -Status: Draft, revision 3 +Status: Draft, revision 5 Target aircraft: ParaPlane PM-2 (twin engine powered parachute) Target publish: AirVenture 2027 @@ -10,7 +10,9 @@ Revision 2 moved position and attitude sensing onto the pilot's phone, moved the 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. Section 25 records what changed and why. +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. --- @@ -36,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. --- @@ -567,7 +570,7 @@ Unlike fuel, the selection is **app-side configuration and not part of the aircr ### 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.** It is a map layer. It may not drive audio, may not drive the annunciator, and may not be the basis of any advisory. +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 @@ -732,6 +735,20 @@ But the boundary becomes a software boundary rather than a physical one, and sof ## 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. diff --git a/spec/README.md b/spec/README.md index 685472d..8fa14d1 100644 --- a/spec/README.md +++ b/spec/README.md @@ -19,3 +19,10 @@ 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 21516ca..1c30b0e 100644 --- a/spec/aircraft-profile.md +++ b/spec/aircraft-profile.md @@ -38,6 +38,72 @@ 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 @@ -51,7 +117,7 @@ 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 index 73644fe..d5e5000 100644 --- a/spec/ble-telemetry.md +++ b/spec/ble-telemetry.md @@ -31,23 +31,25 @@ From PRD section 2 and section 14: ## Service and characteristic layout -**These UUIDs are provisional.** They must be fixed before any unit ships, -because once hardware is in the field they are permanent. Regenerate once, -record the decision here, and never change them again. - -Base: `f5a2c1e0-XXXX-4b7a-9c3d-1e6f8a2b4d70` - -| Slot | Characteristic | Properties | Rate | -|---|---|---|---| -| `0001` | Junco service | — | — | -| `0010` | Air data | notify | 25 Hz | -| `0011` | Engine speed | notify | 5 Hz | -| `0012` | Temperatures | notify | 2 Hz | -| `0013` | Slow channels | notify | 1 Hz | -| `0020` | Node status | notify, read | on change | -| `0030` | Aircraft profile | read | on connect | -| `0040` | Clock | write | on connect | -| `0041` | Configuration | read, write | rare | +**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 @@ -72,9 +74,12 @@ Each channel in the payload is then a triple: | `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. Float-typed values additionally carry NaN when invalid, -matching the convention already used in `dronecan-engine-extension.md`, but the -status bit is authoritative and a client must not infer validity from the value. +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 @@ -83,6 +88,32 @@ 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 @@ -121,6 +152,37 @@ 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 @@ -152,8 +214,21 @@ 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. -A profile larger than one MTU is read in sequential chunks; the encoding is -listed as not yet specified below. + +**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 @@ -165,12 +240,45 @@ 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 therefore rejected while a flight is in progress, and the definition -of "in progress" is listed as not yet specified below. +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 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 @@ -188,13 +296,11 @@ client sizing its parse from the old layout will mis-slice the new one. ## Not yet specified -- The UUID base, which must be generated once and then frozen -- Exact field widths and scaling for each channel, per rate class -- Profile encoding and chunking over the read characteristic -- Configuration characteristic contents and its write schema -- What "flight in progress" means for rejecting configuration writes -- Whether node status carries channel transition counts, or whether the client - derives them from the validity bits it has already seen -- Pairing and bonding, and whether an unpaired client may subscribe at all -- Behavior when two clients subscribe at once, which the phone-plus-FIX-Gateway - case in PRD section 24 makes a real scenario rather than a hypothetical +- 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 323f8db..76d8298 100644 --- a/spec/dronecan-engine-extension.md +++ b/spec/dronecan-engine-extension.md @@ -58,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 ea9e276..41c1e20 100644 --- a/spec/log-format.md +++ b/spec/log-format.md @@ -70,9 +70,48 @@ 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 From 6bc12963f052ce4291d4fd41acc5aa74e67065b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 03:31:45 +0000 Subject: [PATCH 6/7] Add the PFD layout mockup as an installable web app A primary flight display laid out the way pilots already read one: attitude centre, airspeed tape left, altitude tape right, VSI, HSI below, engine strip down the edge. It exists to settle layout questions before anyone writes an APK, because a tape range or a band order is cheap to change here and expensive once there is an app. It answers one question already. Design rule 8 as twenty small source tags reads as noise on a real layout. The answer this proposes is the one real avionics already use: annunciate the source on the instrument. PHONE AHRS ADVISORY sits on the attitude indicator, cyan marks node-plumbed channels, and magenta marks GPS-derived data, which is a convention every pilot already reads without thinking. Same rule, native idiom, almost no visual cost. It half-answers a second. The attitude is low-pass filtered because raw phone fusion is too jittery to watch sitting still on a desk. Section 15 asks whether phone attitude survives a vibrating airframe; this is an early data point and not an encouraging one. Runs with no sensors at all: a scripted circuit on load, and drag anywhere to fly it by hand. That is enough to judge layout and arguably better than real sensors, since it holds sustained bank angles nobody produces waving a phone around. Real attitude needs a top-level HTTPS document, because device orientation is gated by Permissions Policy and never fires in a cross-origin iframe. Packaged as a PWA with a service worker, so once installed to the home screen it runs fullscreen, landscape-locked, screen awake, and 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. Section 19's PWA row is sharpened rather than reversed. Web Bluetooth reaches the node on Android and would genuinely work, so the transport objection now applies to iOS alone, and the socket binding objection scopes to the on-demand Wi-Fi AP since revision 2 moved the in-flight link to BLE. What still decides it is duration: no background operation, a gesture per connection, and the link drops when the page suspends. A second row records that a PWA is adopted as a prototyping vehicle, which is a different question from the shipping client. --- app/README.md | 2 + app/mockup/README.md | 74 +++ app/mockup/icon-192.png | Bin 0 -> 1515 bytes app/mockup/icon-512.png | Bin 0 -> 3581 bytes app/mockup/index.html | 986 ++++++++++++++++++++++++++++++++ app/mockup/manifest.webmanifest | 33 ++ app/mockup/sw.js | 60 ++ docs/prd.md | 3 +- 8 files changed, 1157 insertions(+), 1 deletion(-) create mode 100644 app/mockup/README.md create mode 100644 app/mockup/icon-192.png create mode 100644 app/mockup/icon-512.png create mode 100644 app/mockup/index.html create mode 100644 app/mockup/manifest.webmanifest create mode 100644 app/mockup/sw.js diff --git a/app/README.md b/app/README.md index 82fde4b..3ebfb39 100644 --- a/app/README.md +++ b/app/README.md @@ -12,4 +12,6 @@ iOS is not on the critical path for v1, but the protocol is deliberately reachab Traffic display, if anyone builds it, is an app-side channel that never involves the node. See docs/prd.md section 22. +`mockup/` holds a PFD layout prototype as an installable web app. It is not the client and is not on the path to being it — it exists to settle layout questions before anyone writes an APK, because a tape range is cheap to change there and expensive here. See its README for what it answers and how to serve it. + This app is the reference client and the only one required in v1, but it is no longer the only client. A build with a panel and a Pi can reach pyEFIS through a FIX-Gateway plugin instead. See docs/prd.md section 24. The primary aircraft has no panel, which is why the phone comes first. diff --git a/app/mockup/README.md b/app/mockup/README.md new file mode 100644 index 0000000..bb016bb --- /dev/null +++ b/app/mockup/README.md @@ -0,0 +1,74 @@ +# PFD layout mockup + +A primary flight display laid out the way pilots already read one: attitude in +the centre, airspeed tape left, altitude tape right, VSI, HSI below, engine +strip down the edge. + +**This is not the client and is not on the path to being it.** It exists to +answer layout questions before anyone writes an APK, because a tape range or a +band order is cheap to change here and expensive once there is an app. + +## What it is for + +Three questions it was built to answer: + +1. **Does design rule 8 survive a real layout?** Twenty small source tags read as + noise. The answer this mockup proposes is the one real avionics already use: + annunciate the source on the instrument. `PHONE AHRS · ADVISORY` on the + attitude indicator, cyan for node-plumbed channels, magenta for GPS-derived. +2. **Is phone attitude worth showing at all?** PRD section 15 lists this as open. + The attitude here is low-pass filtered because raw phone fusion is too jittery + to look at sitting still on a desk, which is an early data point and not an + encouraging one. +3. **Does an HSI earn its space** on an aircraft that mostly flies straight? + +## Running it + +It works with no sensors at all. On load it flies a scripted circuit, and +dragging anywhere on the display flies it by hand. That is enough to judge +layout, and arguably better than real sensors, because it holds sustained bank +angles you would not produce waving a phone around. + +For **real** attitude the page has to be a top-level HTTPS document. Device +orientation is gated by Permissions Policy, so in a cross-origin iframe it never +fires, and a page cannot grant itself the permission. + +| Where | What you get | +|---|---| +| Opened inside any iframe | Demo flight only. The diagnostics panel says why | +| Desktop Chrome, top level | DevTools → More tools → Sensors drives it | +| Phone over HTTPS, top level | Real attitude, heading, track, ground speed, position | +| Installed to the home screen | The above, plus fullscreen, landscape lock, screen wake lock, and offline | + +The simplest way to get the last row: publish this directory with GitHub Pages +and open it on the phone, then use the browser's *Add to Home Screen*. The +service worker caches the shell on first load, so it runs afterwards with no +signal. + +## What is real and what is invented + +**Real, from the device:** attitude, heading, track, ground speed, GPS altitude, +position. + +**Simulated:** the entire engine strip, indicated airspeed, pressure altitude, +and vertical speed. Those are node channels and no node exists. Values are +plausible PM-2 cruise numbers. + +**Impossible:** pressure altitude and vertical speed can never come from the +phone, because there is no web API for ambient pressure. PRD section 6 reaches +the same conclusion from a different direction, and this is one more reason the +plumbed plenum is not optional. + +## Why this is a web page when the client will be native + +A PWA is the right shape for a prototype and the wrong shape for the instrument. +It installs in seconds, needs no store account, no signing key, and no build +toolchain, so a layout question gets answered the same afternoon it is asked. + +What it cannot do is hold a BLE link for a two hour flight. Web Bluetooth exists +on Android and would genuinely reach the node, but it needs a user gesture per +connection, has no background operation, and drops when the page is suspended. +It does not exist on iOS at all. See PRD section 19. + +Nothing in this directory should end up in the shipping client. Only the layout +decisions should. diff --git a/app/mockup/icon-192.png b/app/mockup/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..074704a3eaae7312e3abf06c6139ceee18e8cc6b GIT binary patch literal 1515 zcmX|>dpOez7{~Yf?LKzQTw;4Byp(OrnucC=}Vr(cWFD z59fmbD%n@7;G6AW}No_u`_!Q}a~Fn0X>cLnF%0&27*ZLyV(4x~4R&txF1X z%{nX;ZWgHK<>~M@E=v>&hg_louEaSIcsCJVT=T-`!$&#y;n>XF{?@g}_E(lzN`j76 zf)omYfWd&^F9N`P4jhM3KPd=YJ8^Uw!8G{Z^zY~3`+pxnLIldc&lexckqTG;%4~l2o#al zVw>6ni}hL!RZjuCSI5L9ZN-ZE#+N+~qw){UIGlbG4c-0N%2@IaDC8tos^SIJW4yoo ztdP{Z29R1j5K*JXJ&P4|MBt=<6Y~O^b!EGZkm0URwoEkOSsb$-Bi z;<_bdpaxgXzzgn8d3rBcjtHMHcWmo~wbM^Tw1Kz0r%2JKwIogBhOW@RD!y@Q>L4p~ zj$utSkPsuOY(#v7`PNASH@>ey#e%pMlSigBmLh9gA7vf{zNLjNoW+px-i*8(XE{Qp z83|rHz$lz=0zp=O)vfL}9cYdu`KnXG@!>%qQpR1_2=i^)K}i2X-Rgc=Y7l=w4D_|f zsMZqkoriu>ZW^6Jmc8$Gm~ZPa_&$2!E3dkL_7n34nmybzhYj!K_QSdQT3s_SR1zC5 z?{H2c%X0J8&X8=JZ1IvqKE!r_Ysz|Rj9m!z1e!G9CT@;fP8(zO=c^cuJtRa&`K^!L z*w0)nLrGoDY*!%)jw12@X)&NbH1cW7q~hM3!eJ1-_xSvVFWSI2yU^@jrPOCOuIYUZ z7TZ~Ohvn7i&KvdHviv?tU2?oUAX3X1=~j|~%oMtevm|+5_iWA2BQdlT}K+d~w8A46izWJKmxSbl#`qg*S6v8RdkgZNZzZz=^zV z%72qzI{2B2FB^MpKGC5^A?Ms$lnhgYp6$Bs@w`Htm>0A`oBfI)bLV9TT|ZYZaR*Gg z&lV*tvNKb`mB}-FWIHVBw9xGrBB3*$*?TEhWVHi8;X=>&%6)i|_cAfA#PNb2gHHS6 z<5me%n;bS7-B?Tvs6tl6(zG0iOGa1q@^Md2bGA>zRFzY^C7iYj4|Tr(qdTgk zoE>YF?%%65BOgQsxu!@H*)xkdu_4zCxqpPm3gJqhw(Icy$C8}4y7ap{qvf3JN0%Br zs+G4-4a0dsUm+>FG40r%b?l+cr|Y4ieGTH+!m3a)Zs)fHfa{p?4V~tfs!gKgb zfU5aqy^M4c9h#Cnq*!er(Mm@*1%D11jRC8&-krB2ZvuC{C1Ix4_~K%#ooPd zf?1%vq8!-W?jDhuuOg#4^}R0ES^R+e^EpBgoesnEq)>j5K$y=3KUAn~WAQtyvAW79 Oj&fS(YF}jM7xy0#$(-B( literal 0 HcmV?d00001 diff --git a/app/mockup/icon-512.png b/app/mockup/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..c51ac3d5d93df20cd922ec0880be0d7e6ef4c554 GIT binary patch literal 3581 zcmbVOYgAKL7T)LPlEeTKq=2BpH7GvN5|Aodm8(KQ1f_`92d%N7W@%L%r3ffF_@o6b z7AlAjVyj3ImHLD-NZR_qf`H-*iqjz=ii(ON@ev`Jb8|B;e$1Z^Yh{sh&)MJp_P4*Y zbH9n09c(Lcl>mV4%#grH0E&F2K+OG1cAas8JijzEa9Y&Hrl)rw?bps5a&=vPe7wFm zXy?AA`?v3FDJzvd^BVneMp`M;x+Tq8_FZb>89U}&MApq3NqEgsW0o=1 zl|HpMAJ6~@SO6*fUtj-Tc;1Ar zs!Gp$_ZW!yJuhL02Mg-f!>Tv=iR)s;kX@-S!hkO~3TPN}(T#z)4c!T4r+T_Jx}lPHBU3Lhur1FhGNF5kA5i#F zuuuwN0lp-Wsar+w>WZUr%`sJbaJ}Ta5Rms)0mTX%RN^Atx_JN)y#!nwtHmz!{1ITrtQ~dOJ6}%bwr1f`;#G7`V}BVlK74 zwZ@M`1Zw&T)9$Srd*L*oTyDRZpqPAy0j;|qAbBq%5VJ!&RCP;IAIY65Mej6$6gun( zVs1(%qM5h)W}~w@{z@Q8L$aFpF|A*#vMbz@XwRQKa^9FwDT=y&OU!QRFs)FJv)Ro@ zYYbb33=H|)w7Qeh`v&$!k2`TS{uwG=#66?||yvf&B z_S&&OhaCVjHpfcLzR?H3rKR1pA{x%SL90B8io12-ga|9c@*(4dX)zl{5+YkER(7cF zE(VGL3}QB0ZsXkl}EO+OUH^ zt$qdndNZ0G(T|mNm?Ug_KTPHfX9q(6VKj`dpdy@6VR&ajGu)a*6qEugF=C?FIaJ!x z2%|WAD2W!p02OiFP-&Ct%2QMSsBeDb%jVJQ$M8fDLOOeV#l5N6kL!gW3r%jg<25=G z2BR05mY^eZ`a-`%x1Z0#(WHR)RFua4J@nDyvT>8)Y7ec$6H#$@!T2Q-K^ zh6=cEkG&hurFgT6v|lF1ukRtqXw^iF6kqPnys|$qf$Vx!Fa;Nn%HQFF0SAHWQ*La`T0{#j6he zceO>R8-;Ue&2XTZ`lLE%nUHsIN`hNv??yj|~KC?EeqFH;bAntUKWHA2d4C)`=Q&wcNT=E9`+TbW7Go9A(Q5Jg_m3cBfJ9=s`ulC6O&o09e7v!9t|m|pSH zi!&52Ki7gEl4JE^HmpTZvXW*zQCvSVMu-8@9I%hwDOZ$M@am}}Sr?9jKj4N~(~cHQ zv?0bBd_MWUob}^}5ovj#H5P(4U=6q}BKtcb!&>w0QtVF%5i_~t&%Jh_Rw>v*aU~4y zuaWW|uHFeUBHL?S*HA1rAGI7IL-G*SCwf=EwrY&yRYnDE5wKXVMm zqlY3g`jnvPwNkq#jD02w*##SlP)@hth{x5TTtBL=ALLd{gCn=92Q2UK=BhvO@rTbu zg|oEp7P4S~$Fe@Esk7EpmfESaBnxUIskl$w$@K1{MPaoz-2g@NYnH!f=j{W|uTWGl zeE;>`?dwrd#O*~+1Nj~jk|kz-_k@aStM#ZUh*!z1H_rYm>9=S+zpvC)1*pBwN*VRy z?m!vDjL#1cDGV~3pGdWY!?uN{1*8Uz;i|1>Z2NVTo5$DA3V>8)?t9hU>tti#96pWo zFlw(Aipx{Y({bB|hO#rMRWdt8H;E%A;h@ZZTmr47FAm&c2Yc7JYPU~)dNa-BR$co58b$M28m*a?Z zSRc%R1`um7M30!00jr!cea8)ldE<38``A32Y%?26lGPVl$U{lZp1uzcDm6duW7}Qa zQOM=^^ySpSrE!+y=FA~#RKYsdq(iK(DX4-?c5y;g$Iyyy>gkt}mUH_`A7p%?c3d6^ zvE;3i;_9=~J+GdII z%THZXwO!-|Ra_qR=981p4Z7KuC;?yc#tWHjtV9J5z<#3lQ;WuHI;?N>$t8;3WeHl z9p)6~c(9<}w)~C1nA|*arHKv5)cKoV|9(uk!TYU#ZiSD>4i%8UnL)DyPfk~C`4801 By~F?j literal 0 HcmV?d00001 diff --git a/app/mockup/index.html b/app/mockup/index.html new file mode 100644 index 0000000..313f6cb --- /dev/null +++ b/app/mockup/index.html @@ -0,0 +1,986 @@ + + + + + + + + + + + + +Junco — Primary Flight Display + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PHONE AHRS · ADVISORY + + + + + + + + + + + + + + 28 + MPH IAS + NODE PITOT + + + + + + + + + 1180 + FT + NODE PLENUM + + + + + + + 0 + + FPM + + + + + + + + + + + + + + + PHONE GNSS TRACK + + + + ENGINE + + + + + + + + + + POSITION + + ALERTS + + + + + + + +
+ +
+ + + + + + + +
+ +
+

Attitude source diagnostics

+
Modedemo flight
+
Secure context
+
Running in iframe
+
DeviceOrientationEvent
+
Permission request
+
Orientation events seen0
+
Generic Sensor API
+
Geolocation
+
Screen angle
+
+ +
+

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/prd.md b/docs/prd.md index 5a66a30..6c62e70 100644 --- a/docs/prd.md +++ b/docs/prd.md @@ -467,7 +467,8 @@ Successors break what they do not understand the reason for. Every rejection bel | 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 | 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 | +| 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 | From f6f3c5e4aa691870f2c001caaa2b848192bb1365 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 03:44:24 +0000 Subject: [PATCH 7/7] Add an Android wrapper and CI for the PFD mockup APK The wrapper exists for one reason. Device orientation and geolocation are only available to a secure context, so a WebView loading file:// would silently kill exactly the sensors the app is for. It serves the bundled assets over an https:// origin and intercepts those requests locally. Nothing leaves the device and no network is used. Built with the raw SDK tools rather than Gradle. One activity, no dependencies, nothing to resolve, so the Android Gradle Plugin would add a large download and a version matrix to a build that is otherwise six commands. A real client would use Gradle; this is not a real client. Two things worth recording for whoever builds this next: - build-tools 34.0.0 does not work on JDK 21. Its d8 fails on every class with "Cannot invoke String.length() because is null" while writing the dex. build-tools 35 is fine. The script now selects 35 or newer and says so if it cannot find one. - Do not pass file lists to d8 via @argfile. A trailing newline arrives as an empty argument and produces the same opaque NPE for a different reason. The APK is deliberately not committed. .gitignore already excluded *.apk and that decision stands, so the workflow is where the APK comes from: every run uploads it as an artifact, and a mockup-v* tag attaches it to a release. The workflow also checks the APK is signed, declares the right package, and actually contains the web assets, since a wrapper that ships without its payload would still build and install cleanly. This is the repository's first CI. --- .github/workflows/mockup-apk.yml | 78 +++++++++ app/mockup/README.md | 20 +++ app/mockup/android/AndroidManifest.xml | 40 +++++ app/mockup/android/build.sh | 96 +++++++++++ .../android/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1515 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 3581 bytes app/mockup/android/res/values/strings.xml | 4 + .../src/org/junco/mockup/MainActivity.java | 157 ++++++++++++++++++ 8 files changed, 395 insertions(+) create mode 100644 .github/workflows/mockup-apk.yml create mode 100644 app/mockup/android/AndroidManifest.xml create mode 100755 app/mockup/android/build.sh create mode 100644 app/mockup/android/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/mockup/android/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/mockup/android/res/values/strings.xml create mode 100644 app/mockup/android/src/org/junco/mockup/MainActivity.java diff --git a/.github/workflows/mockup-apk.yml b/.github/workflows/mockup-apk.yml new file mode 100644 index 0000000..848834e --- /dev/null +++ b/.github/workflows/mockup-apk.yml @@ -0,0 +1,78 @@ +# Build the PFD layout mockup APK. +# +# The APK is deliberately not committed to the repository: .gitignore excludes +# *.apk on purpose, and a binary in git rots. This workflow is where the APK +# comes from instead. Every run publishes it as a downloadable artifact, and +# pushing a mockup-v* tag also attaches it to a GitHub release. +# +# SPDX-License-Identifier: GPL-2.0-or-later + +name: Mockup APK + +on: + push: + branches: [main] + paths: ['app/mockup/**', '.github/workflows/mockup-apk.yml'] + tags: ['mockup-v*'] + pull_request: + paths: ['app/mockup/**', '.github/workflows/mockup-apk.yml'] + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + # build-tools 35 or newer is required. The d8 in 34.0.0 predates JDK 21 + # and fails on every class with an opaque NPE while writing the dex. + - name: Install SDK packages + run: | + yes 2>/dev/null | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses >/dev/null || true + "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" \ + "platforms;android-34" "build-tools;35.0.0" + + - name: Build APK + run: ./app/mockup/android/build.sh + + - name: Check the APK is well formed + run: | + APK=app/mockup/android/build/junco-pfd-mockup.apk + BT="$ANDROID_HOME/build-tools/35.0.0" + test -f "$APK" + "$BT/apksigner" verify "$APK" + # The whole point of the wrapper is that the web assets ship inside it. + for f in index.html manifest.webmanifest sw.js icon-192.png icon-512.png; do + unzip -l "$APK" | grep -q "assets/$f" || { echo "missing assets/$f"; exit 1; } + done + "$BT/aapt2" dump badging "$APK" | grep -q "package: name='org.junco.mockup'" + echo "APK OK: $(du -h "$APK" | cut -f1)" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: junco-pfd-mockup-apk + path: app/mockup/android/build/junco-pfd-mockup.apk + if-no-files-found: error + + - name: Attach to release + if: startsWith(github.ref, 'refs/tags/mockup-v') + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${GITHUB_REF_NAME}" \ + app/mockup/android/build/junco-pfd-mockup.apk \ + --title "PFD layout mockup ${GITHUB_REF_NAME}" \ + --notes "Layout mockup only. Not an instrument, not airworthy, not the Junco client. Debug-signed for sideloading; enable install from unknown sources. See app/mockup/README.md." \ + || gh release upload "${GITHUB_REF_NAME}" \ + app/mockup/android/build/junco-pfd-mockup.apk --clobber diff --git a/app/mockup/README.md b/app/mockup/README.md index bb016bb..a4f0b13 100644 --- a/app/mockup/README.md +++ b/app/mockup/README.md @@ -45,6 +45,26 @@ and open it on the phone, then use the browser's *Add to Home Screen*. The service worker caches the shell on first load, so it runs afterwards with no signal. +## As an Android app + +`android/` wraps the same files in an APK. It exists for one reason: device +orientation and geolocation are only available to a **secure context**, so a +WebView loading `file://` would silently kill exactly the sensors the app is +for. The wrapper serves the bundled assets over an `https://` origin and +intercepts those requests locally. Nothing leaves the device and no network is +used. + +Build it with `android/build.sh`, which needs `ANDROID_HOME` and build-tools 35 +or newer. It uses the raw SDK tools rather than Gradle, because one activity +with no dependencies does not need a plugin and a version matrix. + +**The APK is not committed.** `.gitignore` excludes `*.apk` deliberately, so the +`Mockup APK` workflow builds it instead: every run uploads it as an artifact, +and pushing a `mockup-v*` tag attaches it to a release. + +It is debug-signed, so installing it means allowing unknown sources. That is +appropriate for a mockup and would not be appropriate for anything else. + ## What is real and what is invented **Real, from the device:** attitude, heading, track, ground speed, GPS altitude, diff --git a/app/mockup/android/AndroidManifest.xml b/app/mockup/android/AndroidManifest.xml new file mode 100644 index 0000000..e835198 --- /dev/null +++ b/app/mockup/android/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/mockup/android/build.sh b/app/mockup/android/build.sh new file mode 100755 index 0000000..1f1e990 --- /dev/null +++ b/app/mockup/android/build.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Build the Junco PFD layout mockup APK. +# +# Deliberately built with the raw SDK tools rather than Gradle. There is one +# activity, no dependencies, and no library resolution to do, so the Android +# Gradle Plugin would add a large download and a version-matrix problem to a +# build that is otherwise six commands. A real client would use Gradle; this is +# not a real client. +# +# Requires: ANDROID_HOME pointing at an SDK with build-tools and platform 34, +# and a JDK. Produces build/junco-pfd-mockup.apk, debug-signed for sideloading. +# +# SPDX-License-Identifier: GPL-2.0-or-later + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WEB="$(cd "$HERE/.." && pwd)" +OUT="$HERE/build" + +: "${ANDROID_HOME:?set ANDROID_HOME to your Android SDK}" + +# build-tools 35 or newer. The d8 shipped in build-tools 34.0.0 predates JDK 21 +# and dies while writing the dex with an opaque "Cannot invoke String.length()" +# NPE on every class. If you hit that, this is why: use a newer build-tools or +# an older JDK. +BT="" +for v in $(ls "$ANDROID_HOME/build-tools" 2>/dev/null | sort -Vr); do + case "$v" in + 3[5-9].*|[4-9][0-9].*) BT="$ANDROID_HOME/build-tools/$v"; break ;; + esac +done +[ -n "$BT" ] || { echo "need build-tools 35.0.0 or newer" >&2; exit 1; } + +JAR="$ANDROID_HOME/platforms/android-34/android.jar" +[ -f "$JAR" ] || { echo "missing platform android-34" >&2; exit 1; } +echo "==> using $(basename "$BT") build-tools, JDK $(java -version 2>&1 | head -1 | sed 's/.*"\(.*\)".*/\1/')" + +rm -rf "$OUT" +mkdir -p "$OUT/assets" "$OUT/flat" "$OUT/gen" "$OUT/classes" + +# The web app is the payload. Copy it in rather than symlink so the APK is +# self-contained and the source of truth stays one directory up. +cp "$WEB/index.html" "$WEB/manifest.webmanifest" "$WEB/sw.js" \ + "$WEB/icon-192.png" "$WEB/icon-512.png" "$OUT/assets/" + +echo "==> resources" +"$BT/aapt2" compile --dir "$HERE/res" -o "$OUT/flat/res.zip" + +echo "==> link" +"$BT/aapt2" link \ + -o "$OUT/base.apk" \ + -I "$JAR" \ + --manifest "$HERE/AndroidManifest.xml" \ + -R "$OUT/flat/res.zip" \ + -A "$OUT/assets" \ + --java "$OUT/gen" \ + --min-sdk-version 24 \ + --target-sdk-version 34 \ + --auto-add-overlay + +echo "==> javac" +# Pass file lists as arguments rather than via @argfile: a trailing newline in +# an argfile arrives as an empty argument and d8 dies on it with an opaque NPE. +javac -nowarn -source 17 -target 17 -encoding UTF-8 \ + -classpath "$JAR" -d "$OUT/classes" \ + $(find "$HERE/src" "$OUT/gen" -name '*.java') 2>&1 | grep -v 'bootstrap class path' || true + +echo "==> dex" +mkdir -p "$OUT/dex" +"$BT/d8" --lib "$JAR" --min-api 24 --output "$OUT/dex" \ + $(find "$OUT/classes" -name '*.class') +cp "$OUT/dex/classes.dex" "$OUT/classes.dex" + +echo "==> package" +cp "$OUT/base.apk" "$OUT/unsigned.apk" +( cd "$OUT" && zip -q -j unsigned.apk classes.dex ) + +echo "==> align" +"$BT/zipalign" -f -p 4 "$OUT/unsigned.apk" "$OUT/aligned.apk" + +echo "==> sign" +KS="$HERE/debug.keystore" +if [ ! -f "$KS" ]; then + keytool -genkeypair -v -keystore "$KS" -storepass android -keypass android \ + -alias juncodebug -keyalg RSA -keysize 2048 -validity 10000 \ + -dname "CN=Junco Mockup Debug, OU=Junco, O=Junco, C=US" >/dev/null 2>&1 +fi +"$BT/apksigner" sign --ks "$KS" --ks-pass pass:android --key-pass pass:android \ + --out "$OUT/junco-pfd-mockup.apk" "$OUT/aligned.apk" + +"$BT/apksigner" verify --print-certs "$OUT/junco-pfd-mockup.apk" | head -3 + +echo +echo "built: $OUT/junco-pfd-mockup.apk" +ls -lh "$OUT/junco-pfd-mockup.apk" | awk '{print $5, $9}' diff --git a/app/mockup/android/res/mipmap-xxhdpi/ic_launcher.png b/app/mockup/android/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..074704a3eaae7312e3abf06c6139ceee18e8cc6b GIT binary patch literal 1515 zcmX|>dpOez7{~Yf?LKzQTw;4Byp(OrnucC=}Vr(cWFD z59fmbD%n@7;G6AW}No_u`_!Q}a~Fn0X>cLnF%0&27*ZLyV(4x~4R&txF1X z%{nX;ZWgHK<>~M@E=v>&hg_louEaSIcsCJVT=T-`!$&#y;n>XF{?@g}_E(lzN`j76 zf)omYfWd&^F9N`P4jhM3KPd=YJ8^Uw!8G{Z^zY~3`+pxnLIldc&lexckqTG;%4~l2o#al zVw>6ni}hL!RZjuCSI5L9ZN-ZE#+N+~qw){UIGlbG4c-0N%2@IaDC8tos^SIJW4yoo ztdP{Z29R1j5K*JXJ&P4|MBt=<6Y~O^b!EGZkm0URwoEkOSsb$-Bi z;<_bdpaxgXzzgn8d3rBcjtHMHcWmo~wbM^Tw1Kz0r%2JKwIogBhOW@RD!y@Q>L4p~ zj$utSkPsuOY(#v7`PNASH@>ey#e%pMlSigBmLh9gA7vf{zNLjNoW+px-i*8(XE{Qp z83|rHz$lz=0zp=O)vfL}9cYdu`KnXG@!>%qQpR1_2=i^)K}i2X-Rgc=Y7l=w4D_|f zsMZqkoriu>ZW^6Jmc8$Gm~ZPa_&$2!E3dkL_7n34nmybzhYj!K_QSdQT3s_SR1zC5 z?{H2c%X0J8&X8=JZ1IvqKE!r_Ysz|Rj9m!z1e!G9CT@;fP8(zO=c^cuJtRa&`K^!L z*w0)nLrGoDY*!%)jw12@X)&NbH1cW7q~hM3!eJ1-_xSvVFWSI2yU^@jrPOCOuIYUZ z7TZ~Ohvn7i&KvdHviv?tU2?oUAX3X1=~j|~%oMtevm|+5_iWA2BQdlT}K+d~w8A46izWJKmxSbl#`qg*S6v8RdkgZNZzZz=^zV z%72qzI{2B2FB^MpKGC5^A?Ms$lnhgYp6$Bs@w`Htm>0A`oBfI)bLV9TT|ZYZaR*Gg z&lV*tvNKb`mB}-FWIHVBw9xGrBB3*$*?TEhWVHi8;X=>&%6)i|_cAfA#PNb2gHHS6 z<5me%n;bS7-B?Tvs6tl6(zG0iOGa1q@^Md2bGA>zRFzY^C7iYj4|Tr(qdTgk zoE>YF?%%65BOgQsxu!@H*)xkdu_4zCxqpPm3gJqhw(Icy$C8}4y7ap{qvf3JN0%Br zs+G4-4a0dsUm+>FG40r%b?l+cr|Y4ieGTH+!m3a)Zs)fHfa{p?4V~tfs!gKgb zfU5aqy^M4c9h#Cnq*!er(Mm@*1%D11jRC8&-krB2ZvuC{C1Ix4_~K%#ooPd zf?1%vq8!-W?jDhuuOg#4^}R0ES^R+e^EpBgoesnEq)>j5K$y=3KUAn~WAQtyvAW79 Oj&fS(YF}jM7xy0#$(-B( literal 0 HcmV?d00001 diff --git a/app/mockup/android/res/mipmap-xxxhdpi/ic_launcher.png b/app/mockup/android/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c51ac3d5d93df20cd922ec0880be0d7e6ef4c554 GIT binary patch literal 3581 zcmbVOYgAKL7T)LPlEeTKq=2BpH7GvN5|Aodm8(KQ1f_`92d%N7W@%L%r3ffF_@o6b z7AlAjVyj3ImHLD-NZR_qf`H-*iqjz=ii(ON@ev`Jb8|B;e$1Z^Yh{sh&)MJp_P4*Y zbH9n09c(Lcl>mV4%#grH0E&F2K+OG1cAas8JijzEa9Y&Hrl)rw?bps5a&=vPe7wFm zXy?AA`?v3FDJzvd^BVneMp`M;x+Tq8_FZb>89U}&MApq3NqEgsW0o=1 zl|HpMAJ6~@SO6*fUtj-Tc;1Ar zs!Gp$_ZW!yJuhL02Mg-f!>Tv=iR)s;kX@-S!hkO~3TPN}(T#z)4c!T4r+T_Jx}lPHBU3Lhur1FhGNF5kA5i#F zuuuwN0lp-Wsar+w>WZUr%`sJbaJ}Ta5Rms)0mTX%RN^Atx_JN)y#!nwtHmz!{1ITrtQ~dOJ6}%bwr1f`;#G7`V}BVlK74 zwZ@M`1Zw&T)9$Srd*L*oTyDRZpqPAy0j;|qAbBq%5VJ!&RCP;IAIY65Mej6$6gun( zVs1(%qM5h)W}~w@{z@Q8L$aFpF|A*#vMbz@XwRQKa^9FwDT=y&OU!QRFs)FJv)Ro@ zYYbb33=H|)w7Qeh`v&$!k2`TS{uwG=#66?||yvf&B z_S&&OhaCVjHpfcLzR?H3rKR1pA{x%SL90B8io12-ga|9c@*(4dX)zl{5+YkER(7cF zE(VGL3}QB0ZsXkl}EO+OUH^ zt$qdndNZ0G(T|mNm?Ug_KTPHfX9q(6VKj`dpdy@6VR&ajGu)a*6qEugF=C?FIaJ!x z2%|WAD2W!p02OiFP-&Ct%2QMSsBeDb%jVJQ$M8fDLOOeV#l5N6kL!gW3r%jg<25=G z2BR05mY^eZ`a-`%x1Z0#(WHR)RFua4J@nDyvT>8)Y7ec$6H#$@!T2Q-K^ zh6=cEkG&hurFgT6v|lF1ukRtqXw^iF6kqPnys|$qf$Vx!Fa;Nn%HQFF0SAHWQ*La`T0{#j6he zceO>R8-;Ue&2XTZ`lLE%nUHsIN`hNv??yj|~KC?EeqFH;bAntUKWHA2d4C)`=Q&wcNT=E9`+TbW7Go9A(Q5Jg_m3cBfJ9=s`ulC6O&o09e7v!9t|m|pSH zi!&52Ki7gEl4JE^HmpTZvXW*zQCvSVMu-8@9I%hwDOZ$M@am}}Sr?9jKj4N~(~cHQ zv?0bBd_MWUob}^}5ovj#H5P(4U=6q}BKtcb!&>w0QtVF%5i_~t&%Jh_Rw>v*aU~4y zuaWW|uHFeUBHL?S*HA1rAGI7IL-G*SCwf=EwrY&yRYnDE5wKXVMm zqlY3g`jnvPwNkq#jD02w*##SlP)@hth{x5TTtBL=ALLd{gCn=92Q2UK=BhvO@rTbu zg|oEp7P4S~$Fe@Esk7EpmfESaBnxUIskl$w$@K1{MPaoz-2g@NYnH!f=j{W|uTWGl zeE;>`?dwrd#O*~+1Nj~jk|kz-_k@aStM#ZUh*!z1H_rYm>9=S+zpvC)1*pBwN*VRy z?m!vDjL#1cDGV~3pGdWY!?uN{1*8Uz;i|1>Z2NVTo5$DA3V>8)?t9hU>tti#96pWo zFlw(Aipx{Y({bB|hO#rMRWdt8H;E%A;h@ZZTmr47FAm&c2Yc7JYPU~)dNa-BR$co58b$M28m*a?Z zSRc%R1`um7M30!00jr!cea8)ldE<38``A32Y%?26lGPVl$U{lZp1uzcDm6duW7}Qa zQOM=^^ySpSrE!+y=FA~#RKYsdq(iK(DX4-?c5y;g$Iyyy>gkt}mUH_`A7p%?c3d6^ zvE;3i;_9=~J+GdII z%THZXwO!-|Ra_qR=981p4Z7KuC;?yc#tWHjtV9J5z<#3lQ;WuHI;?N>$t8;3WeHl z9p)6~c(9<}w)~C1nA|*arHKv5)cKoV|9(uk!TYU#ZiSD>4i%8UnL)DyPfk~C`4801 By~F?j literal 0 HcmV?d00001 diff --git a/app/mockup/android/res/values/strings.xml b/app/mockup/android/res/values/strings.xml new file mode 100644 index 0000000..962d054 --- /dev/null +++ b/app/mockup/android/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Junco PFD + diff --git a/app/mockup/android/src/org/junco/mockup/MainActivity.java b/app/mockup/android/src/org/junco/mockup/MainActivity.java new file mode 100644 index 0000000..b69f3ed --- /dev/null +++ b/app/mockup/android/src/org/junco/mockup/MainActivity.java @@ -0,0 +1,157 @@ +/* + * Junco PFD layout mockup — Android wrapper. + * + * This is deliberately thin. It exists for one reason: device orientation and + * geolocation are only available to a *secure context*, so loading the mockup + * from file:// would silently kill exactly the sensors this app is for. The + * WebView therefore serves the bundled assets over a real https:// origin and + * intercepts those requests locally. Nothing leaves the device and no network + * is used. + * + * This is not the Junco client and is not on the path to being it. See + * ../README.md and PRD section 19. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +package org.junco.mockup; + +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; +import android.webkit.GeolocationPermissions; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +public class MainActivity extends Activity { + + /** Any host works; it just has to be https so the page is a secure context. */ + private static final String ORIGIN = "https://appassets.junco.local"; + private static final String START = ORIGIN + "/index.html"; + + private WebView web; + + @Override + protected void onCreate(Bundle state) { + super.onCreate(state); + + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + web = new WebView(this); + setContentView(web); + + WebSettings s = web.getSettings(); + s.setJavaScriptEnabled(true); + s.setDomStorageEnabled(true); + s.setGeolocationEnabled(true); + s.setMediaPlaybackRequiresUserGesture(false); + s.setAllowFileAccess(false); + s.setAllowContentAccess(false); + s.setCacheMode(WebSettings.LOAD_DEFAULT); + + web.setWebViewClient(new WebViewClient() { + @Override + public WebResourceResponse shouldInterceptRequest(WebView v, WebResourceRequest req) { + return serve(req.getUrl()); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView v, WebResourceRequest req) { + // Keep everything inside the bundled origin. There is nowhere else to go. + return !ORIGIN.equals(req.getUrl().getScheme() + "://" + req.getUrl().getHost()); + } + }); + + web.setWebChromeClient(new WebChromeClient() { + @Override + public void onGeolocationPermissionsShowPrompt(String origin, + GeolocationPermissions.Callback cb) { + // The Android-level permission prompt below is the real gate. + cb.invoke(origin, true, false); + } + }); + + immersive(); + requestLocation(); + + web.loadUrl(START); + } + + /** Serve a bundled asset for a request against our synthetic https origin. */ + private WebResourceResponse serve(Uri uri) { + if (uri == null || !"https".equals(uri.getScheme())) { return null; } + if (!Uri.parse(ORIGIN).getHost().equals(uri.getHost())) { return null; } + + String path = uri.getPath(); + if (path == null || path.equals("/")) { path = "/index.html"; } + String asset = path.startsWith("/") ? path.substring(1) : path; + + // No traversal out of assets/. + if (asset.contains("..")) { return null; } + + try { + InputStream in = getAssets().open(asset); + Map headers = new HashMap<>(); + headers.put("Cache-Control", "no-cache"); + return new WebResourceResponse(mime(asset), "utf-8", 200, "OK", headers, in); + } catch (IOException e) { + return new WebResourceResponse("text/plain", "utf-8", 404, "Not Found", + new HashMap(), null); + } + } + + private static String mime(String name) { + if (name.endsWith(".html")) { return "text/html"; } + if (name.endsWith(".js")) { return "text/javascript"; } + if (name.endsWith(".css")) { return "text/css"; } + if (name.endsWith(".png")) { return "image/png"; } + if (name.endsWith(".webmanifest") || name.endsWith(".json")) { return "application/manifest+json"; } + return "application/octet-stream"; + } + + private void requestLocation() { + if (Build.VERSION.SDK_INT >= 23 + && checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION }, 1); + } + } + + private void immersive() { + View d = getWindow().getDecorView(); + d.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + + @Override + public void onWindowFocusChanged(boolean focused) { + super.onWindowFocusChanged(focused); + if (focused) { immersive(); } + } + + @Override + public void onBackPressed() { + if (web != null && web.canGoBack()) { web.goBack(); } else { super.onBackPressed(); } + } +}