also looking at this
feat: add owned VTOL hover development
#76
30 files
+780
-111
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.
| @@ -142,6 +142,10 @@ jobs: | ||
| 142 | 142 | working-directory: ${{ runner.temp }} |
| 143 | 143 | - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone controls:simulate --scenario hover --seconds 3 |
| 144 | 144 | working-directory: ${{ runner.temp }} |
| 145 | - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:hover | |
| 146 | working-directory: ${{ runner.temp }} | |
| 147 | - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone vtol:hover --scenario host-loss | |
| 148 | working-directory: ${{ runner.temp }} | |
| 145 | 149 | - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone authority:simulate --scenario pi-loss --seconds 4 |
| 146 | 150 | working-directory: ${{ runner.temp }} |
| 147 | 151 | - run: $GITHUB_WORKSPACE/storage/runtime-build/cli/drone drivers:bench --driver ardupilot --drop-first-ack |
| @@ -76,6 +76,10 @@ These profiles use owned raw sensors, estimation, mission policy, controls and a | ||
| 76 | 76 | outputs remain inhibited. Read the [shared contract](docs/vehicles.md), [air model](docs/vehicles-air.md), |
| 77 | 77 | [ground model](docs/vehicles-ground.md) and [surface corrections](docs/vehicles-surface.md) and [subsea pressure/DVL inspection](docs/vehicles-subsea.md). |
| 78 | 78 | |
| 79 | `./buddy vtol:hover` separately exercises [VTOL hover development](docs/vehicles-vtol.md): | |
| 80 | vertical takeoff, a nearby hover waypoint and contact-gated landing. Wing transitions, cruise, | |
| 81 | hybrid energy qualification and a selectable VTOL mission workspace remain open. | |
| 82 | ||
| 79 | 83 | ## Simulated carried load |
| 80 | 84 | |
| 81 | 85 | Characterize a centered or forward point mass, a prescribed separation and native link loss through |
| @@ -0,0 +1,27 @@ | ||
| 1 | import { defineCommand } from '@stacksjs/cli' | |
| 2 | import { print, printError } from '@stacksjs/cli/output' | |
| 3 | import { createVtolHoverProfile, vtolHoverScenarios } from '../../resources/functions/drone/vehicles/vtol' | |
| 4 | import { runVehicleSimulation } from '../../resources/functions/drone/vehicles/runtime' | |
| 5 | ||
| 6 | export default defineCommand({ | |
| 7 | name: 'vtol:hover', | |
| 8 | description: 'Exercise the fictional lift-and-cruise airframe in hover only; no wing transition or physical control', | |
| 9 | options: { | |
| 10 | '--scenario <name>': { description: vtolHoverScenarios.join(', '), default: 'hover' }, | |
| 11 | '--seconds <n>': { description: 'Maximum virtual duration, 2..180 seconds', default: '60' }, | |
| 12 | }, | |
| 13 | handle(options) { | |
| 14 | try { | |
| 15 | const result = runVehicleSimulation(createVtolHoverProfile(options.scenario), { scenario: options.scenario, seconds: Number(options.seconds) }) | |
| 16 | const { trace, path, ...report } = result | |
| 17 | const expectedStatus = options.scenario === 'hover' ? 'completed' : options.scenario === 'host-loss' ? 'host-lost' : 'blocked' | |
| 18 | const observedFlight = path.some(row => row.truthPositionM[2] < -1 && row.actuators.slice(0, 4).some(n => n > 0)) | |
| 19 | const boardNeutral = result.board.actuators.every(n => n === 0) && result.board.expiresAtMs === null | |
| 20 | const hoverOnly = path.every(row => row.actuators.slice(4).every(n => n === 0)) | |
| 21 | const demonstrationMatched = result.status === expectedStatus && observedFlight && boardNeutral && hoverOnly | |
| 22 | print(JSON.stringify({ ...report, scope: 'development-hover-only', transitionsImplemented: false, expectedStatus, demonstrationMatched, observedFlight, boardNeutral, hoverOnly, recordedMissionRows: trace.rows.length, evaluatedPathRows: path.length }, (_key, value) => typeof value === 'bigint' ? String(value) : value, 2)) | |
| 23 | if (!demonstrationMatched) process.exitCode = 1 | |
| 24 | } | |
| 25 | catch (error) { printError(error instanceof Error ? error.message : String(error)); process.exitCode = 1 } | |
| 26 | }, | |
| 27 | }) | |
| @@ -50,6 +50,7 @@ import PatrolReplay from '../app/Commands/PatrolReplay' | ||
| 50 | 50 | |
| 51 | 51 | import AntennaBench from '../app/Commands/AntennaBench' |
| 52 | 52 | import AirSimulate from '../app/Commands/AirSimulate' |
| 53 | import VtolHover from '../app/Commands/VtolHover' | |
| 53 | 54 | import GroundSimulate from '../app/Commands/GroundSimulate' |
| 54 | 55 | import SurfaceSimulate from '../app/Commands/SurfaceSimulate' |
| 55 | 56 | import SubseaSimulate from '../app/Commands/SubseaSimulate' |
| @@ -126,6 +127,7 @@ PatrolSimulate(app) | ||
| 126 | 127 | PatrolReplay(app) |
| 127 | 128 | AntennaBench(app) |
| 128 | 129 | AirSimulate(app) |
| 130 | VtolHover(app) | |
| 129 | 131 | GroundSimulate(app) |
| 130 | 132 | SurfaceSimulate(app) |
| 131 | 133 | SubseaSimulate(app) |
| @@ -76,6 +76,7 @@ const config: BunPressOptions = { | ||
| 76 | 76 | items: [ |
| 77 | 77 | { text: 'Owned vehicle runtime', link: '/vehicles' }, |
| 78 | 78 | { text: 'Owned fixed-wing profile', link: '/vehicles-air' }, |
| 79 | { text: 'VTOL hover development', link: '/vehicles-vtol' }, | |
| 79 | 80 | { text: 'Owned ground vehicle profile', link: '/vehicles-ground' }, |
| 80 | 81 | { text: 'Owned surface vessel profile', link: '/vehicles-surface' }, |
| 81 | 82 | { text: 'Owned subsea inspection profile', link: '/vehicles-subsea' }, |
| @@ -56,6 +56,9 @@ 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 hover development command](vehicles-vtol.md) exercises vertical takeoff, | |
| 60 | hover and landing. Front/cruise/back transitions and the common VTOL mission workspace remain open. | |
| 61 | ||
| 59 | 62 | ## Explore a workflow |
| 60 | 63 | |
| 61 | 64 | | Workflow | Current development surface | Guide | |
| @@ -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 | No log/FTP/parameter management | [#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 | No mission upload or mission-mode control | [#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 aerodynamic trainer with runway takeoff/survey/landing and wind/airspeed-loss tests; VTOL, helicopter, soaring, blimp and experimental families remain planned | 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 and [VTOL hover development](../vehicles-vtol.md) with vertical takeoff/hover/contact-gated landing; VTOL transitions/cruise, hybrid energy, helicopter, soaring, blimp and experimental 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) | |
| @@ -0,0 +1,64 @@ | ||
| 1 | # VTOL hover development | |
| 2 | ||
| 3 | The owned lift-and-cruise model can execute a **hover-only** mission through the shared raw sensor, | |
| 4 | estimator, mission, authority and independent output-lease runtime. It takes off vertically, reaches | |
| 5 | a nearby hover waypoint and lands against fresh modeled contact evidence. Wing transitions and | |
| 6 | cruise control are not implemented. This development profile is separate from the selectable | |
| 7 | profiles in the `/missions` workspace. | |
| 8 | ||
| 9 | ```sh | |
| 10 | ./buddy vtol:hover | |
| 11 | ./buddy vtol:hover --scenario host-loss | |
| 12 | ./buddy vtol:hover --scenario imu-loss | |
| 13 | ``` | |
| 14 | ||
| 15 | The native Stacks command defaults to 60 virtual seconds. It accepts `hover`, `imu-loss`, | |
| 16 | `position-loss`, `vehicle-sensor-loss`, `host-loss`, `rc-override` and `kill`. Faults begin at 3000 ms, | |
| 17 | after takeoff. A report retains the actual `completed`, `blocked`, `host-lost` or `duration-exhausted` | |
| 18 | status. Exit 0 means the selected demonstration matched its expected outcome, observed flight, | |
| 19 | retained neutral pusher/surfaces and ended with neutral board outputs. It does not turn a fault | |
| 20 | into a completed mission. An unmatched outcome, invalid option or insufficient duration exits 1. | |
| 21 | ||
| 22 | The copied standalone CLI registers the same command definition: | |
| 23 | ||
| 24 | ```sh | |
| 25 | bun run runtime:cli | |
| 26 | ./storage/runtime-build/cli/drone vtol:hover | |
| 27 | ``` | |
| 28 | ||
| 29 | ## Model and measurements | |
| 30 | ||
| 31 | The fictional 2.5 kg body has four vertical rotors, each limited to 12 N, with 60 ms motor response | |
| 32 | and a 120 N/s slew limit. An independent 14 N pusher and the owned trainer's aerodynamic surfaces | |
| 33 | share the rigid body. Rotor allocation preserves collective thrust first, then roll/pitch demand, | |
| 34 | then yaw. No coefficient is measured from a purchased airframe. | |
| 35 | ||
| 36 | The hover demonstration commands the pusher and three surfaces to zero. Calibrated modeled raw | |
| 37 | IMU/GNSS samples feed the owned estimator. The controller receives estimated pose, a current qualified | |
| 38 | raw gyro capture and delayed contact measurements. Plant truth is used for sensor synthesis and | |
| 39 | independent evaluation, never as controller input. Initial north heading and a flat contact plane | |
| 40 | are declared simulation assumptions. Landing uses a 0.1 m below-plane target to obtain finite | |
| 41 | contact; mission completion still requires estimated arrival, low speed, settling and fresh contact. | |
| 42 | ||
| 43 | Contact and paused-ground neutral decisions occur after estimator/rate validation. A stale gyro | |
| 44 | cannot become acceptable because contact is present. Control faults latch for the controller | |
| 45 | lifetime. Each session owns its plant, controller, sensor sequence and returned actuator arrays. | |
| 46 | RC takeover, kill and sensor loss revoke shared authority. Host death leaves its final host snapshot | |
| 47 | stale while the independently ticking modeled board expires the output lease. | |
| 48 | ||
| 49 | The mission's 50 Wh capacity, 1200 W ceiling and 30% reserve are declared budget inputs for this | |
| 50 | fictional hover experiment. They are not identified electrical power, measured endurance or a | |
| 51 | complete hybrid energy model. Neutral outputs during an airborne fault do not establish a safe | |
| 52 | emergency landing. Physical flight commands remain disabled. | |
| 53 | ||
| 54 | ## Remaining implementation | |
| 55 | ||
| 56 | [Issue #12](https://github.com/stacksjs/drone/issues/12) remains open for explicit correlated | |
| 57 | transition requests and observed outcomes, front/cruise/back control, airspeed and attitude | |
| 58 | qualification, bounded transition abort/recovery, complete hybrid power/reserve accounting and | |
| 59 | the common mission workspace. Full TECS/soaring, tiltrotor/tailsitter mechanisms, helicopters and | |
| 60 | other airframe families remain separate work. Native board control, measured airframe identification, | |
| 61 | HIL and physical flight require their own evidence. | |
| 62 | ||
| 63 | Read the [shared vehicle contract](vehicles.md), [fixed-wing model](vehicles-air.md) and | |
| 64 | [authority contract](authority.md). | |
| @@ -61,7 +61,9 @@ 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. VTOL, helicopter, soaring, blimp and other vehicle |
| 64 | families remain explicit roadmap work. | |
| 64 | families remain explicit roadmap work. A separate [VTOL hover development command](vehicles-vtol.md) | |
| 65 | now exercises vertical takeoff, a nearby hover waypoint and contact-gated landing. It does not | |
| 66 | implement wing transitions or add a VTOL selection to the common workspace. | |
| 65 | 67 | |
| 66 | 68 | Read [air](vehicles-air.md), [ground](vehicles-ground.md), [surface](vehicles-surface.md), [subsea](vehicles-subsea.md), |
| 67 | 69 | [mission semantics](missions.md) and [the replacement matrix](roadmap/feature-coverage.md). |
| @@ -0,0 +1,31 @@ | ||
| 1 | import { expect, test } from 'bun:test' | |
| 2 | import { command } from '@stacksjs/testing' | |
| 3 | ||
| 4 | test('native VTOL hover command preserves nominal and lost-host outcomes with explicit development scope', async () => { | |
| 5 | for (const [scenario, status] of [['hover', 'completed'], ['host-loss', 'host-lost']]) { | |
| 6 | const result = await command(['vtol:hover', '--scenario', scenario!]).withTimeout(60000) | |
| 7 | expect(result.exitCode).toBe(0) | |
| 8 | const report = JSON.parse(result.stdout) | |
| 9 | expect(report.status).toBe(status) | |
| 10 | expect(report.scope).toBe('development-hover-only') | |
| 11 | expect(report.transitionsImplemented).toBe(false) | |
| 12 | expect(report.physicalFlightEnabled).toBe(false) | |
| 13 | expect(report.board.actuators).toEqual([0, 0, 0, 0, 0, 0, 0, 0]) | |
| 14 | expect(report.recordedMissionRows).toBeGreaterThan(100) | |
| 15 | if (scenario === 'host-loss') expect(report.board.fault).toBe('output-lease-expired') | |
| 16 | else expect(report.mission.reason).toBe('landed') | |
| 17 | } | |
| 18 | }, 180000) | |
| 19 | ||
| 20 | test('native VTOL command rejects unknown scenarios and preserves insufficient-duration failure', async () => { | |
| 21 | const invalid = await command(['vtol:hover', '--scenario', 'cruise']).withTimeout(60000) | |
| 22 | expect(invalid.exitCode).toBe(1) | |
| 23 | expect(invalid.stdout).toBe('') | |
| 24 | const short = await command(['vtol:hover', '--seconds', '2']).withTimeout(60000) | |
| 25 | expect(short.exitCode).toBe(1) | |
| 26 | const report = JSON.parse(short.stdout) | |
| 27 | expect(report.status).toBe('duration-exhausted') | |
| 28 | expect(report.demonstrationMatched).toBe(false) | |
| 29 | expect(report.mission.state).toBe('running') | |
| 30 | expect(report.board.actuators).toEqual([0, 0, 0, 0, 0, 0, 0, 0]) | |
| 31 | }, 180000) | |