e3917e6 hid any row whose key failed its seeding read, which also hid
"Link remote controller": an action key has no value to read. Unreadable is
now tracked apart from supported, and only rows that show a value hide on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stopping a stream to an RTMP receiver crashed the app with SIGSEGV inside
LiveStreamManager.stopStream (Mavic Pro, 2026-10-22). stopStream returns at
once and frees the native streamer on an SDK thread, while the bridge kept
reading bitrate/fps/time through native getters from its poll and right
after the stop. The poll now ends first, nothing reads those getters while
stopping, and the bridge waits for the SDK to report the stream stopped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Mavic Pro simulator closes by itself every 3-11 minutes and logcat on the
dev tablet rolls over before it can be read. SimLog appends the simulator's
on/off changes (with the flight state at that moment), the app's own start
and stop commands, and every diagnostics change to files/sim_log.txt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Mavic Pro remote reports GIMBAL_CONTROL_SPEED_COEFFICIENT as supported but
fails every read, so "Gimbal dial speed" sat on "—" forever. The key bridge
now reports a failed seeding read, and the row hides like an unsupported one
until a value does arrive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the tablet in landscape the keyboard leaves ~200 dp, and the save-route
dialog squeezed its text field to a line. The route-name, stream-address and
settings number/text dialogs now scroll instead and use smaller margins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
backup_pocketbase_app/ held the pre-API-Server login and PocketBase service
from June; nothing uses it, and its imports made `flutter analyze` report 20
errors. It stays in git history.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the Mavic Pro simulator run on 2026-10-22:
- The tablet's 2-minute sleep blanked the flight screen mid-flight. The
flight screen now sets FLAG_KEEP_SCREEN_ON while it is open.
- TapFly's GO got no answer from the aircraft and no error, so the button
looked dead. ActiveTrack/TapFly starts now give up after 15 s with "the
aircraft did not answer".
- Tripod works in the air but the flight controller ignores it on the
ground, so the gate asks to take off first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the Mavic Pro the TRIPOD_MODE_ENABLED write succeeds but the flight
controller keeps Tripod off, and the panel showed "Tripod on" for a moment
before silently flipping back. The gate now reads the key back after the write
and refuses with "The aircraft kept Tripod off." when it did not change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the Mavic Pro the flight controller refused Course Lock -> Home Lock with
"There is a conflicting setting in the mission"; from normal heading the same
switch works, so the gate now goes through normal first. The mode chip also
stops repeating the lock when the aircraft already names it as the mode
("CL · CL" -> "CL").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the Mavic Pro in the simulator a waypoint mission failed with "Execution
of this process has timed out". The bridge now resends a timed-out upload
with retryUploadMission (up to 3 times) and only reports the upload done once
the operator is READY_TO_EXECUTE, and the error now says whether the upload
or the start failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous fix silenced tile errors, which made flutter_map count a failed
tile as a loaded transparent one, so it was never fetched again. Errors now
reach the layer, and OsmTileLayer rebuilds itself a few seconds after a
failure: loaded tiles come back from the image cache and only the failed ones
are requested again. Seen on the tablet, whose 2.4 GHz Wi-Fi loses a fifth of
its packets next to the OcuSync remote.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Leaving the Album put the camera in video mode even when it had been in
photo mode: it restored FlightModel.captureMode, which only follows the HUD
switch. It now restores the mode the camera reported before it was taken.
- Playing a video on the aircraft drew the player over the whole HUD. The
Album now sets FlightModel.hudHidden, and the flight screen shows only the
feed, full-screen, while it is set (the HUD stays mounted, offstage).
- Map tiles that failed once stayed as dark squares. Every map now uses one
OpenStreetMap provider whose client also retries dropped connections, and
failed tiles are fetched again when panned back into view.
- Find My Drone saved simulated positions as the aircraft's last fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MainActivity now makes the app's only start-up permission request and tells
Dart when it finishes; the phone-location stream waits for that instead of
asking in parallel, which made Android drop the location grant.
The P3 simulator run passed, so the pre-P2 FlightControlPage, the pages only
it used and its Settings toggle are gone. The app smoke test checks the home
screen again instead of the login gate removed in July.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Virtual sticks come from the product matrix: the Mavic Pro answers
isVirtualStickControlModeAvailable = false even in flight, which hid
the STICKS button.
- While landing, the rail's second slot belongs to the landing (the
Mavic Pro keeps isGoingHome set through a plain Land, so it showed
Cancel RTH beside Confirm landing); Cancel landing is allowed at the
confirmation.
- Rail buttons have a fixed width so long labels wrap instead of
pushing the rail onto the gimbal slider.
- Outlined HUD buttons get a transparent background (the app theme
filled them white behind white text).
Verified in the simulator: take-off, RTH, cancel RTH, land, cancel and
confirm landing, and the virtual-stick axes (roll = forward, pitch = right).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DJI SDK can drop a registration attempt without ever calling
onRegister (it logged "Exception when executing encrypt/decrypt
functions" and nothing more), which left the app on "Registering…" with
no way out but a restart. The bridge now gives up after 60 s and reports
a failure that names the likely causes; a late answer from the SDK still
replaces it.
The failure was also only visible in the settings sheet, which the home
screen's Connect aircraft flow never opens, so failures now show a
snackbar too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Intelligent modes: Point of Interest (orbit on the map, live radius and
speed), Follow Me (follows the phone), QuickShots on a boxed subject,
Course Lock / Home Lock, Tripod and Cinematic, a Free hyperlapse, and
Waypoints 2.0 for V2-only aircraft. ActiveTrack gains Spotlight Pro and
Circle. Every mode has a first-use tutorial and stops from the rail.
Waypoints can resume an interrupted mission, change cruise speed in
flight and check terrain clearance (Open-Meteo, on tap).
Settings: units, C1/C2 actions, dual-operator master/slave, HDMI/SDI
output, a link spectrum chart, Wi-Fi name and password, aircraft name,
video cache, battery warning history and pairing, non-smart cell
voltages. Calibration: compass interference meter, per-IMU calibration,
lens infinity focus and a first-manual-focus prompt. Also an RTK chip,
gimbal movement presets and motor tuning, Precision Takeoff gated on the
flight controller, and the precise GEO database's server version.
Built and unit/widget-tested only; nothing has flown yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Playback: the flight screen's playback button opens the album over the
live view, so a video tapped there plays on the aircraft into the feed
with a transport bar. Long press selects files to download or delete
(delete asks first); one file opens in a viewer with the camera's
preview. Leaving the album puts the camera back in its capture mode.
Flight records: every flight is recorded from take-off to landing (1 Hz
track, max height / distance / speed, distance flown, battery used) and
kept on the phone. When it lands while signed in, it is uploaded to the
PilotVault logbook through the existing /api/drones/auto and
/api/flights endpoints as an automatic-path entry with the track as
GeoJSON. Simulator flights are never uploaded; flights recorded while
signed out retry on sign-in. Flight logs lists them with their upload
state and a track map, and the profile stats come from real flights.
Find My Drone: the aircraft's last fix is kept across lost links and
restarts, and shown against the phone's own GPS with distance, direction
and a hand-off to the maps app.
Live streaming: a new LiveStreamBridge streams the main camera to an
RTMP address set in Settings > General, with a LIVE pill on the flight
screen that turns amber when the uplink falls behind and stops on tap.
Pre-flight checklist (manual checks plus the aircraft's live status)
and an in-app Academy with six short guides that work offline.
None of this has run on hardware yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vision radar: graded obstacle sectors around the centre of the video,
the nearest distance per side, the obstacle overhead, and BLOCKED on a
side whose sensors cannot see once the motors run. On aircraft with 360°
sensing, sides without their own readings are filled from the
perception ring. The same sectors sit on the attitude ball's rim, turned
by the heading. Settings > Vision gains Display radar chart and the
braking / sensing distances per direction.
On-screen sticks: the rail's STICKS button hands the aircraft to two
stick pads after a slide-to-confirm. The 25 Hz stream runs natively and
sends zeros once Dart's last command is 300 ms old, so a stalled UI
leaves the aircraft hovering. The sticks close when the remote takes
over, on RTH or on landing. They are simulator-only on aircraft with a
remote unless Settings > General > Developer allows real flights; Spark
and Mavic Air on Wi-Fi may fly them for real. Which MSDK axis is forward
in velocity + body mode is coded from DJI's documentation and must be
checked in the simulator first.
AirSense: a banner for manned aircraft at warning level 2 and up, with
distance, direction and ICAO code; the planes on the map; the full list
in a sheet and Settings > Safety; and the aircraft's own avoidance
action with its countdown.
Fly zones: DJI GEO zones around the aircraft (or the phone) on the map
with their height limits, a banner for FlySafe notices and in/near-zone
states, take-off refused in restricted and locked authorization zones,
self-unlock of authorization zones, the aircraft's licences (enable,
sync from the DJI account, remove all; ground only), and the GEO
database version with an update button.
Tested with unit and widget tests and screenshot renders only; nothing
has run on an aircraft or the simulator yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Routes saved in the Fly App's mission library now sync to a new per-user
routes collection on the API Server, in the WaypointMissionSpec JSON
format. The library pulls on load and pushes on every save and delete,
merges last-writer-wins by updatedAt, keeps unacknowledged deletes as
tombstones, and keeps working offline. Each signed-in account has its
own copy on the device; routes saved while signed out go to the first
account that signs in.
The API Server gets list / get / upsert / delete route endpoints with a
PocketBase migration, and the Web App a read-only Routes view on the
dashboard (rebuilt dist).
Tested against a fake PocketBase and a fake server only; not yet tried
with a real sign-in on the tablet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auto-detect: on aircraft that can sense subjects themselves, the
ActiveTrack panel offers Auto-detect; the subjects it finds show as dashed
boxes on the video, and tapping one then GO follows it by its index.
Trace gets a Backward switch (fly ahead of a subject walking towards the
aircraft, obstacle-sensing aircraft only) and TapFly Free a Reset heading
button. Settings > Vision gains Gesture mode and Palm control where the
flight assistant supports them, and the HUD shows when palm control is
flying the aircraft.
The live view used to stretch the feed over the screen. It now fits the
picture to the decoder's own size with black bars, and reports that size,
so the ActiveTrack / TapFly overlays, tap-to-focus / meter and the grid
all map into the picture and ignore taps on the bars.
Not yet run on a simulator or aircraft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
P7b of the GO 4-parity plan. Both modes open from the MODES picker on the
live video, where they take over the touches the camera and gimbal used.
ActiveTrack: drag a box round the subject (or tap it), then GO. The
operator's own box follows on the video, coloured by confidence, with
confirm / reject when it asks and the reason when it cannot follow.
Trace, Profile and Spotlight switch live from the mode panel.
TapFly: tap where to fly, then GO; the aircraft's heading point and any
obstacle bypass show while it flies, and the speed changes live. Forward
only on aircraft without rear sensors; Backward and Free otherwise.
Both go through new SafetyGate checks (flying, P mode, above 2 m, nothing
else in control, battery), start on GO when the check is clean and ask for
the slide when it warns. Stop on the rail and Exit stop them. MissionBridge
now listens to the ActiveTrack and TapFly operators. Not yet run on a
simulator or aircraft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
P7a of the GO 4-parity plan. The rail's GRID button becomes MODES, which
opens a picker of the product's intelligent flight modes with first-use
terms; Waypoints is the first one built.
Waypoints opens on the big map: tap to add waypoints or POIs, long-press
to drag, "+" to insert on a leg, and a point editor for altitude, speed,
heading (route / custom / POI), gimbal (set or aim at POI), camera action
and hover, and turn radius, with a swipe-to-adjust readout. Route settings,
a local mission library and undo sit in a tool column. Start runs the
SafetyGate's pre-start check in the slide-to-confirm dialog, then uploads
and starts; a run panel shows upload and flight progress with Pause,
Resume and Stop.
MissionBridge now listens to the waypoint operator and builds the full V1
mission (per-waypoint headings, turn direction, gimbal pitch, actions).
Not yet run on a simulator or aircraft.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
P6b: a new CalibrationBridge owns every calibration call and publishes
a `calibration` telemetry group (IMU per-unit and per-side progress,
gravity centre, lens, sensor cleaning, RC calibration state and live
sticks). Guided full-screen wizards cover compass, IMU, remote
controller, gimbal auto calibration, lens, sensor cleaning, centre of
gravity, propellers and a vision-sensor guide; gimbal adjust is a
horizon-line overlay on the live video. Every start goes through
SafetyGate.runMaintenance, and closing mid-run stops what the SDK can
stop.
Reachable from Settings > Safety > Calibration & maintenance, the
Aircraft Status calibrate buttons and the gimbal sheet. Not yet run on
an aircraft; the RC key sequence still needs checking per remote.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "•••" button now opens a right-side glass sheet over the live view
with Safety, Vision, Remote, Video link, Battery, Gimbal and General
tabs. Each row is a one-line spec bound to a KeyManager key: rows hide
when the aircraft does not support their key, writes show at once and
roll back if refused, and ground-only settings lock in flight.
KeyBridge gains argument-less action keys and converts range and LED
settings values. settings_menu_page.dart is retired.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plan P5: a value bubble on the pitch slider while dragging, press-and-drag
on the video as a speed joystick (re-sent at 10 Hz, zero on release), and a
gimbal settings sheet: mode, 30° extension, yaw follow, recenter / face down /
yaw recenter, roll fine-tune, auto calibration and advanced speed/smoothness
rows gated by the gimbal's capability ranges.
GimbalBridge reports mode, trim, calibration and stop/overload state in the
gimbal telemetry group, and getter-only settings in a new gimbalSettings slow
group. The reported-plus-pending overlay moves out of CameraState into a
SettingsOverlay mixin shared by camera and gimbal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One settings table in CameraBridge watches every camera value into the
telemetry tick and backs a generic setCameraValue, so the tabbed camera
menu (Exposure / Photo / Video / Other) is built from row specs and the
camera's own capability ranges. Writes show at once and fall back to what
the camera reports. Adds the quick exposure panel (Auto/A/S/M, ISO slider,
shutter and aperture wheels, EV strip, metered EV in M), a new camera
column with record timer and interval/pano progress, a zoom column for
zoom cameras, tap-to-focus / tap-to-meter, and a live histogram.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take-off, land, RTH, the landing cancel/confirm and Stop now all go
through one SafetyGate (lib/fly/safety_gate.dart). The gate sorts its
preconditions into blockers and warnings: connection, flight state,
battery against the thresholds, IMU warm-up, compass, critical
diagnostics, GPS, home point, SD card and the RTH battery budget. It
checks again right before sending.
The left rail is now a state machine (lib/fly/action_rail_state.dart):
Take-off becomes Land, then Cancel or Confirm landing. RTH becomes
Cancel RTH. A Stop button shows while an intelligent mode is flying.
Moving the aircraft needs a slide-to-confirm dialog, which can offer
Precision Takeoff or set the RTH altitude. A blocked command shows why
and has no slider. When the pilot is more than 30 m from home, RTH asks
whether to return to them or to the original home point.
The HUD gains an error stack. Its cards can be dismissed, it fits the
space it has, and a new warning triggers haptic feedback. It also gets a
smart-RTH countdown banner with Cancel / Go home now, a forced-landing
banner, and an Aircraft Status sheet. The sheet opens from the status
chip and appears before each connection's first take-off. It offers
Calibrate compass/IMU and Format SD.
On the bridge side this adds precisionTakeOff, setHomeLocation, compass
and IMU calibration start/stop, formatStorage, and compass health in the
telemetry tick.
The classic flight screen stays available until the simulator run passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new flight screen (lib/ui/fly/fly_screen.dart) is built from layers.
Each readout listens to its own telemetry sub-model, so a 10 Hz tick only
repaints what changed. It adds a status bar with a status chip and tip
marquee, a smart battery bar with land/RTH markers, a camera info strip,
an attitude ball, an H/D/VS/HS/VPS strip, and a minimap that swaps places
with the video when tapped. The HUD is laid out in design pixels and
scaled to phone or tablet inside the safe area.
What the HUD shows depends on the product: dual or multi battery bars,
SSD status, a thermal spot reading, a master/slave RC badge, and Wi-Fi
bars in place of RC/HD. The bridges now report the data for these: the
camera's ssd/thermal groups and the RC's master/slave mode.
The old FlightControlPage can still be opened from Settings > About >
Classic flight screen until the P3 safety flows land. The same tab also
gets a toggle for the performance overlay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GO 4-style HUD needs far more live data than the bridge sent, and
pushing every SDK callback straight to Dart would rebuild the screen
dozens of times a second. This is the telemetry foundation (plan P1).
On the Kotlin side, every source now writes into a TelemetryHub that
emits one telemetry event per tick: flight-controller state, gimbal and
vision at up to 10 Hz, battery, links, RC, storage and camera settings
folded in at up to 2 Hz. Each event carries the full latest snapshot, so
a consumer that drops packets never loses a slow group. New bridges
cover batteries (every pack, cell voltages, warning thresholds), link
quality and RC battery, diagnostics, and obstacle sensing per sensor.
KeyBridge adds generic getKey/setKey/listenKey over KeyManager for the
settings rows to come. The flight controller also gains smart-RTH
confirmation and simulator start/stop.
On the Dart side, FlightModel now owns one ChangeNotifier per subsystem
(aircraft, battery, links, camera, gimbal, vision, alerts, mission), so
a HUD widget can listen to just the one it draws. TelemetryReducer
routes events into them, only notifies groups that changed, and gives
FlightModel one notification per tick. Flat getters keep the existing
screens working. Diagnostics become graded, de-duplicated alerts.
Settings > About > Telemetry inspector shows every field live and can
start the simulator.
Battery no longer arrives as its own event, so the API Server reads the
percentage from the telemetry snapshot (the old event is still accepted).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A folder name with a space has to be quoted in every cd, docker build -f
and compose path, and URL-encoded in README links. The compose file's
dockerfile path, the build examples and the README now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GO 4-parity HUD has to work on every aircraft MSDK V4 drives, so no
screen may assume one model. This adds the capability layer the rest of
the plan builds on (P0).
ProductProfileBridge reads what the SDK can report at runtime (cameras
and their ISO/shutter/aperture/EV ranges, gimbal axes, air link, FC and
RC feature flags, batteries) and emits it as a productProfile event on
connect and component change. Capability ranges stay empty until the
camera handshakes, so it re-probes at 3, 10 and 30 s and emits only
when something changed.
On the Dart side, ProductProfile merges that with a static
product_matrix keyed by the MSDK Model enum name, for facts the SDK
cannot report: vision sensor layout, battery bay count, intelligent
modes. SDK answers win and every fact records its source. HudFeatures
lists each capability-gated element with its predicate, and Requires
hides a widget the profile rules out. The flight page now gates its
gimbal slider, camera column and vision pill, labels the link Wi-Fi or
HD, and takes the gimbal pitch range from the profile.
Settings > About > Capability profile shows the active profile, fact
sources and which features are on. Matrix rows are seeded from published
specs until runtime capture confirms them; nothing has run on a real
aircraft yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Fly App's DJI App Key moved out of android/gradle.properties and into the
developer's ~/.gradle/gradle.properties in 5865553, but the root README's setup
steps still sent people to the old location — where a key would now be both
ignored by the build and committed to git, which is exactly the pair of
outcomes that change existed to prevent.
Audited the rest of the docs and the container stack while here and changed
nothing else: no other README names the key or the old package, and the Fly App
appears across all the Docker files only as a .dockerignore exclusion, which is
correct — it is a Flutter APK built on the developer's machine, not a service in
the stack.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app still shipped under the identity of the DJI MSDK Flutter sample it
was started from, so every install, log line and crash report named a DJI
sample rather than PilotVault. Rename both namespaces:
Android com.dji.flutter.dji_msdk_sample -> com.pilotvault.flyapp
Dart dji_msdk_sample -> pilotvault_fly
The Kotlin sources move under com/pilotvault/flyapp to match. The manifest's
meta-data name stays com.dji.sdk.API_KEY — that string is fixed by the SDK's
own lookup and is not ours to rename.
A DJI App Key is bound to the application id, so the old key died with the
old id and a new one was registered against com.pilotvault.flyapp. That
forced the key to be touched anyway, so stop committing it: the Gradle
property becomes PILOTVAULT_FLY_API_KEY and now lives in the developer's
~/.gradle/gradle.properties, which Gradle merges into every build.
android/gradle.properties keeps the build flags — gitignoring it wholesale
would have taken useAndroidX and the Flutter migrator flags out of version
control with it — and documents where the key belongs.
A missing key prints a banner rather than silently baking the placeholder
into an APK that cannot register, which otherwise only surfaces as a
registration failure once the tablet is out at the aircraft. That warning
uses println because `flutter build` filters Gradle's warn-level output.
The rename means Android treats this as a new app: uninstall
com.dji.flutter.dji_msdk_sample before installing, or both will sit on the
tablet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Docker builds from the working tree, so a CRLF checkout of entrypoint.sh (from
core.autocrlf) would break the shebang inside Alpine and inject \r into the AIO
Dockerfile's heredoc-generated supervisord.conf. Force LF on shell scripts and
Dockerfiles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The combined stack now includes PocketBase as a first-class service instead
of assuming an external one: a Docker/pocketbase image built from the release
binary that bundles the pb_migrations and upserts the superuser on boot, so a
fresh stack checks/creates its collections and super-admin the same way
DriverVault does. Add production compose variants (registry images) and
.env(.prod).example files for both Docker/ and Docker AIO/, a top-level compose
name, a PocketBase healthcheck with a service_healthy dependency, and
env-driven ports. Move the containerized PocketBase to :8070 (the live remote
PB stays 10.2.1.10:8026). Add a root .dockerignore to trim the repo-root build
context used by the pocketbase and AIO images.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Overview's tables documented 31 routes where the server serves 53. Everything
added since the plugin cascade went in was simply absent: the logbook, the fleet,
documents, and the integrations surface itself.
Cross-checked against every mux.HandleFunc in server.go, so the only routes left
unlisted are GET /assets/ and GET /favicon.svg — the panel serving itself.
Two judgement calls worth naming:
The integrations triplet is written once as /api/integrations/{plugin} with the
five plugin names in the description, rather than seventeen near-identical rows.
The routes are registered individually, not by wildcard, so that path is an
abstraction the server would not match — flagged in a comment above the list.
/api/health and /api/status are public and belong to no audience in particular.
They sit beside /healthz, which was already in the Device table, retitled
"Device uplink · public" rather than mint a table for three rows.
Logbook/fleet and documents each get their own table, mirroring how the Web App's
nav splits them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getSerialNumber() is a BaseComponent method, so every component answers for
itself — and the bridge reads it off the flight controller. A Mavic Pro reports
08RDE1J00103H1 (what DJI Go labels "Flight Controller SN") where the airframe
sticker, and the registration, say 08QDE3H012032E. We were publishing the former
as the drone's serial, onto records that exist to satisfy BEK 1649 §5.
Same trap as 002e484, where a component's own firmware stood in for the
aircraft's, but with no correct source to switch to: MSDK v4 exposes no
aircraft-level serial at all — BaseProduct offers only the model and the
firmware package version — so the registered serial can only be typed by hand.
So split the two rather than pick one:
serial the airframe's, hand-entered, and the only one that
reaches the logbook and the CSV export
flight_controller_serial what the aircraft reports; auto-filled on connect,
and what POST /api/drones/auto now upserts on
Keying auto-add on the flight controller's serial keeps the fleet recognising a
connected drone without typing — it is stable per airframe — while leaving the
compliance record's serial to the pilot. A flight controller swapped in a repair
now costs a duplicate fleet entry to merge, where before it would have quietly
rewritten what the logbook claimed the drone was.
Note droneInput.payload() is a whole-record write, so any UI editing a drone must
round-trip flightControllerSerial; blanking it forks the drone into a duplicate
on its next connect. Drones.vue carries it through the edit form for that reason.
The migration copies existing serials into flight_controller_serial rather than
moving them: every current value came from auto-add and is therefore a flight
controller's, but a pilot may since have corrected one by hand and this cannot
tell them apart. Copying keeps auto-add matching the airframes it matched before.
Applied to the remote PocketBase, where drones held no records, so the backfill
was a no-op there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fleet lived as a tab inside the Logbook, which buried it, and every
drone had to be typed in by hand — model, serial and firmware copied off
an airframe the app was already talking to.
Promote it to its own nav section above Logbook, and let a connecting
drone register itself. The Fly App already forwarded model, serial and
firmware upstream; the hub was keeping only the model. It now carries the
identity through to DeviceState, and the Web App offers it to a new
POST /api/drones/auto, which upserts keyed by serial. The auto path only
writes what the aircraft is authoritative about (model, both firmware
versions) and never touches what the pilot curates.
Serial and the firmware versions resolve on their own schedules after
connect — the serial in seconds, the aircraft firmware sometimes a minute
later — so nothing along the path treats an absent value as a cleared one,
and a later event filling firmware in still reaches the server. The auto
call rides every telemetry frame, so the client remembers the identity
tuple it last sent and only a change goes out; a 4xx is the server's
settled answer and is not retried, or one drone connected for an hour
would mean one request per frame for an hour.
New fields on drones: firmware, controller_firmware, and registration for
the FAA/CAA aircraft number — distinct from operator_number, which stays
the EU operator ID. Controller firmware is the remote controller's own
version, read from its component; the flight controller's version is a
different quantity and stays off this field (see 002e484). name becomes
optional and is now the pilot's custom name: auto-added drones arrive
unnamed, so the API serves a computed displayName (name, else model +
serial) for the fleet table, the flight picker and the CSV export. A
unique index on serial is what keeps the find-then-create path from
forking a drone's history across two records.
The schema is applied to the remote PocketBase; the migration is here for
fresh deployments, which the remote does not read.
Verified against a simulated device over the real socket with identity
resolving late: one record from four events, both firmware versions
filled, curated fields intact across re-registration, and a drone deleted
while connected coming back on the next frame.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Album showed durations like -36:35, -136:23 and 545:26 on a Mavic
Pro's photos. MediaItem.durationLabel only checked for null, but the
SDK fills durationInSeconds for every file, so stills carry junk that
was formatted straight into a badge.
Require isVideo and a positive duration. Stills now get no badge and
nonsense values are rejected; real clips are unaffected. Display-only —
durationLabel renders in one place and nothing else reads it.
Pre-existing, unrelated to the identity work; spotted while testing the
Album against a real aircraft.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The About panel and home card showed 01.03.0800 on a Mavic Pro whose
firmware is 03.02.35.05, sometimes flipping from the correct value to
the wrong one seconds after connect.
Cause was the component-level fallback I added in ec4b1bc. It is not a
slower route to the same value: BaseComponent.getFirmwareVersion()
reports the component's OWN firmware, so the flight controller answers
01.03.0800 while the aircraft is 03.02.35.05. Because it resolves
quickly and getFirmwarePackageVersion() stays null for far longer than
assumed, the fallback consistently won the race, published a wrong
version, and — since the poll stopped once firmware was non-null — ended
the search for the real one.
Remove it. getFirmwarePackageVersion() is the only source for this
field, so the value can now only be the aircraft's or absent, and the
wrong version has no source in the codebase at all. The field stays
blank until the package version is readable, which is the pre-existing
behaviour and strictly better than showing something wrong.
Widen the poll window to 60s as a best-effort head start now that
nothing fills the gap early. It is not a guarantee: a Mavic Pro was
still null after a minute, and whenever the window expires first the
next connection event carries the version through connectionMap, as it
did before this feature existed.
Found by testing against a real Mavic Pro; two earlier attempts to fix
this by re-ordering the fallback were both disproved on the aircraft.
The serial number this feature added is verified working (08RDE1J00103H1).
This fix is not yet confirmed on hardware — the controller needed charging.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Firmware was already half-wired: the bridge read
BaseProduct.getFirmwarePackageVersion() once, at connect. The SDK
returns null there until it has finished handshaking with the
aircraft, so the About panel almost always showed "-" instead. The
serial was never fetched at all.
Resolve both asynchronously after connect. fetchIdentity() reads the
serial from FlightController.getSerialNumber() and the firmware from
the product package version, falling back to the component-level
getFirmwareVersion() for aircraft that only report the latter. It
re-checks every 2s (max 6 attempts) and emits each value on a new
`identity` event as it lands, so the serial still appears when
firmware never resolves. Values are cached to answer getProductInfo
without re-fetching, and cleared on disconnect.
The SDK delivers lifecycle callbacks on arbitrary threads, so all
identity mutation hops onto the main thread, guarded by a generation
counter that strands retries queued for a product that has since
changed or dropped - otherwise a reconnect could race a stale chain
and report the previous aircraft.
Verified via flutter analyze and an APK build (which type-checks the
new MSDK calls). Runtime timing and the reported values still need a
physical aircraft.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dji-sdk-classes.html reference is a generated artifact, not source,
so exclude the root-level copy from version control.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refresh the Vite build embedded in the Web App server (new hashed
asset filenames + index.html references) after rebuilding the frontend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuild the Flutter/DJI-MSDK-V4 Fly App to the v2 UI kit and wire the
full SDK surface behind it.
Native (Kotlin): split DjiSdkBridge into a method/event router delegating
to per-subsystem SubBridge helpers sharing a BridgeCtx — FlightController
(takeoff/land/RTH + rich telemetry), Camera (mode/record/photo/exposure),
Gimbal, Mission (Waypoint + ActiveTrack; QuickShots via ActiveTrack
QUICK_SHOT), Media (MediaManager list/thumbnail/download), and optional
DJI account login. Manifest gains scoped media permissions.
Flutter: ten screens under lib/ui/ (Flight HUD, capture modes, camera
settings, settings menu, map+waypoints, home, album, academy, profile,
routes/flight logs), driven by an expanded FlightModel. New PVIcon renders
the kit's SVG paths via flutter_svg; map uses flutter_map + latlong2.
Pin transitive androidx.core/browser down to SDK-35-compatible versions
so the newer plugins don't force AGP 8.9.1 onto the DJI toolchain.
Verified with `flutter build apk --debug` (compiles Dart + all Kotlin);
runtime behaviour is untested here — it needs a physical DJI-connected
device.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the descriptor Default of 60 so the global panel field can be left
blank. A non-blank global value sits at the top of the cascade and locks
every lower layer, so the fixed default made the per-minute limit
un-overridable by organizations and users. Blank now means "inherit";
the runtime still falls back to 60 when no layer sets a value, so the
usage gauge is unchanged. Update the descriptor help and the Settings
row copy to explain the inherit behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a weather card to the Web App Overview, wired like the OpenSky live-map
overlay and backed by the OpenWeather plugin.
- API Server: GET /api/integrations/openweather/current resolves the
caller's cascade, gates it (master/org/personal opt-in/key), and returns
trimmed current conditions for the configured or a supplied ?lat=&lon=
point; off/keyless returns {unavailable, detail} so the card degrades.
- BFF relay (forwards the location override) + route; api.js client.
- Dashboard: a Weather card stacked above Schedule showing an emoji
condition, temperature in the resolved unit, description, location, and a
feels-like/wind/humidity/cloud grid. Location follows the same cascade as
the map (drone -> phone -> browser -> default) without prompting for geo;
refreshes every 10 min while on Overview.
- validLatLon test; rebuilt embedded frontend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The panel health probe tests the global plugin config only. When the
plugin is enabled as a master switch with no global API key (users supply
their own), the probe reported a hard "down", which read as broken.
Return "degraded" with an explanatory detail instead — a keyless master
switch is a valid setup, nothing is failing. The Web App user-facing path
is unchanged: a caller whose effective key is empty still gets "down" with
an actionable "add one to connect" message before the plugin is called.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OpenWeather reports no remaining quota (only 429 when over), so add an
in-app gauge that counts the calls PilotVault itself makes and shows them
against the plan's per-minute limit.
- Plugin: process-wide usage store bucketed per API key (hashed) into the
current minute and UTC day; a do() wrapper counts each request that
reaches OpenWeather (transport errors consume no quota, so uncounted).
Health and Invoke both route through it.
- New callsPerMinute config field (default 60) that cascades global -> org
-> user; plugin contract gains plugins.HealthUsage on Health.
- Web App: a "Calls per minute limit" field and an "API call usage" meter
in the OpenWeather card, shown after Test connection and cached.
- Counts reset on restart and cover only PilotVault's own calls (noted in
the UI); the account dashboard stays authoritative.
- Tests for per-key counting and minute/day rollover; rebuilt frontend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Units field carried a Default of "metric" and had no blank option, so
the global panel layer always set a value and locked organizations and
users out of changing it. Mirror the fix already applied to OpenSky's plan.
- Plugin descriptor: drop the "metric" Default and add a blank "Not set"
option; runtime still falls back to metric when no layer sets it.
- Web App: add a "Not set" choice to the Units control and stop forcing
metric back into the form on load.
- Test guards that the units field has no Default and offers a blank option.
- Rebuilt embedded frontend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builtin API Server connector for the OpenWeather API (current weather +
5-day forecast for a point, API-key auth, stdlib-only), surfaced to end
users through the same three-layer settings cascade as OpenSky/WebDAV.
- New builtin plugin (internal/plugins/builtin/openweather) with
health probe, weather.current/forecast.5day capabilities, and tests.
- resolveOpenWeather cascade (global -> org -> user) with per-field
independent resolution and API-key masking; GET/PUT/health endpoints.
- Web App BFF relays, api.js client, and a Settings card under
APIs - External (scope switch, enable toggles, test connection).
- Resolver unit tests + rebuilt embedded frontend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>