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>
This commit is contained in:
1 parent
33595c99e8
commit
183c83c177
14 files changed
+297
-139
No files matched your search
@@ -346,9 +346,10 @@ export async function createDrone(drone) {
|
||||
return { ok: r.ok, status: r.status, body: await r.json().catch(() => ({})) }
|
||||
}
|
||||
|
||||
// Upsert the drone a connected device just reported, keyed by serial. Safe to
|
||||
// call on every connection event: the server refreshes an existing entry rather
|
||||
// than duplicating it, and answers { created, updated }.
|
||||
// Upsert the drone a connected device just reported, keyed by the flight
|
||||
// controller's serial (the only one an aircraft reports). Safe to call on every
|
||||
// connection event: the server refreshes an existing entry rather than
|
||||
// duplicating it, and answers { created, updated }.
|
||||
export async function autoAddDrone(identity) {
|
||||
try {
|
||||
const r = await fetch('/bff/drones/auto', {
|
||||
|
||||
@@ -311,10 +311,15 @@ const fleet = computed(() =>
|
||||
}),
|
||||
)
|
||||
|
||||
// Serials of aircraft connected right now — lets the Drones section flag which
|
||||
// fleet entry is the drone in front of the pilot.
|
||||
const connectedSerials = computed(() =>
|
||||
ids.value.map((id) => (devices[id].connected ? devices[id].serial : '')).filter(Boolean),
|
||||
// Flight-controller serials of aircraft connected right now — lets the Drones
|
||||
// section flag which fleet entry is the drone in front of the pilot. Keyed on
|
||||
// the flight controller's serial because that is the only one an aircraft
|
||||
// reports; the airframe serial on a fleet entry is hand-entered and no connected
|
||||
// device ever knows it.
|
||||
const connectedFcSerials = computed(() =>
|
||||
ids.value
|
||||
.map((id) => (devices[id].connected ? devices[id].flightControllerSerial : ''))
|
||||
.filter(Boolean),
|
||||
)
|
||||
const onlineCount = computed(() => ids.value.filter((id) => devices[id].online).length)
|
||||
const flyingCount = computed(() =>
|
||||
@@ -391,9 +396,10 @@ const orgLabel = computed(
|
||||
/* ---------- fleet auto-registration ---------- */
|
||||
|
||||
// Connecting a drone in the Fly App should be enough to get it into the pilot's
|
||||
// fleet — nobody wants to retype a serial off an airframe. Every device update
|
||||
// carrying a connected aircraft's serial is offered to the server, which upserts
|
||||
// on serial (see POST /api/drones/auto).
|
||||
// fleet — nobody wants to retype an airframe's identity. Every device update
|
||||
// carrying a connected aircraft's flight-controller serial is offered to the
|
||||
// server, which upserts on it (see POST /api/drones/auto). The airframe serial
|
||||
// is not part of this: the SDK cannot read it, so the pilot enters it by hand.
|
||||
//
|
||||
// upsert() runs on every telemetry frame, so the identity tuple is remembered
|
||||
// and only a *change* is sent: without that this would fire a request per frame.
|
||||
@@ -404,14 +410,19 @@ const sentIdentities = new Set()
|
||||
const dronesView = ref(null)
|
||||
|
||||
async function autoRegister(d) {
|
||||
if (!d.connected || !d.serial) return
|
||||
if (!d.connected || !d.flightControllerSerial) return
|
||||
const identity = {
|
||||
serial: d.serial,
|
||||
flightControllerSerial: d.flightControllerSerial,
|
||||
model: d.model || '',
|
||||
firmware: d.firmware || '',
|
||||
controllerFirmware: d.controllerFirmware || '',
|
||||
}
|
||||
const key = [identity.serial, identity.model, identity.firmware, identity.controllerFirmware].join('|')
|
||||
const key = [
|
||||
identity.flightControllerSerial,
|
||||
identity.model,
|
||||
identity.firmware,
|
||||
identity.controllerFirmware,
|
||||
].join('|')
|
||||
if (sentIdentities.has(key)) return
|
||||
sentIdentities.add(key)
|
||||
|
||||
@@ -432,10 +443,10 @@ async function autoRegister(d) {
|
||||
// A drone deleted from the fleet must be able to come back: without this, its
|
||||
// identity tuple stays in sentIdentities and every later event short-circuits,
|
||||
// so a drone deleted while connected would not reappear until a page reload.
|
||||
function forgetIdentity(serial) {
|
||||
if (!serial) return
|
||||
function forgetIdentity(fcSerial) {
|
||||
if (!fcSerial) return
|
||||
for (const key of sentIdentities) {
|
||||
if (key.startsWith(`${serial}|`)) sentIdentities.delete(key)
|
||||
if (key.startsWith(`${fcSerial}|`)) sentIdentities.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1046,7 +1057,7 @@ onBeforeUnmount(() => {
|
||||
<Logbook v-else-if="active === 'Logbook'" :email="email" :role="role" :organization="organization" :organization-name="organizationName" />
|
||||
|
||||
<!-- ---------- Drones ---------- -->
|
||||
<Drones v-else-if="active === 'Drones'" ref="dronesView" :connected-serials="connectedSerials"
|
||||
<Drones v-else-if="active === 'Drones'" ref="dronesView" :connected-fc-serials="connectedFcSerials"
|
||||
@deleted="forgetIdentity" />
|
||||
|
||||
<!-- ---------- Documents ---------- -->
|
||||
|
||||
@@ -6,9 +6,10 @@ import { getDrones, createDrone, updateDrone, deleteDrone } from '../api.js'
|
||||
// Org/role scoping is enforced server-side, so this component needs no identity
|
||||
// of its own — only which aircraft are connected right now.
|
||||
const props = defineProps({
|
||||
// Serials of the aircraft currently connected to a device, so the fleet can
|
||||
// show which entry is the drone in front of the pilot right now.
|
||||
connectedSerials: { type: Array, default: () => [] },
|
||||
// Flight-controller serials of the aircraft currently connected to a device, so
|
||||
// the fleet can show which entry is the drone in front of the pilot right now.
|
||||
// Not the airframe serials: a connected aircraft cannot report one.
|
||||
connectedFcSerials: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
// Deleting a drone has to reach the auto-registration bookkeeping in Dashboard,
|
||||
@@ -41,9 +42,9 @@ async function loadAll() {
|
||||
onMounted(loadAll)
|
||||
defineExpose({ reload: loadAll })
|
||||
|
||||
const connected = computed(() => new Set(props.connectedSerials.filter(Boolean)))
|
||||
const connected = computed(() => new Set(props.connectedFcSerials.filter(Boolean)))
|
||||
function isConnected(d) {
|
||||
return !!d.serial && connected.value.has(d.serial)
|
||||
return !!d.flightControllerSerial && connected.value.has(d.flightControllerSerial)
|
||||
}
|
||||
|
||||
/* ---------------- drone form ---------------- */
|
||||
@@ -51,7 +52,8 @@ function isConnected(d) {
|
||||
const C_CLASSES = ['', 'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6']
|
||||
function blankDrone() {
|
||||
return {
|
||||
name: '', model: '', serial: '', firmware: '', controllerFirmware: '',
|
||||
name: '', model: '', serial: '', flightControllerSerial: '',
|
||||
firmware: '', controllerFirmware: '',
|
||||
registration: '', operatorNumber: '', mtomGrams: '', isToy: false,
|
||||
autologsFlights: false, cClass: '',
|
||||
}
|
||||
@@ -71,6 +73,10 @@ function newDrone() {
|
||||
function editDrone(d) {
|
||||
Object.assign(form, {
|
||||
name: d.name || '', model: d.model || '', serial: d.serial || '',
|
||||
// Carried through untouched: the save is a whole-record write, so dropping
|
||||
// this would blank the key the aircraft is recognised by on its next connect
|
||||
// — and the fleet would grow a duplicate entry for the same drone.
|
||||
flightControllerSerial: d.flightControllerSerial || '',
|
||||
firmware: d.firmware || '', controllerFirmware: d.controllerFirmware || '',
|
||||
registration: d.registration || '', operatorNumber: d.operatorNumber || '',
|
||||
mtomGrams: d.mtomGrams || '', isToy: !!d.isToy,
|
||||
@@ -86,7 +92,7 @@ function cancel() {
|
||||
}
|
||||
async function save() {
|
||||
msg.value = ''
|
||||
if (!form.name.trim() && !form.model.trim() && !form.serial.trim()) {
|
||||
if (!form.name.trim() && !form.model.trim() && !form.serial.trim() && !form.flightControllerSerial.trim()) {
|
||||
msg.value = 'Give the drone a custom name, model or serial.'
|
||||
return
|
||||
}
|
||||
@@ -107,7 +113,7 @@ async function removeDrone(d) {
|
||||
const res = await deleteDrone(d.id)
|
||||
confirmId.value = ''
|
||||
if (res.ok) {
|
||||
emit('deleted', d.serial)
|
||||
emit('deleted', d.flightControllerSerial)
|
||||
await loadAll()
|
||||
} else msg.value = res.body?.error || 'Could not delete the drone.'
|
||||
}
|
||||
@@ -171,12 +177,17 @@ const stats = computed(() => ({
|
||||
<label class="block"><span class="eyebrow mb-1 block">Model</span>
|
||||
<input v-model="form.model" class="field" placeholder="DJI Mavic 3 Enterprise" /></label>
|
||||
<label class="block"><span class="eyebrow mb-1 block">Serial number</span>
|
||||
<input v-model="form.serial" class="field" /></label>
|
||||
<input v-model="form.serial" class="field" placeholder="08QDE3H012032E" />
|
||||
<span class="mt-1 block text-xs text-ink-muted">From the sticker on the airframe.</span></label>
|
||||
|
||||
<label class="block"><span class="eyebrow mb-1 block">Flight controller SN</span>
|
||||
<input v-model="form.flightControllerSerial" class="field" placeholder="08RDE1J00103H1" />
|
||||
<span class="mt-1 block text-xs text-ink-muted">Auto-filled on connect.</span></label>
|
||||
<label class="block"><span class="eyebrow mb-1 block">Drone firmware</span>
|
||||
<input v-model="form.firmware" class="field" placeholder="03.02.35.05" /></label>
|
||||
<label class="block"><span class="eyebrow mb-1 block">Controller firmware</span>
|
||||
<input v-model="form.controllerFirmware" class="field" placeholder="01.03.0800" /></label>
|
||||
|
||||
<label class="block"><span class="eyebrow mb-1 block">Registration (FAA/CAA)</span>
|
||||
<input v-model="form.registration" class="field" placeholder="FA3X7K9PLM" /></label>
|
||||
|
||||
@@ -191,8 +202,9 @@ const stats = computed(() => ({
|
||||
</div>
|
||||
|
||||
<p class="mt-3 text-xs text-ink-muted">
|
||||
Model, serial and both firmware versions fill themselves in when the drone connects — anything
|
||||
you type here is kept as-is.
|
||||
Model, flight controller SN and both firmware versions fill themselves in when the drone
|
||||
connects — anything you type here is kept as-is. The serial number is the one you register the
|
||||
drone under, and only you can supply it: the SDK cannot read it off the aircraft.
|
||||
</p>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-6">
|
||||
@@ -249,7 +261,16 @@ const stats = computed(() => ({
|
||||
<div v-if="d.name && d.model" class="text-xs text-ink-muted">{{ d.model }}</div>
|
||||
<div v-else-if="!d.name" class="text-xs text-ink-muted">no custom name yet</div>
|
||||
</td>
|
||||
<td class="px-5 py-3 font-mono text-xs text-ink-secondary">{{ d.serial || '—' }}</td>
|
||||
<td class="px-5 py-3 font-mono text-xs text-ink-secondary">
|
||||
{{ d.serial || '—' }}
|
||||
<!-- Second line, not a column of its own: the flight controller's SN
|
||||
is how the app recognises the drone, but the airframe's is the
|
||||
one that matters on paper, so it must not read as an equal. -->
|
||||
<div v-if="d.flightControllerSerial" class="text-[11px] text-ink-muted"
|
||||
title="Flight controller SN — reported by the aircraft, not its registered serial">
|
||||
FC {{ d.flightControllerSerial }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-3 font-mono text-xs text-ink-secondary">{{ d.firmware || '—' }}</td>
|
||||
<td class="px-5 py-3 font-mono text-xs text-ink-secondary">{{ d.controllerFirmware || '—' }}</td>
|
||||
<td class="px-5 py-3 font-mono text-xs text-ink-secondary">{{ d.registration || '—' }}</td>
|
||||
|
||||
Reference in new issue
Block a user