also looking at this
feat: add VTOL scenario playback to the mission workspace
#84
26 files
+1336
-66
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -150,6 +150,10 @@ jobs: | ||
| 150 | 150 | working-directory: ${{ runner.temp }} |
| 151 | 151 | - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:hover --scenario host-loss --electrical |
| 152 | 152 | working-directory: ${{ runner.temp }} |
| 153 | - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:simulate | |
| 154 | working-directory: ${{ runner.temp }} | |
| 155 | - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:simulate --electrical | |
| 156 | working-directory: ${{ runner.temp }} | |
| 153 | 157 | - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone authority:simulate --scenario pi-loss --seconds 4 |
| 154 | 158 | working-directory: ${{ runner.temp }} |
| 155 | 159 | - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone drivers:bench --driver ardupilot --drop-first-ack |
| @@ -78,8 +78,9 @@ outputs remain inhibited. Read the [shared contract](docs/vehicles.md), [air mod | ||
| 78 | 78 | |
| 79 | 79 | `./buddy vtol:hover` separately exercises [VTOL development](docs/vehicles-vtol.md): |
| 80 | 80 | vertical takeoff, a nearby hover waypoint and contact-gated landing. The TypeScript library also |
| 81 | includes a bounded modeled front/cruise/back experiment with stable-phase pause and resume. The | |
| 82 | command stays hover-only. Add `--electrical` for fictional propulsion/battery load and delayed energy measurements. | |
| 81 | includes a bounded modeled front/cruise/back experiment with stable-phase pause and resume. | |
| 82 | `./buddy vtol:simulate` runs its nominal route through vertical landing and reports the qualified | |
| 83 | mode observations. Add `--electrical` to either command for fictional propulsion/battery load and delayed energy measurements. | |
| 83 | 84 | Plans can explicitly declare bounded hover-and-land transition recovery. Complete recovery/abort envelopes, |
| 84 | 85 | measured hybrid energy qualification and a selectable VTOL mission workspace remain open. |
| 85 | 86 | |
| @@ -0,0 +1,28 @@ | ||
| 1 | import { defineCommand } from '@stacksjs/cli' | |
| 2 | import { print, printError } from '@stacksjs/cli/output' | |
| 3 | import { createVtolTransitionProfile } from '../../resources/functions/drone/vehicles/vtol/transition-profile' | |
| 4 | import { runVehicleSimulation } from '../../resources/functions/drone/vehicles/runtime' | |
| 5 | ||
| 6 | export default defineCommand({ | |
| 7 | name: 'vtol:simulate', | |
| 8 | description: 'Exercise the fictional owned front/cruise/back/landing route; no physical control', | |
| 9 | options: { | |
| 10 | '--seconds <n>': { description: 'Maximum virtual duration, 2..180 seconds', default: '160' }, | |
| 11 | '--electrical': { description: 'Use fictional electrical load and delayed battery measurements', default: false }, | |
| 12 | }, | |
| 13 | handle(options) { | |
| 14 | try { | |
| 15 | const result = runVehicleSimulation(createVtolTransitionProfile({ electrical: options.electrical }), { seconds: Number(options.seconds) }) | |
| 16 | const { trace, path, ...report } = result | |
| 17 | const transitionEvidence = trace.rows.flatMap(row => row.observation.transition?.outcome === 'confirmed' ? [row.observation.transition] : []) | |
| 18 | const completeTransitions = transitionEvidence.length === 3 && transitionEvidence.every((row, index) => row.taskId === ['initial-hover', 'front', 'back'][index] && row.phase === ['hover', 'cruise', 'hover'][index]) | |
| 19 | const observedFlight = path.some(row => row.positionM && row.positionM[2] < -1 && row.actuators.slice(0, 4).some(n => n > 0)) | |
| 20 | const landingContact = path.at(-1)?.measurement?.values.onSurface === true | |
| 21 | const boardNeutral = result.board.actuators.every(n => n === 0) && result.board.expiresAtMs === null | |
| 22 | const demonstrationMatched = result.status === 'completed' && result.fault === null && result.mission.taskId === 'land' && result.mission.reason === 'landed' && completeTransitions && observedFlight && landingContact && boardNeutral | |
| 23 | print(JSON.stringify({ ...report, scope: 'development-lift-and-cruise', transitionEvidence, completeTransitions, observedFlight, landingContact, boardNeutral, demonstrationMatched, recordedMissionRows: trace.rows.length, evaluatedPathRows: path.length }, (_key, value) => typeof value === 'bigint' ? String(value) : value, 2)) | |
| 24 | if (!demonstrationMatched) process.exitCode = 1 | |
| 25 | } | |
| 26 | catch (error) { printError(error instanceof Error ? error.message : String(error)); process.exitCode = 1 } | |
| 27 | }, | |
| 28 | }) | |
| @@ -51,6 +51,7 @@ import PatrolReplay from '../app/Commands/PatrolReplay' | ||
| 51 | 51 | import AntennaBench from '../app/Commands/AntennaBench' |
| 52 | 52 | import AirSimulate from '../app/Commands/AirSimulate' |
| 53 | 53 | import VtolHover from '../app/Commands/VtolHover' |
| 54 | import VtolSimulate from '../app/Commands/VtolSimulate' | |
| 54 | 55 | import GroundSimulate from '../app/Commands/GroundSimulate' |
| 55 | 56 | import SurfaceSimulate from '../app/Commands/SurfaceSimulate' |
| 56 | 57 | import SubseaSimulate from '../app/Commands/SubseaSimulate' |
| @@ -128,6 +129,7 @@ PatrolReplay(app) | ||
| 128 | 129 | AntennaBench(app) |
| 129 | 130 | AirSimulate(app) |
| 130 | 131 | VtolHover(app) |
| 132 | VtolSimulate(app) | |
| 131 | 133 | GroundSimulate(app) |
| 132 | 134 | SurfaceSimulate(app) |
| 133 | 135 | SubseaSimulate(app) |
| @@ -56,7 +56,7 @@ surface and subsea profiles. [Optional external drivers](drivers.md) have | ||
| 56 | 56 | [acknowledged protocol services](driver-protocols.md); scripted peers are distinct |
| 57 | 57 | from [firmware SITL](sitl.md) and physical testing. |
| 58 | 58 | |
| 59 | The separate [VTOL development guide](vehicles-vtol.md) covers the hover-only command and an | |
| 59 | The separate [VTOL development guide](vehicles-vtol.md) covers the hover-only and nominal full-route commands, and an | |
| 60 | 60 | experimental TypeScript library profile for modeled front transition, cruise, back transition and |
| 61 | 61 | vertical landing, with stable-phase pause/resume and explicitly declared hover-and-land recovery |
| 62 | 62 | routes. Complete recovery/abort envelopes, hybrid energy qualification and the common VTOL mission |
| @@ -16,7 +16,7 @@ means there is no native implementation yet. A protocol-compatible transport is | ||
| 16 | 16 | | Diagnostics, logs, replay, firmware update and configuration provenance | Owned runtime journal, loss accounting, replay, CLI/viewer and [protected signed preparation, owned candidates, host slots, observed image/session health and fresh-policy rollback](../runtime-updates.md); full mission correlation, key repair, complete storage faults and board install/boot/rollback qualification remain pending | Named parameter read/write with bounded readback in the SITL adapter; no log/FTP services, full-table synchronization or reboot persistence qualification | [#9](https://github.com/stacksjs/drone/issues/9) | |
| 17 | 17 | | Missions, tasks, waypoints, terrain, rally and resume | Owned versioned graph, flat-terrain/fence/rally/energy validation, physics execution, pause/resume, recorded recovery, CLI and stx viewer; correlated durable checkpoints, driver translation and hardware evidence pending | Bounded SITL waypoint upload/download and acknowledged item selection/resume; no arming, automatic flight-mode selection or owned-plan translation | [#10](https://github.com/stacksjs/drone/issues/10) | |
| 18 | 18 | | Avoidance, route planning, precision landing, follow and orbit | Owned modeled range detection, inflated route planning, terrain-clearance/RTL/selected-site landing physics; metric follow/orbit goals; physical calibration and dynamic obstacle campaigns pending | No avoidance/landing integration | [#11](https://github.com/stacksjs/drone/issues/11) | |
| 19 | | Fixed-wing, VTOL, heli, blimp and experimental airframes | Owned fixed-wing runway takeoff/survey/landing, hover-only VTOL command and [experimental VTOL library](../vehicles-vtol.md) with modeled front/cruise/back/vertical landing, pause/resume, opt-in hover-and-land recovery and fictional electrical load/battery/sensor accounting; complete recovery/abort envelopes, measured hybrid energy, native transition CLI/workspace, helicopter, soaring, blimp and other airframe families remain open | Current drivers reject non-quadcopter heartbeats | [#12](https://github.com/stacksjs/drone/issues/12) | | |
| 19 | | Fixed-wing, VTOL, heli, blimp and experimental airframes | Owned fixed-wing runway takeoff/survey/landing, hover-only and nominal full-route VTOL commands and [experimental VTOL library](../vehicles-vtol.md) with modeled front/cruise/back/vertical landing, pause/resume, opt-in hover-and-land recovery and fictional electrical load/battery/sensor accounting; complete recovery/abort envelopes, measured hybrid energy, CLI fault/operator scenarios, workspace selection, helicopter, soaring, blimp and other airframe families remain open | Current drivers reject non-quadcopter heartbeats | [#12](https://github.com/stacksjs/drone/issues/12) | | |
| 20 | 20 | | Rovers, mowing, logistics and materials handling | Owned differential traction/braking, encoder/GNSS odometry, guarded mowing coverage and scoped material-drop simulation; hardware and broader chassis/implements pending | Not exposed | [#13](https://github.com/stacksjs/drone/issues/13) | |
| 21 | 21 | | Surface vessels, bathymetry, water sampling and harbours | Owned planar hull/current/wind, collision inhibition, corrected single-beam export, scoped sample ACK and energy-return simulation; physical survey/sampling and broader hulls pending | Not exposed | [#14](https://github.com/stacksjs/drone/issues/14) | |
| 22 | 22 | | Subsea ROV, depth hold, tether and leak handling | Owned eight-thruster 6DOF physics, pressure/DVL local estimation, depth hold, tether/leak supervision and scoped synthetic camera/sonar evidence; hardware, full hydrodynamics and physical inspection pending | Not exposed | [#15](https://github.com/stacksjs/drone/issues/15) | |
| @@ -74,8 +74,27 @@ new CLI scenario. Existing hover-only recordings keep their prior shapes. | ||
| 74 | 74 | is a separate development experiment on the same owned runtime. Its nominal mission observes initial |
| 75 | 75 | hover, takes off vertically to 15 m, requests a front transition, flies a cruise leg, requests back |
| 76 | 76 | transition, returns to the back request's capture point, observes settled hover, and lands vertically. |
| 77 | A turning leg also has an actual completed model campaign. This profile is not registered as a CLI | |
| 78 | scenario or selectable workspace profile. | |
| 77 | A turning leg also has an actual completed model campaign. The native `vtol:simulate` command runs | |
| 78 | the nominal route. This profile is not a selectable workspace profile. | |
| 79 | ||
| 80 | ```sh | |
| 81 | ./buddy vtol:simulate | |
| 82 | ./buddy vtol:simulate --electrical | |
| 83 | ./storage/runtime-build/cli/drone vtol:simulate --electrical | |
| 84 | ``` | |
| 85 | ||
| 86 | The command defaults to 160 virtual seconds and accepts an integer `--seconds` from 2 to 180. | |
| 87 | It reports the existing runtime's actual status and the three qualified initial-hover, cruise and | |
| 88 | back-hover observations, including their event, task, reference and capture epochs. Exit 0 requires | |
| 89 | those ordered confirmations, observed estimated flight, completed landing with qualified modeled | |
| 90 | contact, no runtime fault, neutral board outputs and no active output lease. An incomplete route | |
| 91 | retains its partial observations and status and exits 1. The condensed report omits the full path | |
| 92 | and trace. The library still exposes both for replay and evaluation. | |
| 93 | ||
| 94 | Buddy discovers the typed command natively; the standalone binary registers the same definition. | |
| 95 | This command currently exposes the nominal route only. Fault scenarios, operator schedules and | |
| 96 | recovery route selection remain library capabilities. Simulation completion does not qualify | |
| 97 | physical transitions, arbitrary flight envelopes or hardware landing. | |
| 79 | 98 | |
| 80 | 99 | The controller receives measured relative airspeed, differential pressure, airflow angles and modeled |
| 81 | 100 | rotor/pusher response, together with the qualified raw gyro and estimated navigation. Delayed airflow |
| @@ -150,7 +169,8 @@ and releases outputs. Neither result establishes an airborne landing. | ||
| 150 | 169 | Task execution time stops during pause. Global endurance and the declared energy charge continue, |
| 151 | 170 | so a later scheduled resume can be preempted by reserve, endurance, health or authority refusal. |
| 152 | 171 | Operator actions never extend the original planning allowance or rearm revoked authority. The |
| 153 | transition library and these actions remain separate from the hover-only CLI and common workspace. | |
| 172 | operator schedules remain library-only; neither CLI command exposes them, and the common workspace | |
| 173 | does not select this VTOL profile. | |
| 154 | 174 | |
| 155 | 175 | ## Declared transition recovery routes |
| 156 | 176 | |
| @@ -209,11 +229,11 @@ campaigns do not qualify arbitrary wind, failed lift rotors, every transition st | ||
| 209 | 229 | |
| 210 | 230 | ## Fictional electrical model |
| 211 | 231 | |
| 212 | Select `--electrical` in the hover command, or pass `{ electrical: true }` to | |
| 232 | Select `--electrical` in either VTOL command, or pass `{ electrical: true }` to | |
| 213 | 233 | `createVtolHoverProfile('hover', options)` or `createVtolTransitionProfile(options)` in TypeScript. |
| 214 | 234 | The default/disabled profiles keep their prior reports and declared-ceiling energy accounting. |
| 215 | The same hover option runs through Buddy and the copied standalone CLI. Full transition execution | |
| 216 | remains a library feature. | |
| 235 | Both commands run through Buddy and the copied standalone CLI. The nominal full-route command | |
| 236 | uses the same experimental transition library profile. | |
| 217 | 237 | |
| 218 | 238 | Each session owns its electrical state. The plant uses actual lagged individual rotor and pusher |
| 219 | 239 | thrust, with these declared coefficients: |
| @@ -272,7 +292,7 @@ it does not model a seized motor's electrical current. | ||
| 272 | 292 | |
| 273 | 293 | [Issue #12](https://github.com/stacksjs/drone/issues/12) remains open. The experimental nominal |
| 274 | 294 | controller and opt-in recovery routes do not establish complete VTOL support. Complete recovery/abort |
| 275 | envelopes, fresh-process restoration, identified hybrid power/voltage/cell behavior, native transition CLI and | |
| 295 | envelopes, fresh-process restoration, identified hybrid power/voltage/cell behavior, CLI fault/operator scenarios and | |
| 276 | 296 | the common mission workspace still require implementation and qualification. Wind campaigns and |
| 277 | 297 | broader mission/envelope coverage remain necessary. Full TECS/soaring, tiltrotor/tailsitter mechanisms, |
| 278 | 298 | helicopters and other airframe families remain separate work. Native board control, measured airframe |
| @@ -61,11 +61,12 @@ recompute mission replay, reject altered traces and exercise payload uncertainty | ||
| 61 | 61 | All evidence is owned physics simulation. The Pico neutral-only board image does not run these |
| 62 | 62 | TypeScript controllers. Native board execution, external firmware SITL, HIL and measured physical |
| 63 | 63 | vehicle acceptance remain separate gates. A separate [VTOL development guide](vehicles-vtol.md) |
| 64 | covers the hover-only command and an experimental lift-and-cruise library profile. The latter | |
| 64 | covers the hover-only command and an experimental lift-and-cruise library profile with a native | |
| 65 | `vtol:simulate` nominal-route command. The latter | |
| 65 | 66 | executes a bounded modeled front/cruise/back/vertical-landing route with stable-phase pause and |
| 66 | 67 | resume, plus explicitly declared hover-and-land transition recovery. Complete recovery/abort |
| 67 | envelopes, hybrid energy qualification, | |
| 68 | a native transition command and a VTOL selection in the common workspace remain open. Helicopter, | |
| 68 | envelopes, measured hybrid energy qualification, | |
| 69 | CLI fault/operator scenarios and a VTOL selection in the common workspace remain open. Helicopter, | |
| 69 | 70 | soaring, blimp and other airframe families remain explicit roadmap work. |
| 70 | 71 | |
| 71 | 72 | Read [air](vehicles-air.md), [ground](vehicles-ground.md), [surface](vehicles-surface.md), [subsea](vehicles-subsea.md), |
| @@ -77,6 +77,7 @@ | ||
| 77 | 77 | "./unit/vtol-runtime.test.ts", |
| 78 | 78 | "./unit/vtol-energy.test.ts", |
| 79 | 79 | "./unit/vtol-cli.test.ts", |
| 80 | "./unit/vtol-transition-cli.test.ts", | |
| 80 | 81 | "./unit/vtol-mode-mission.test.ts", |
| 81 | 82 | "./unit/vtol-transition.test.ts", |
| 82 | 83 | "./unit/ground.test.ts", |
| @@ -0,0 +1,73 @@ | ||
| 1 | import { expect, test } from 'bun:test' | |
| 2 | import { command } from '@stacksjs/testing' | |
| 3 | ||
| 4 | test('CI qualifies both full-route commands outside the checkout', async () => { | |
| 5 | const workflow = Bun.YAML.parse(await Bun.file(new URL('../../.github/workflows/ci.yml', import.meta.url)).text()) as { | |
| 6 | jobs: { check: { steps: { run?: string, 'working-directory'?: string }[] } } | |
| 7 | } | |
| 8 | expect(workflow.jobs.check.steps.filter(step => step.run?.startsWith('$GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:simulate')).map(step => ({ command: step.run, directory: step['working-directory'] }))).toEqual([ | |
| 9 | { command: '$GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:simulate', directory: '$' }, | |
| 10 | { command: '$GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:simulate --electrical', directory: '$' }, | |
| 11 | ]) | |
| 12 | }) | |
| 13 | ||
| 14 | test('native VTOL full-route command reports qualified initial hover, cruise, back hover and landing', async () => { | |
| 15 | const result = await command(['vtol:simulate']).withTimeout(60000) | |
| 16 | expect(result.exitCode).toBe(0) | |
| 17 | const report = JSON.parse(result.stdout) | |
| 18 | expect(report.status).toBe('completed') | |
| 19 | expect(report.scope).toBe('development-lift-and-cruise') | |
| 20 | expect(report.demonstrationMatched).toBe(true) | |
| 21 | expect(report.transitionEvidence.map((row: { taskId: string, phase: string }) => [row.taskId, row.phase])).toEqual([ | |
| 22 | ['initial-hover', 'hover'], ['front', 'cruise'], ['back', 'hover'], | |
| 23 | ]) | |
| 24 | for (const row of report.transitionEvidence) { | |
| 25 | expect(row.eventId).toBeString() | |
| 26 | expect(row.reference).toBeString() | |
| 27 | expect(row.outcome).toBe('confirmed') | |
| 28 | expect(row.measurementCapturedAtMs).toBeLessThanOrEqual(row.capturedAtMs) | |
| 29 | } | |
| 30 | expect(report.observedFlight).toBe(true) | |
| 31 | expect(report.landingContact).toBe(true) | |
| 32 | expect(report.mission.state).toBe('completed') | |
| 33 | expect(report.mission.taskId).toBe('land') | |
| 34 | expect(report.mission.reason).toBe('landed') | |
| 35 | expect(report.board.actuators).toEqual([0, 0, 0, 0, 0, 0, 0, 0]) | |
| 36 | expect(report.board.expiresAtMs).toBeNull() | |
| 37 | expect(report.physicalFlightEnabled).toBe(false) | |
| 38 | expect(report.boardOutputsInhibited).toBe(true) | |
| 39 | expect(report).not.toHaveProperty('trace') | |
| 40 | expect(report).not.toHaveProperty('path') | |
| 41 | expect(report).not.toHaveProperty('energy') | |
| 42 | }, 90000) | |
| 43 | ||
| 44 | test('native full-route electrical option retains qualified landing and modeled energy evidence', async () => { | |
| 45 | const result = await command(['vtol:simulate', '--electrical']).withTimeout(60000) | |
| 46 | expect(result.exitCode).toBe(0) | |
| 47 | const report = JSON.parse(result.stdout) | |
| 48 | expect(report.demonstrationMatched).toBe(true) | |
| 49 | expect(report.completeTransitions).toBe(true) | |
| 50 | expect(report.landingContact).toBe(true) | |
| 51 | expect(report.energy.model).toEqual({ evidence: 'modeled-vehicle-battery-v1', capacityWh: 100, maximumSourceW: 1061 }) | |
| 52 | expect(report.energy.lastObservation.energyUsedWh).toBeGreaterThan(0) | |
| 53 | expect(report.energy.lastObservation.capturedAtMs).toBeLessThan(report.energy.lastObservation.observedAtMs) | |
| 54 | expect(report.energy.lastObservation.cutoff).toBe(false) | |
| 55 | }, 90000) | |
| 56 | ||
| 57 | test('native full-route command preserves partial outcomes and rejects invalid duration', async () => { | |
| 58 | const short = await command(['vtol:simulate', '--seconds', '2']).withTimeout(60000) | |
| 59 | expect(short.exitCode).toBe(1) | |
| 60 | const report = JSON.parse(short.stdout) | |
| 61 | expect(report.status).toBe('duration-exhausted') | |
| 62 | expect(report.demonstrationMatched).toBe(false) | |
| 63 | expect(report.completeTransitions).toBe(false) | |
| 64 | expect(report.landingContact).toBe(false) | |
| 65 | expect(report.mission.state).toBe('running') | |
| 66 | expect(report.boardNeutral).toBe(true) | |
| 67 | expect(report.transitionEvidence.map((row: { taskId: string }) => row.taskId)).toEqual(['initial-hover']) | |
| 68 | for (const seconds of ['NaN', '1', '181', '2.5']) { | |
| 69 | const invalid = await command(['vtol:simulate', '--seconds', seconds]).withTimeout(60000) | |
| 70 | expect(invalid.exitCode).toBe(1) | |
| 71 | expect(invalid.stdout).toBe('') | |
| 72 | } | |
| 73 | }, 90000) | |