Commit Graph
71 Commits
Author SHA1 Message Date
tajniak81andClaude Opus 5.5 bc41d8b128 Split the automatic logging path by the app that logged the flight
logging_path is now manual, automatic_flyapp or automatic_flyagentapp. The
Fly App's entries are backed by the aircraft's flight recorder and keep the
missing-FDR red flag; the Fly Agent App's come from Remote ID, which is not a
flight recorder, so they must carry the § 5 fields like a manual entry. The API
maps the old "automatic" onto automatic_flyapp so Fly App builds already on
devices keep filing. The migration has been applied to the remote PocketBase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:36:16 +02:00
tajniak81andClaude Opus 5.5 3dd31b257a Add the Fly Agent App, a Remote ID companion to DJI Fly
A Flutter app that runs next to DJI Fly without the DJI SDK. It receives the
aircraft's Remote ID (Bluetooth, BT5, Wi-Fi NaN and Beacon) in a foreground
service, streams the paired aircraft to the hub over /ws/device, and turns
take-off to landing into a logbook entry filed through /api/flights, queued
when offline. Covers phases A0-A3 of the Fly Agent plan; needs no server change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:25:52 +02:00
tajniak81andClaude Opus 5.5 415c6184b1 Keep action rows when their key cannot be read
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>
2026-10-22 15:16:04 +02:00
tajniak81andClaude Opus 5.5 128220dcb7 Stop reading live-stream stats while the SDK tears the stream down
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>
2026-10-22 15:12:43 +02:00
tajniak81andClaude Opus 5.5 727850bb0e Log simulator starts, stops and diagnostics to a file on the tablet
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>
2026-10-22 14:56:49 +02:00
tajniak81andClaude Opus 5.5 e3917e6f25 Hide settings rows whose key the aircraft cannot read
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>
2026-10-22 14:51:14 +02:00
tajniak81andClaude Opus 5.5 a813648b75 Stop text dialogs collapsing their field above the landscape keyboard
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>
2026-10-22 14:49:26 +02:00
tajniak81andClaude Opus 5.5 66c3dbcd71 Remove the old PocketBase backup from the Fly App
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>
2026-10-22 14:43:51 +02:00
tajniak81andClaude Opus 5.5 8f259339fc Keep the screen on in flight, time out silent vision starts, gate Tripod on the ground
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>
2026-10-22 10:38:07 +02:00
tajniak81andClaude Opus 5.5 a7984d7f28 Report a Tripod or Cinematic switch the aircraft did not take
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>
2026-10-22 09:58:30 +02:00
tajniak81andClaude Opus 5.5 789818679a Switch between Course Lock and Home Lock through normal heading
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>
2026-10-22 09:48:07 +02:00
tajniak81andClaude Opus 5.5 9cca65019f Retry a timed-out waypoint upload and wait for the route to be ready
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>
2026-10-22 09:25:28 +02:00
tajniak81andClaude Opus 5.5 91520560a7 Retry map tiles that failed while they are still on screen
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>
2026-10-21 21:35:19 +02:00
tajniak81andClaude Opus 5.5 cfab444c65 Fix what the first secondary-screen run on the Mavic Pro turned up
- 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>
2026-10-21 21:13:24 +02:00
tajniak81andClaude Opus 5.5 2db557a3b4 Retire the classic flight screen and stop the start-up location grant being dropped
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>
2026-10-21 20:36:17 +02:00
tajniak81andClaude Opus 5.5 63acacced7 Fix what the first Fly App simulator runs on a Mavic Pro turned up
- 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>
2026-10-21 20:27:05 +02:00
tajniak81andClaude Opus 5.5 56d43ddb1e Stop the Fly App hanging forever on DJI registration
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>
2026-10-21 17:39:39 +02:00
tajniak81andClaude Opus 5.5 636da1ac2c Give the Fly App every remaining GO 4 flight mode and the missing settings
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>
2026-10-20 23:06:16 +02:00
tajniak81andClaude Opus 5.5 91ca150a35 Give the Fly App GO 4-style playback, flight records, Find My Drone and live streaming
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>
2026-10-20 22:07:10 +02:00
tajniak81andClaude Opus 5.5 e02bb0e5cd Give the Fly App GO 4-style obstacle radar, on-screen sticks, AirSense and fly zones
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>
2026-10-20 21:14:12 +02:00
tajniak81andClaude Opus 5.5 93f4a580e9 Sync the Fly App's waypoint routes through the API Server
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>
2026-10-20 17:26:45 +02:00
tajniak81andClaude Opus 5.5 e49f318a0e Finish the Fly App's ActiveTrack and TapFly extras and letterbox the feed
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>
2026-10-20 17:23:37 +02:00
tajniak81andClaude Opus 5.5 2c908cf17e Give the Fly App GO 4-style ActiveTrack and TapFly
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>
2026-10-20 17:11:09 +02:00
tajniak81andClaude Opus 5.5 43a66df8df Give the Fly App GO 4-style intelligent modes and a Waypoints editor
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>
2026-10-20 16:22:53 +02:00
tajniak81andClaude Opus 5.5 a892bfb209 Give the Fly App GO 4-style calibration wizards
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>
2026-10-19 21:41:19 +02:00
tajniak81andClaude Opus 5.5 284de5442a Give the Fly App a GO 4-style settings sheet
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>
2026-10-19 21:13:45 +02:00
tajniak81andClaude Opus 5.5 60c2fd7f64 Give the Fly App GO 4-style gimbal controls
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>
2026-10-19 20:22:42 +02:00
tajniak81andClaude Opus 5.5 c1770fff73 Give the Fly App GO 4-style camera controls
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>
2026-10-19 19:57:38 +02:00
tajniak81andClaude Opus 5.5 6ebc567e97 Put every flight command behind a SafetyGate
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>
2026-10-19 19:24:17 +02:00
tajniak81andClaude Opus 5.5 590f2a6152 Give the Fly App a GO 4-style flight HUD
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>
2026-10-19 18:54:09 +02:00
tajniak81andClaude Opus 5.5 f6bbb1cfee Batch the Fly App's telemetry and split it per subsystem
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>
2026-10-19 18:34:56 +02:00
tajniak81andClaude Opus 5.5 a8483ecf45 Rename "Docker AIO" to Docker-AIO
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>
2026-10-19 17:56:48 +02:00
tajniak81andClaude Opus 5.5 606838a0b1 Teach the Fly App what the connected aircraft can do
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>
2026-10-19 17:56:08 +02:00
tajniak81andClaude Opus 4.8 f2a20e541d Point the root README at the App Key's new home
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>
2026-07-21 21:30:52 +02:00
tajniak81andClaude Opus 4.8 58655531f0 Give the Fly App its own identity instead of the DJI sample's
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>
2026-07-21 21:11:17 +02:00
tajniak81andClaude Opus 4.8 6ef15a9e80 Pin container build files to LF via .gitattributes
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>
2026-07-19 19:17:23 +02:00
tajniak81andClaude Opus 4.8 2d20f6f29c Bring the Docker setup in line with DriverVault's
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>
2026-07-19 19:16:21 +02:00
tajniak81andClaude Opus 4.8 11fb1e060d List the endpoints the panel had stopped keeping up with
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>
2026-07-16 18:48:00 +02:00
tajniak81andClaude Opus 4.8 183c83c177 Stop reporting the flight controller's serial as the drone's
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>
2026-07-16 18:36:14 +02:00
tajniak81andClaude Opus 4.8 33595c99e8 Add a Drones fleet section that fills itself in on connect
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>
2026-07-16 17:04:12 +02:00
tajniak81andClaude Opus 4.8 3e066c0a99 Hide bogus duration badges on Album stills
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>
2026-07-16 15:32:27 +02:00
tajniak81andClaude Opus 4.8 002e484d80 Fix Fly App reporting the flight controller's firmware as the aircraft's
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>
2026-07-16 15:32:17 +02:00
tajniak81andClaude Opus 4.8 ec4b1bcdd3 Show drone serial number and firmware in the Fly App
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>
2026-07-16 13:49:57 +02:00
tajniak81andClaude Opus 4.8 42f8054b45 Git-ignore generated DJI SDK class reference
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>
2026-07-15 22:23:29 +02:00
tajniak81andClaude Opus 4.8 20699fd88d Rebuild Web App embedded frontend bundle
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>
2026-07-15 17:48:44 +02:00
tajniak81andClaude Opus 4.8 a4a2709456 Rebuild Fly App to design v2 with full DJI SDK integration
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>
2026-07-15 16:51:08 +02:00
tajniak81andClaude Opus 4.8 3903428964 Let OpenWeather calls-per-minute limit cascade to orgs and users
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>
2026-07-14 18:02:31 +02:00
tajniak81andClaude Opus 4.8 5960fb4806 Show current weather on the Overview
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>
2026-07-14 16:34:01 +02:00
tajniak81andClaude Opus 4.8 522fe450eb Report keyless OpenWeather as degraded, not down
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>
2026-07-14 12:23:31 +02:00
tajniak81andClaude Opus 4.8 6d73c89cc8 Show OpenWeather API call usage in the app
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>
2026-07-14 11:49:19 +02:00