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:
1 parent
fb1b8af2d1
commit
f9a61a086f
211 files changed
+45657
-12740
No files matched your search
@@ -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};
|
||||
Reference in new issue
Block a user