feat(firmware): validate configuration and migrate sensor-driven flight APIs

Validate configuration through its owning schemas and keep board
declarations separate from shared F722 resource ownership. Bound USB
transitions, isolate transport failures and retain restart-only activation.

Migrate navigation and flight consumers with the sensor and schema
contracts. Require explicit installation data and synchronous output
authority; board motor outputs remain inhibited.

Validation: assembled-prefix host tests, strict Clippy, platform/USB tests
and both ARM release builds. Final-source coverage and scoped Miri/Kani
evidence are retained separately from hardware qualification.
This commit is contained in:
Onur Özkan committed 2026-11-07 18:12:11 +03:00
1 parent fb1b8af2d1
commit f9a61a086f
211 files changed
+45657 -12740

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
// Local view ordering and intent binding. Contexts are not credentials.
function sameContext(left, right) {
return ['instance', 'connection', 'configuration', 'draft'].every(key => left?.[key] === right?.[key]);
}
class ViewSession {
constructor({read, send, render, error, permissions, edited = () => {}}) {
Object.assign(this, {read, send, render, error, permissions, edited});
this.view = null;
this.queue = [];
this.ticket = 0;
this.acceptedTicket = 0;
this.retired = new Set();
this.message = '';
}
get busy() { return this.queue.length !== 0; }
get onlyEdits() { return this.queue.every(entry => entry.path === '/api/config/edit'); }
editing(id) {
return this.queue.some(entry => entry.body.field_id === id
&& entry.context.instance === this.view?.state.context.instance
&& entry.context.connection === this.view?.state.context.connection
&& entry.context.configuration === this.view?.state.context.configuration);
}
accept(view, ticket) {
const current = this.view?.state;
const next = view.state;
if (current) {
if (current.context.instance === next.context.instance) {
if (BigInt(next.config_revision) < BigInt(current.config_revision)) return false;
} else {
if (ticket < this.acceptedTicket || this.retired.has(next.context.instance)) return false;
this.retired.add(current.context.instance);
}
}
this.acceptedTicket = Math.max(ticket, this.acceptedTicket);
this.view = view;
this.render(view);
if (this.message) this.error(this.message);
return true;
}
async refresh() {
const ticket = ++this.ticket;
const view = await this.read();
return this.accept(view, ticket);
}
mutate(path, body = {}, context = this.view?.state.context) {
if (!this.view) return Promise.resolve(null);
return new Promise(resolve => {
this.queue.push({path, body, context, resolve});
this.permissions();
if (this.queue.length === 1) this.drain();
});
}
async drain() {
while (this.queue.length) {
const entry = this.queue[0];
const ticket = ++this.ticket;
let result = null;
try {
if (!sameContext(entry.context, this.view.state.context)) {
const error = Error('View changed; queued action discarded. Review the current configuration.');
error.staleIntent = true;
throw error;
}
const view = await this.send(entry.path, {...entry.body, context: entry.context});
if (this.accept(view, ticket)) {
this.message = '';
result = view;
// Only our own accepted edits can advance already queued edits.
// A poll, another editor, a reset, or a new connection cannot rebase intent.
if (entry.path === '/api/config/edit'
&& view.state.context.instance === entry.context.instance
&& view.state.context.connection === entry.context.connection
&& view.state.context.configuration === entry.context.configuration) {
for (const queued of this.queue.slice(1)) {
if (sameContext(queued.context, entry.context)) queued.context = view.state.context;
}
this.edited(entry.context, view.state.context);
}
}
} catch (error) {
const relevant = error.view
? this.accept(error.view, ticket)
: error.staleIntent || sameContext(entry.context, this.view.state.context);
if (relevant) this.message = error.message;
} finally {
this.queue.shift();
this.render(this.view);
if (this.message) this.error(this.message);
this.permissions();
entry.resolve(result);
}
}
}
}
if (typeof module !== 'undefined') module.exports = {ViewSession, sameContext};