Fleet management is the “which vehicle am I flying, and what is it doing” layer of the Dashboard. It covers four concerns that all revolve around a single selected drone:
  • Listing & selecting vehicles from the fleet (the dropdown and the fleet dialog).
  • The DroneService action surface — the flat REST facade every component calls to CRUD drones and issue flight/mission/geofence/param/video actions.
  • The telemetry HUD — the five status tags (system state, GPS/RTK fix, mode, altitude, battery) rendered by DroneStatusComponent.
  • Selected-drone propagation — how a selection made anywhere fans out to every component and back-end subscription without infinite loops.
This page is about fleet state and the action facade. The transports underneath are documented elsewhere: the Socket.IO telemetry stream that feeds the HUD is in Real-time Telemetry Client; the gateway endpoints these actions hit are in Drone Management & Control Actions; manual/gamepad control is in Vehicle Commands & Gamepad.

The players

PieceFileResponsibility
DroneStatusComponentsrc/app/home/drone-status/drone-status.component.tsTelemetry HUD tags + the vehicle-select dropdown; wires all telemetry subscriptions on selection.
DroneServicesrc/app/services/drone-service/drone.service.tsFlat REST facade for drone CRUD + every /drone/action/* command, video rooms, VPN, params, WireGuard status.
DroneSubscriptionServicesrc/app/services/drone-subscription.service.tsCached fleet list + a keyed Subject registry (currentDroneList, selectedDrone) shared across components without prop drilling.
AppStateServicesrc/app/services/app-state.service.tsOwns selectedDrone$ and persists the selection to localStorage.
CreateDroneDialogComponentsrc/app/home/dialogs/create-drone-dialog/create-drone-dialog.component.tsThe Add Vehicle / edit form.
ListDronesDialogComponentsrc/app/home/dialogs/list-drones-dialog/list-drones-dialog.component.tsFleet manager: per-vehicle status, video, dock, params, restart, edit/delete menus.
The Drone model (src/app/models/drone.model.ts) is the shared shape everywhere:
src/app/models/drone.model.ts
export interface Drone {
  id: string;
  name: string;
  ip: string;
  port: number;
  mission_id: number;
  type: string;          // 'physical' | 'sitl'
  initial_pos?: { lat: number; lng: number };
  vehicle_type: string;  // 'copter' | 'rover'
  video_room_id?: number;
  video_room_password?: string;
  activation_token?: string;
}

Listing & selecting drones

The fleet list is fetched once and cached in DroneSubscriptionService, so multiple components (the status dropdown, the fleet dialog, HomeComponent) share a single copy.
1

Load the fleet

DroneStatusComponent.ngAfterViewInit() only fetches if the cache is empty (droneSubscriptionService.hasDroneList()); otherwise it reuses the cached list. getDroneList() calls DroneService.getDrones()GET /drones and pushes the result into droneSubscriptionService.setCurrentDroneList(...).
2

Display-name truncation

Vehicle names are split on _ and, when there are 4+ parts, only parts[3:] are kept for display (drone-status.component.ts:95-99). This hides the internal SKYHUB_SITL_<n>_...-style prefixes. Note this mutates drone.name in place on the shared list object.
3

Select via the dropdown

The PrimeNG dropdown is bound [(ngModel)]="selectedDrone" with optionLabel="name" and [showClear]="true". Picking a vehicle fires onChange($event.value, true) — the true marks it as a user interaction.
4

Restore the last selection

On list load, DroneStatusComponent reads appStateService.getSelectedDrone() (backed by localStorage key selected_drone_id) and re-selects the matching drone if it still exists in the fleet; otherwise it clears the stale selection.

The fleet dialog

ListDronesDialogComponent (app-list-drones-dialog) is the richer manager surface. It re-fetches drones, then computes a per-vehicle status with fetchDroneStatuses():
  • type === 'sitl' → always Online.
  • Has an activation_tokenPending Activation (a physical drone that has not yet called GET /drone/activate on the gateway).
  • Otherwise → getVPNStatus(id) (GET /drone/{id}/vpn/status); truthy statusOnline, else Offline.
It also tracks each drone’s live flight mode by subscribing to websocketService.diagnosticsSubject and reading the Mode value out of the MAVROS Heartbeat diagnostic. Its context menus expose dock start/stop, takeoff, motor test, toggle lights, video start/stop/restart, camera focus, restart vehicle / flight controller, charging on/off, edit, delete, and view — most routed through confirmation dialogs in AppStateService.
Some fleet actions in the dialog are not DroneService REST calls. Restart, charging, and camera-focus commands are pushed as system_command / charging_control / camera_command payloads through appStateService.setControllerData(...), which travels the low-latency redispad WebSocket path — see Vehicle Commands & Gamepad.

Selected-drone propagation

A selection can originate from three places, and all three must converge on one source of truth without looping. DroneStatusComponent subscribes to all three and de-dupes by object identity: The fromUserInteraction flag is the loop-breaker. onChange(drone, fromUserInteraction):
  1. Always tears down the previous telemetry subscriptions (diagnostics, gps, gpsRaw, relAlt) and calls websocketService.killAllActiveEventSource(), then clears the HUD fields.
  2. Always re-subscribes for the new drone: websocketService.getAllData(drone.id) (the dashboard stream) plus the four subject subscriptions.
  3. Only when fromUserInteraction === true writes back to appStateService.setSelectedDrone(drone) and droneSubscriptionService.setValue('selectedDrone', drone).
Because the two service subscriptions call onChange(drone, false), a selection that arrives from a service never writes back to that service — that is what prevents the circular dispatch. HomeComponent independently subscribes to appStateService.selectedDrone$ to drive the map / 3D marker (home.component.ts:268).
Do not drop the fromUserInteraction argument or start writing back on false. AppStateService, DroneSubscriptionService, and the dropdown feed each other; without the gate you get an infinite onChange → setValue → onChange loop and duplicate telemetry subscriptions.

Adding a vehicle

The Add Vehicle dialog (CreateDroneDialogComponent, shown from HomeComponent.editDrone() / app-create-drone-dialog) is deliberately minimal — only Name, Type, and Vehicle Type are surfaced: Add Vehicle dialog (Type: Physical/SITL, Vehicle Type: Copter/Rover)
  • Type: Physical (physical) or Simulated (sitl). typeChange() disables the ip/port controls for SITL, since simulated containers are spawned by the gateway (their ip/mac/port are assigned server-side — see SITL Drone Lifecycle).
  • Vehicle Type: Rover or Copter — drives control/axis mappings downstream.
On save (create-drone-dialog.component.ts):
1

Free-tier limit check (create only)

If the user has no subscription, onSave() calls getDrones() first; if they already own 1 or more vehicles it opens app-subscription-dialog instead of creating. Editing skips this.
2

POST the drone

proceedWithSave() calls DroneService.createDrone(drone)POST /drone (or updateDronePUT /drone/{id} when editing). A loading dialog shows “Creating Simulated Vehicle…” for SITL vs “Creating vehicle…” otherwise.
3

Handle billing rejection

A 402 response with reason of no_subscription, subscription_inactive, or vehicle_limit_reached re-opens the subscription dialog. The backend is the real enforcement point; the client-side count check is a shortcut. See Stripe Billing & Vehicle Limits.
4

Show activation details

On success the fleet list is refreshed (setValue('currentDroneList', ...)) and app-vehicle-view-dialog opens with the created drone, including the activation_token a physical vehicle uses to bootstrap itself.

DroneService action surface

DroneService is a thin, providedIn-root HTTP facade — no state, one method per gateway endpoint. All URLs are prefixed with environment.url (the …/api/v1 base). The AuthInterceptor attaches the Bearer JWT (see Auth: Guard, Interceptor & AuthService).
MethodHTTPBody / notes
getDrones()GET /dronesReturns Drone[].
createDrone(drone)POST /droneFull Drone payload.
updateDrone(drone)PUT /drone/{id}
deleteDrone(droneId)DELETE /drone/{id}
When adding a new drone action, add one method here and call the gateway route from it — keep DroneService free of component state and RxJS beyond the returned Observable. Components subscribe and handle toasts/state. The full REST inventory across all services is in Angular Services & REST Reference.

The telemetry HUD

DroneStatusComponent renders five PrimeNG tags above the dropdown. Each is fed by a distinct telemetry subject on WebsocketService, filtered to the selected drone. All five tags show placeholder text (STATE/GPS/MODE/ALT/BATT) when no drone is selected. While isLoading, the System-state tag shows Loading... and the other four (GPS/MODE/ALT/BATT) show ---.

What each subject provides

SubjectSource topicHUD fields
diagnosticsSubject/diagnosticsbattery/voltage/current (from mavros: Battery), mode + systemStatus (from Heartbeat via getMode), gpsStatus + gpsSatellitesCount (from mavros: GPS).
gpsRawSubjectGPS_RAWgpsFixType + isRtkFix from fix_type.
relAltSubject/mavros/global_position/rel_altalt — relative altitude (AGL, height above home/takeoff).
gpsSubject/mavros/global_position/globalgpsAltitude — absolute altitude (MSL) + gpsAccuracy derived from position covariance.
Subject filtering is inconsistent by design and easy to break. gpsSubject/gpsRawSubject filter on event.droneId !== String(this.selectedDrone?.id) (string compare), while diagnosticsSubject/relAltSubject filter on event.drone_id !== this.selectedDrone?.id (numeric). The subjects are shared across the whole fleet, so every consumer must filter — preserve the exact key name and type when touching this.

GPS / RTK fix mapping

gpsRawSubject carries the MAVLink GPS_RAW fix_type. getGpsRawData() maps it to a label and an isRtkFix flag that drives tag color:
fix_typegpsFixTypeisRtkFixHUD severity
6RTK Fixedsuccess (green)
5RTK Floatsuccess (green)
4DGPSwarning
33D Fixwarning
22D Fixdanger
1No Fixdanger
0No GPSdanger
getGpsSeverity() returns success for any RTK fix, warning for 3d fix/dgps/sbas, otherwise danger. The GPS tag text is (gpsFixType || gpsStatus) + ' ' + gpsSatellitesCount.

Altitude: MSL vs AGL

The HUD deliberately tracks two altitudes:
  • alt — relative altitude from relAltSubject, i.e. AGL height above the home/takeoff point. This is what the ALT tag shows (alt <n>M) and what appStateService.altitude is set to.
  • gpsAltitude — absolute MSL altitude from gpsSubject.
The altitude tooltip combines them to estimate terrain: ground elevation = gpsAltitude − alt (MSL below the vehicle). Keep the MSL-vs-AGL distinction — conflating them would put the drone marker at the wrong height and break the ground-elevation readout.

Other tag logic

  • System statesystemStatus === 'ACTIVE' renders ARMED (green); anything else renders DISSARMED (red — the literal string is misspelled in the template). systemStatus comes from the Heartbeat System status diagnostic.
  • ModeMANUAL/LOITER/LOYTER/POSHOLD are warning, GUIDED is info, everything else success. MODE_DESCRIPTIONS provides tooltip text.
  • Battery≤ 0 or < 10% → danger, < 30% → warning, else success. Tooltip adds voltage/current and a Good/Low/Critical health line.
Tooltip caching is a performance guard, not decoration. The getXxxTooltip() / getGpsSeverity() getters are bound in the template, so Angular calls them on every change-detection pass. invalidateTooltipCacheIfStale() compares a snapshot of all telemetry fields and only recomputes the memoized strings when something actually changed, avoiding per-pass string/array allocations at telemetry frequency. Preserve this when adding a tag.

Gotchas to preserve

  • getAllData is the only live path. The HUD subscribes via getAllData(id) → the dashboard stream. getGpsData()/getLogsData() (single-stream subscriptions) exist but the gateway only delivers data on the dashboard room — treat them as effectively dead. Details in Real-time Telemetry Client.
  • Selection teardown is mandatory. onChange() unsubscribes the four subjects and calls killAllActiveEventSource() before re-subscribing. Skipping teardown leaks subscriptions and mixes telemetry from the previous drone into the new HUD.
  • Name mutation. The parts.slice(3) truncation rewrites drone.name on the shared cached object, so downstream consumers see the shortened name too.
  • localStorage is the persistence layer. Selection survives reloads via selected_drone_id; a stale id (deleted drone) is cleared on next fleet load.
  • Client-side vehicle-limit check is advisory. The real free-tier / subscription enforcement is the gateway 402 response; keep both in sync but trust the backend.