Skip to main content
Glama

Jarvis

v0.6.20 tagged tip · T12 FOLLOW ★ · T11 arm UX ★ · basic mando + Safety latch + FOLLOW CLOSED · ontology explain @ v0.6.0 · bloque 0.5 historical @ v0.5.44

Deterministic engineering engine for designing physical systems with AI-assisted natural language.

Jarvis is aerial-first (drones and related vehicles): you describe goals and components in Spanish; calculation and simulation stay rule-based and auditable. The model may interpret — it does not invent the physics.

Read the one-page contract: VISION.md.

Fase C (platform scaffold) in plain language: what “scaffold” means and what each new package is for — ARCHITECTURE.md §1a.

Quick start

git clone https://github.com/mcuestamunoz/Jarvis.git
cd Jarvis
python3 -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev,mcp]"

CLI chat (optional: local Ollama for analyze / free-form interpret):

python -m jarvis.main --chat
# or
jarvis --chat

Pizarra (visor 3D + Continuity spatial assembly; huecos de arquitectura = slots, no BOM; mutación = CLI / Continuity / Situar drag):

jarvis board

Tests:

pytest

MCP server (Cursor / MCP clients):

python -m jarvis.adapters.mcp.server

Workspace projects live under workspace/ (override with JARVIS_WORKSPACE_ROOT).
Ollama defaults: JARVIS_OLLAMA_BASE_URL, JARVIS_OLLAMA_MODEL (see src/jarvis/config.py).

Related MCP server: COMSOL MCP Server

What v0.5.30 includes

Fase C · C32 (B1-fase-c-mcu-spi-hal-stub) — the SPI port got a name, still not the gyro:

  • New native/flight_control/include/jarvis/fc/spi.hpp/src/spi.cpp: SpiBytePort — an abstract base with a virtual destructor exposing transfer(tx, rx, n) -> size_t (copies up to n bytes from TX into RX, in order, never blocks) — and LoopbackSpi, the only implementation: RX = TX, default capacity 256.

  • A transfer past capacity returns a short count rather than growing unbounded — same overflow policy LoopbackUart (C28) already uses. Unlike LoopbackUart's own persistent FIFO, LoopbackSpi carries no state between calls — a real SPI transfer is a single synchronous exchange, not a stream.

  • Zero SPI registers, zero CMSIS, zero chip-select/NSS GPIO, zero IRQ/DMA anywhere in either file.

  • The desk FC's own gyro (when later wired) is an ICM42688P on SPI — cited only as desk identity in a comment, never in real code: no WHO_AM_I read, no register map, no sample.

  • dshot.hpp/dshot.cpp/dshot.py (C31), hello_led.h/hello_led.c/stub_main.cpp (C30), and uart.hpp/uart.cpp (C28) all stay byte-identical — idle is still only PC13, no SPI poll in main. SimulatedImuHal (C3) untouched.

  • MCU SPI stub != chip SPI != gyro live != flying. A named SPI byte port exists; an in-memory loopback implements it.

  • Package 0.5.30 · tagged v0.5.30 · suite 3649 · host ctest 60/60 — ★ ACCEPT CLOSED — review

  • Next: C33 CLOSED @ v0.5.31.

What v0.5.31 includes

Fase C · C33 (B1-fase-c-spi-scripted-slave) — a second SpiBytePort: canned RX, not TX echo, still not the gyro:

  • Extends the existing native/flight_control/include/jarvis/fc/spi.hpp/src/spi.cpp (no new file) with ScriptedSpi, a second SpiBytePort implementation — LoopbackSpi (C32) stays byte-behavior-unchanged.

  • ScriptedSpi(canned_rx) fills rx[0..accepted) from a pre-loaded byte script, never from tx — this is how a test pretends "a device answered" without any real chip. set_next_rx(...) reprograms the script for subsequent transfers; each transfer(...) call fills RX from the start of the currently-loaded script (a fixed canned response, not a consuming stream across calls).

  • Same overflow policy as LoopbackSpi/LoopbackUart: a script shorter than the requested n returns a short count, and the untouched tail of rx is left exactly as the caller passed it in.

  • Zero SPI registers, zero CMSIS, zero chip-select/NSS GPIO, zero IRQ/DMA, zero CRSF/ELRS tokens anywhere in native/. WHO_AM_I/ICM42688P appear only in comments (e.g. "a future gyro test could load 0x47 here"), never in real code.

  • stub_main.cpp, hello_led.h/hello_led.c (C30), dshot.hpp/dshot.cpp/dshot.py (C31), and uart.hpp/uart.cpp (C28) all stay byte-identical — no SPI poll in main.

  • Scripted SPI != gyro live != chip SPI != flying. A canned-RX test double exists. Nothing here reads a real device.

  • Package 0.5.31 · tagged v0.5.31 · suite 3667 · host ctest 66/66 — ★ ACCEPT CLOSED — review

  • Next: C34 CLOSED @ v0.5.32.

What v0.5.32 includes

Fase C · C34 (B1-fase-c-spi-scripted-gyro-probe) — a client of SpiBytePort, not a gyro driver:

  • New native/flight_control/include/jarvis/fc/spi_probe.hpp/src/spi_probe.cpp: probe_rx(SpiBytePort& port, uint8_t* rx, size_t n) -> size_t — sends n dummy zero TX bytes (never a register address) and returns whatever port.transfer(...) moves into rx. n == 0 returns 0. spi.hpp/spi.cpp (C32/C33) get zero edits — the port stays the port, spi_probe.* is the first caller.

  • On ScriptedSpi (C33) loaded with a placeholder fixture byte, probe_rx returns that byte — proving the RX path end-to-end. On LoopbackSpi (C32), probe_rx returns all-zeros (the dummy TX echoed back) — proving the client is port-shaped, not tied to one implementation.

  • Same overflow policy as the port itself: a script shorter than n returns a short count, untouched tail of rx left exactly as passed in.

  • Zero SPI registers, zero CMSIS, zero chip-select/NSS GPIO, zero IRQ/DMA, zero CRSF/ELRS tokens anywhere in native/. WHO_AM_I/ICM42688P/register address 0x75 do not appear anywhere in spi_probe.*, not even in a comment — the fixture byte itself lives in tests only, never in library code.

  • stub_main.cpp, hello_led.h/hello_led.c (C30), dshot.hpp/dshot.cpp/dshot.py (C31), uart.hpp/uart.cpp (C28), and loop.hpp/loop.cpp/loop.py (C3/C4) all stay byte-identical — no probe poll in main, no IMU-into-step wiring.

  • Scripted gyro probe != gyro live != chip SPI != WHO_AM_I != flying. A byte-probe client exists. Nothing here reads a real device or claims an identity register.

  • Package 0.5.32 · tagged v0.5.32 · suite 3679 · host ctest 72/72 — ★ ACCEPT CLOSED — review

  • Next: Taller CSS B1-geometry-taller-css-cuboid-faces READY — six faces on a thin plate (visor, not CAD). Cola: standoff points. DShot wire / C30 DFU parked until bench.

What v0.5.33 includes

Fase C · C35 (B1-fase-c-step-failsafe-hold-ticks) — tests only: many ticks, still not flying:

  • No production code changed. loop.py/loop.hpp/loop.cpp, rc_hold.hpp/rc_hold.cpp, crsf_failsafe.py, and spi_probe.hpp/spi_probe.cpp are all byte-unchanged — this Buy only chains the EXISTING ControlLoop::step/FlightControlLoop.step and RcHoldWatch/CrsfRcHoldWatch + failsafe_loop_inputs more times, and in a new combination.

  • New tests (Python + 4 new Catch2 cases in test_loop.cpp): 1000 step() ticks with a canned level IMU sample (accel ≈ (0,0,-9.81), gyro zeros) and no plant — every tick's four motor forces stay finite and in [0, 1].

  • A stale hold watch (never noted, or past the 0.5 s timeout) feeds failsafe_loop_inputs(t) into that same step() — collective passed in is 0, forces still finite and in [0, 1]. No EscOutput/GPIO call anywhere in the new tests.

  • "Hold" means feeding level_setpoint for those N ticks, not AutonomyVerb.HOLD reaching execution — submit_command(HOLD) still returns execution="not_attempted" under the default RejectAllSafetyGate, re-asserted explicitly.

  • probe_rx (C34) is not used as an IMU source anywhere in these tests — the canned IMU sample is a plain struct literal, same as every prior Buy on this axis.

  • Many ticks != flying != 6-DoF. Failsafe -> step != motors cut != HOLD executed. A thousand ticks of step() on canned IMU, and a stale-RC path into the same step(), both exist. Nothing here is a flying plant, a motor cut, or an executed autonomy command.

  • Package 0.5.33 · tagged v0.5.33 · suite 3691 · host ctest 76/76 — ★ ACCEPT CLOSED — review

  • Next: Taller CSS cylinder faces ★ ACCEPT CLOSED @ v0.5.35. D2 docs LANDED @ package 0.5.36 (awaiting Cursor review + Engineer ★ ACCEPT, no tag yet). Cola: standoff points. DShot wire / C30 DFU parked until bench.

What v0.5.34 includes

Geometry / visor · B1-geometry-taller-css-cuboid-faces — six box faces stop exploding on a thin plate, still not CAD:

  • UI/CSS visor fix, not a Fase C flight-software Buy. New pure helper ui/spatial-board/src/cuboidFaces.ts — cuboidFaceLayout(w, d, h) — returns six {name, width, height, left, top, transform} entries; Solid3D.tsx's box branch now maps them onto the six .sb-solid__face nodes instead of hardcoding six transform strings inline.

  • Root cause: each face sat at the .sb-solid__face CSS default left: 0; top: 0. front/back match the wrapper's own w x h size so they were never wrong, but left/right (width d) and top/bottom (height d) do not — with transform-origin: 50% 50% (never overridden), an off-center face rotates about the wrong point and swings out, worst on a thin plate.

  • Fix: center every face first (left: (w-fw)/2, top: (h-fh)/2), then rotate, then translateZ(half-extent along that face's own normal). Still exactly six .sb-solid__face nodes — no seventh, no CAD, no fit verdict.

  • Verified against the MY5 top-plate fixture (161x42x2mm, pxPerMm 0.5 -> 80.5x21x1px): top/bottom center to translateZ(0.5px), left/right to translateZ(40.25px), and no face's transform string contains translateX/translateY — centering lives entirely in left/top, never in the transform.

  • Cylinder/disk branches in Solid3D.tsx, the projector, the geometry DTO, and spatial_board.py all stay byte-unchanged — git diff --stat empty.

  • Visor cuboid != CAD != fit != extra parts != a 2D card's own origin. Six CSS faces that meet on a thin declared box exist. Nothing here is a machined plate, a fit verdict, or a seventh part.

  • Package 0.5.34 · tagged v0.5.34 · UI vitest 136/136 · tsc --noEmit clean · Python suite 3691 (unchanged — no new Python tests) — ★ ACCEPT CLOSED (Engineer Taller smoke 2026-09-25) — review

  • Next: Taller CSS cylinder faces ★ ACCEPT CLOSED @ v0.5.35. D2 docs B1-docs-truth-sync-after-c35 READY. Cola: standoff points.

What v0.5.35 includes

Geometry / visor · B1-geometry-taller-css-cylinder-faces — caps + 16 slats stop exploding on a short/tall cylinder, still not CAD:

  • UI/CSS visor fix, not a Fase C flight-software Buy — same fix, same class of bug, as the cuboid Buy that shipped @ v0.5.34. New pure helper ui/spatial-board/src/cylinderFaces.ts — cylinderSolidLayout(diameterPx, heightPx) — returns {caps, slats}: 2 caps and 16 side slats, each {width, height, left, top, transform}; Solid3D.tsx's cylinder branch now maps them onto the .sb-solid__disk-face/.sb-solid__face nodes instead of hardcoding the transforms inline.

  • Root cause: each D x D cap sat at the CSS default left: 0; top: 0 on a D x H wrapper — correct only when H == D. With transform-origin: 50% 50% (never overridden), a short prop hub (H << D) or a tall standoff-shaped post (H >> D) rotates each cap about the wrong point and it swings off axis. The 16 side slats already re-centered horizontally before this Buy — that part was already correct and is reused unchanged, just relocated into the new helper.

  • Fix: center each cap first (left: 0, top: (H-D)/2 — negative when H < D), then rotate, then translateZ(H/2) — never translateY again after centering. Still exactly 2 caps and 16 slats (N frozen) — no 17th face, no invented diameter or height.

  • Verified against a short-hub fixture (Ø130.72 x 6.8mm -> px 65.36x3.4, cap top=-30.98, translateZ(1.7px)) and a tall-post fixture (Ø6 x 30mm -> px 3x15, cap top=6, translateZ(7.5px)) — both directions of the bug, not just the thin-plate case. No cap or slat transform string contains translateX/translateY.

  • cuboidFaces.ts (the box helper), the disk branch, SCENE3D.pxPerMm, the projector, the geometry DTO, and spatial_board.py all stay byte-unchanged — git diff --stat empty on every one of them.

  • Visor cylinder != CAD != fit != extra parts != round metal != a standoff hole pattern. 2 CSS caps + 16 CSS slats that meet on a declared Ø x H exist. Nothing here is a turned standoff, a prop hub from the mill, or a fit verdict.

  • Package 0.5.35 · tagged v0.5.35 · UI vitest 142/142 · tsc --noEmit clean · Python suite 3691 (unchanged — no new Python tests) — ★ ACCEPT CLOSED (Engineer Taller smoke 2026-09-25) — review

  • Next: D2 docs LANDED @ package 0.5.36 (awaiting Cursor review + Engineer ★ ACCEPT, no v0.5.36 tag yet) — B1-docs-truth-sync-after-c35 · report. Cola: standoff points.

What v0.5.36 includes (landed, awaiting Cursor review + Engineer ★ ACCEPT — no v0.5.36 tag yet)

Docs · B1-docs-truth-sync-after-c35 — maps and READMEs now name the same tip as ARCHITECTURE.md:

  • Docs-only Buy — no src/, ui/, or library/ product edits. A Phase 0 inventory (.jes/artifacts/inventory_docs_truth_sync_after_c35_b0.md, one row per in-scope file) was written and gated before any Phase 1 edit, per the IC's own STOP-before-bulk-edits rule.

  • docs/system_map/README.md and JARVIS_SYSTEM_MAP.md's own "Fase C" section stopped naming tip v0.5.3/suite 3236 (a snapshot from C1-C5, 2026-09-20) as current — both now name tip v0.5.35, live suite/UI/ctest counts, and point at ARCHITECTURE.md §1c / PLATFORM_CAPABILITY_VISION.md §13.

  • CONNECTIONS.md gets one new changelog paragraph (C6-C35 + Taller CSS, no new C-xxx) after its existing C1-C5 entry — the canonical registry table itself is untouched, still ending at C-113.

  • The interactive canvas (jarvis-system-map.canvas.tsx) and DIAGRAMS.md both stopped implying "no C++/CMake tree" or "C++ (future IC)" as a current fact — a C++ host+MCU-cross-compile tree has existed under native/flight_control/ since C13. No fake SPI/step graph nodes were added — this is prose-only.

  • native/flight_control/README.md's own layout list now names dshot.hpp, spi.hpp, spi_probe.hpp, and their test files (previously stopped at C28's uart.hpp); C30's two "not yet tagged" mentions are corrected to "★ ACCEPT CLOSED @ tag v0.5.28" (the tag exists — git tag -l v0.5.28); new honesty paragraphs for C31-C35 were added matching this README's own established per-Buy style.

  • docs/USER_GUIDE_CRAFT_MONTAGE.md §8.4 gets one short Spanish paragraph: a declared box is one six-face prism, a declared cylinder is one body (2 caps + 16 slats), a thin plate or short hub is still one solid — not extra parts, not CAD, not a fit verdict. No MY5 mm, no standoff ×8 anywhere in it.

  • docs/system_map/00_entry/ENTRY_MAP.md's visor projector row gets one clause naming Taller CSS cuboid + cylinder @ v0.5.35, still C-094/C-113 class, no new C-xxx.

  • Left untouched, verified clean: VISION.md, docs/PROJECT_CONTINUITY.md, docs/ENGINEERING_READINESS_VISION.md, docs/BUGS.md/FASE_LLM.md/CODE_AUDIT_CORE.md (already carry HISTORICAL banners), docs/IMPLEMENTATION_TASKS.md's closed archive sections, ARCHITECTURE.md §1c's own historical C3 quote, and 22 other docs/*.md files with zero stale Fase C references (grep-verified, listed in the inventory).

  • Docs tip != flying != gyro live != DShot pin != new C-xxx != standoff ×8 shipped. Maps and READMEs that name the same tagged tip as ARCHITECTURE.md exist. Nothing here is a new craft connection, firmware on the desk, or the MY5/standoff catalog claimed as shipped.

  • Package 0.5.36 · Python suite 3691 (unchanged) · UI vitest 142/142 · host ctest 76/76 — landed, awaiting Cursor review + Engineer ★ ACCEPT, no v0.5.36 tag yet — report

  • Next: awaiting Engineer ★ ACCEPT on this docs Buy. Cola: standoff points.

What v0.5.37 includes

Fase C · C36 (B1-fase-c-sim-6dof-plant) — a toy body that can move through space, not just tilt:

  • New ToyQuad6DofPlant (Python plant.py + C++ plant.hpp/plant.cpp) beside unchanged ToyQuadAttitudePlant (C11) — purely additive; C11 tilt-recovery smoke still green.

  • Translation: thrust_body = (0,0,thrust_gain*sum(forces)) · a_world = R(q)·(thrust/mass)+g · semi-implicit Euler · ENU g=(0,0,-9.81).

  • ImuSample stays C11-shaped (gravity in body + gyro — no specific force). Pose on true_position_m / true_velocity_mps only. Plant stays outside loop.step.

  • 6-DoF toy ≠ flying ≠ product aero ≠ MY5 truth.

  • Package 0.5.37 · tagged v0.5.37 · suite 3696 · host ctest 82/82 — ★ ACCEPT CLOSED — review

  • Next: C37 ★ ACCEPT CLOSED @ v0.5.38.

What v0.5.38 includes

Fase C · C37 (B1-fase-c-mag-yaw-rung) — yaw stops being only gyro drift:

  • MagSample + SimulatedMagHal (Py+C++) — caller-supplied true q; toy horizontal world field; HAL never owns a plant.

  • ComplementaryAttitudeEstimator.update(sample, mag=None) — mag optional; mag=None ≡ C7; yaw-only world-frame correction when present.

  • RC_CH_YAW unlocked (RC_MAX_YAW_RAD=π); roll/pitch/throttle unchanged. Mag fusion outside loop.step.

  • Sim mag ≠ live mag ≠ flying.

  • Package 0.5.38 · tagged v0.5.38 · suite 3706 · host ctest 88/88 — ★ ACCEPT CLOSED — review

  • Next: C38 ★ ACCEPT CLOSED @ v0.5.39. Cola: C39 position loop (sim). Assistant PARKED. Silicon parked.

What v0.5.39 includes (★ ACCEPT CLOSED)

Fase C · C38 (B1-fase-c-altitude-loop) — collective stops being only the RC stick or a fixed constant:

  • New AltitudeSample + SimulatedAltitudeHal (Python sim_altitude_hal.py + C++ sim_altitude_hal.hpp/.cpp) — a direct altitude port, not a fake ISA pressure/meteorology model: read_altitude(true_z_m, t_s) returns the caller-supplied true height directly. This HAL never owns or secretly consults a plant.

  • New AltitudeController (Python altitude_controller.py + C++ altitude_controller.hpp/.cpp) — one law only, run outside FlightControlLoop.step: collective = clip(hover_bias + kp*(z_des_m - altitude_m) - kd*vz_mps, 0, 1). hover_bias defaults to the toy hover point that actually balances ToyQuad6DofPlant's own default gravity/thrust constants (mass_kg*g/(4*thrust_gain) ≈ 0.1226) — not the IC's suggested hover_collective() (0.5), which was verified empirically to badly mismatch this plant's own physics and never settle near the setpoint. vz_mps is caller-supplied (the smoke passes ToyQuad6DofPlant.true_velocity_mps[2] directly), not internally integrated.

  • Deliberately named sim_altitude_hal.py/altitude_controller.py, not altitude.py — this codebase already ships attitude.py (the C7 estimator); "altitude"/"attitude" differ by one letter, avoided the same way in file names, class names (no AltitudeSetpoint next to the existing AttitudeSetpoint — a plain z_des_m float is used instead), and the C++ headers.

  • New smoke run_altitude_loop_smoke chains alt.compute(...) → loop.step(...) → plant.step(...) exactly as this Buy's own IC requires — starting at z=0 with z_des_m=2.0, verified converging monotonically (no overshoot) to within 0.07m of the setpoint over 500 steps (5 seconds sim time).

  • Sim altitude != live baro/ToF chip. z→collective in RAM != altitude hold in air. A simulated height measurement drives thrust so the toy plant moves in z. Nothing here reads a real sensor or claims any real vehicle holds altitude.

  • Package 0.5.39 · tagged v0.5.39 · suite 3714 · host ctest 93/93 — ★ ACCEPT CLOSED — review

  • Next: C39 ★ ACCEPT CLOSED @ v0.5.40. Cola: C40 autonomy executor (sim). Assistant PARKED. Silicon parked.

What v0.5.40 includes (★ ACCEPT CLOSED)

Fase C · C39 (B1-fase-c-position-loop) — horizontal motion stops being only open-loop tilt or luck:

  • New PositionSample + SimulatedPositionHal (Python sim_position_hal.py + C++ sim_position_hal.hpp/.cpp) — a direct ENU port, not a NMEA/WGS84/optical-flow stack: read_position(true_x_m, true_y_m, t_s) returns the caller-supplied true horizontal position (East/North) directly. Optional z_m allowed, unused by the controller. This HAL never owns or secretly consults a plant.

  • New PositionSetpoint(x_m, y_m) + PositionController (Python position_controller.py + C++ position_controller.hpp/.cpp) — one law only, run outside FlightControlLoop.step: pitch_rad = clip(kp*(x_des-x) - kd*vx, -max_tilt_rad, max_tilt_rad), roll_rad = clip(-(kp*(y_des-y) - kd*vy), -max_tilt_rad, max_tilt_rad), composed into an AttitudeSetpoint quaternion with yaw held at 0. max_tilt_rad defaults to RC_MAX_TILT_RAD (pi/6, 30 degrees, C25).

  • Signs derived and proven: a positive pitch rotates body +Z thrust onto positive world X (East) — no sign flip on the pitch term; a positive roll rotates body +Z thrust onto negative world Y (South) — hence the sign flip on the roll term. Verified with a direct closed-loop test: East-only setpoint moves true_position_m[0] up, North-only setpoint moves true_position_m[1] up, with no cross-axis coupling.

  • No naming-collision concern this Buy (unlike C38's altitude/attitude) — PositionSetpoint/PositionController/PositionSample/SimulatedPositionHal are used directly, typed, per the IC's own preference.

  • New smoke run_position_loop_smoke chains pos.compute(...) → alt.compute(...) (C38's own AltitudeController, unchanged) → loop.step(...) → plant.step(...) — starting at (0,0,0) with x_des_m=5.0, y_des_m=0.0, z_des_m=2.0, verified horizontal distance shrinking from 5.0m to under 0.25m over 1000 steps (10 seconds sim time).

  • Sim position != live GPS/flow chip. xy→tilt in RAM != position hold in air != GO_TO executed. An ENU point in RAM != a house map. Nothing here reads a real sensor or claims any real vehicle holds position, and this Buy does not implement the AutonomyVerb.GO_TO executor (that stays C40, a separate, future Buy) — propose_command(GO_TO) still hits RejectAllSafetyGate.

  • Package 0.5.40 · tagged v0.5.40 · suite 3723 · host ctest 101/101 — ★ ACCEPT CLOSED — review

  • Next: C40 ★ ACCEPT CLOSED @ v0.5.41. Cola: C41 safety-sim policy. Assistant PARKED. Silicon parked.

What v0.5.41 includes (★ ACCEPT CLOSED)

Fase C · C40 (B1-fase-c-autonomy-executor) — autonomy verbs stop being only labels that RejectAll:

  • New SimAutonomyExecutor (Python flight_software/autonomy/sim_executor.py + C++ sim_autonomy_executor.hpp/.cpp) — a separate, sim-only driver, not a change to the C4 command surface: propose_command/submit_command/AutonomySubmissionResult are completely untouched, and execution is never set to "executed" anywhere in this module.

  • Reuses, never reimplements: every tick(verb, params, dt_s) call chains PositionController.compute (C39) → AltitudeController.compute (C38) → FlightControlLoop.step (C24) → plant.step (C36) — the same four-call chain run_position_loop_smoke already used, now a stateful per-verb driver.

  • HOLD freezes a PositionSetpoint at the current (or caller-supplied) xy + z the first tick it's ticked, then holds that frozen point every subsequent tick. GO_TO requires finite x_m/y_m; z_m optional, defaulting to the altitude frozen at the first GO_TO tick (not a new invented cruise-altitude constant). LAND freezes xy the same way HOLD does and ratchets z_des_m downward by a documented land_rate_mps (default 0.5 m/s) toward a documented floor z_land_m (default 0.0) — not a claim of touchdown gear. Any other verb raises.

  • Respects C39 N2 (z may droop under tilt): tests do not require altitude glued to setpoint during an aggressive GO_TO chase; HOLD asserts both xy and z stay within a documented bound only after settling; LAND asserts z strictly decreases toward the floor.

  • Verified: GO_TO shrinks horizontal distance to (3.0, 0.0) from 3.0m to under 0.1m over 1500 steps (with z_m=2.0 also converging); HOLD (after that convergence) keeps a 500-tick window within 0.5m on every axis; LAND (from a converged z≈2.0) brings z under 0.5m over 1000 steps.

  • verb -> setpoints in RAM != execute on copper. Sim HOLD/LAND/GO_TO != flying != Safety allow. propose_command/submit_command through the default RejectAllSafetyGate still returns reject/not_attempted for all three verbs — this Buy does not touch Safety (that's C41).

  • Package 0.5.41 · tagged v0.5.41 · suite 3732 · host ctest 105/105 — ★ ACCEPT CLOSED — review

  • Next: C41 ★ ACCEPT CLOSED @ v0.5.42. Cola: C42 ICM client. Assistant PARKED. Silicon parked.

What v0.5.42 includes (★ ACCEPT CLOSED)

Fase C · C41 (B1-fase-c-safety-sim-policy) — Safety can say yes to the same three verbs the sim executor understands:

  • ArmedAllowlistSafetyGate (C17) widened, not replaced: its allow-list grows from {HOLD, LAND} to {HOLD, LAND, GO_TO} — one gate class, still opt-in, still starting disarmed, still parsing autonomy:{verb}:{id}. No second, competing SimAutonomyAllowlistSafetyGate was added — the IC's own "prefer one clear gate story" default.

  • default_safety_gate() is unchanged — still always RejectAllSafetyGate. No AllowAllSafetyGate exists anywhere under src/.

  • Allow still never means execute: submit_command's allow branch stays execution="not_implemented" for all three verbs — this gate is never called from SimAutonomyExecutor.tick (C40), and that executor is never called from submit_command or from safety.py. The two APIs stay entirely separate call paths.

  • AuthoritySignal still never flips allow — re-verified for GO_TO specifically.

  • C17's own pre-existing tests were disclosed-retargeted (not weakened): GO_TO moved from the "rejected" list to the "allowed" list in tests/test_fase_c_safety_real_policy_b1.py, since the allow-list itself changed by design.

  • allow != execute != flying. sim allowlist != copper arm != motors. GO_TO allow != GO_TO in air != SimAutonomyExecutor.tick.

  • Package 0.5.42 · tagged v0.5.42 · suite 3741 — ★ ACCEPT CLOSED — review

  • Next: C42 ★ ACCEPT CLOSED @ v0.5.43. Cola: C43 craft↔FS bind. Assistant PARKED until C43 CLOSED. Silicon parked.

What v0.5.43 includes (★ ACCEPT CLOSED)

Fase C · C42 (B1-fase-c-icm-register-client) — the placeholder probe becomes a named device transaction:

  • New read_who_am_i(SpiBytePort&) -> WhoAmIResult (C++ only, native/flight_control/include/jarvis/fc/icm42688p.hpp + .cpp) — a second, named client of SpiBytePort sitting beside probe_rx (C34, kept, unmodified).

  • Datasheet citation: TDK InvenSense ICM-42688-P (DS-000347) — WHO_AM_I register at address 0x75, expected value 0x47. Verified via web search cross-referenced against the open-source PX4-Autopilot driver's own register header, since every direct datasheet PDF fetch attempted this session returned HTTP 403 — that corroboration path is disclosed in the header comment rather than a fabricated page/table citation.

  • Transaction: the standard InvenSense-family 2-byte full-duplex read — TX[0] = 0x75 | 0x80, TX[1] = dummy, RX[1] = value — sent through SpiBytePort::transfer, no bypass (verified with a recording test double, not just a real-port smoke).

  • WhoAmIResult{value, matches_expected, bytes_transferred} — a short transfer (exhausted ScriptedSpi fixture) is always a documented mismatch, never a silent success; LoopbackSpi (which only ever echoes TX) is verified to never falsely claim a match.

  • No second register was added — a disclosed scope choice (one well-corroborated register beats a second, weakly-sourced one), not a silent omission.

  • This is a C++-native Buy, same axis as C32-C34 — no Python SpiBytePort/client exists or was added; tests/test_fase_c_icm_register_client_b1.py's own checks are structural (freeze-diffs, forbidden-token greps, CMake wiring), matching C34's own established pattern.

  • ICM register client on ScriptedSpi != chip SPI1. WHO_AM_I in RAM != gyro live != samples in step. Datasheet cite != lab measurement on copper.

  • Package 0.5.43 · tagged v0.5.43 · suite 3750 · host ctest 111/111 — ★ ACCEPT CLOSED — review

  • Next: C43 ★ ACCEPT CLOSED @ v0.5.44. Software-month CLOSED. Assistant DC discuss.

What v0.5.44 includes (★ ACCEPT CLOSED)

Fase C · C43 (B1-fase-c-craft-fs-bind) — the software-month tip can finally name which craft it's about:

  • New bind_profile_to_craft_identity(profile, craft_sku, library=None) -> BoundVehicleProfile (src/jarvis/vehicle_profiles/bind.py) — a one-way, read-only bind: vehicle_profiles reads craft identity via ComponentLibrary (jarvis.knowledge.library, the existing sole reader of library/ JSON); craft packages (core/adapters/Continuity/CLI/Board) still never import jarvis.flight_software/jarvis.vehicle_profiles — a directed seam, not a two-way bridge.

  • BoundVehicleProfile is a small wrapper type (profile + craft_sku/craft_manufacturer/craft_model/craft_size_class_inch), not an extension of VehicleProfile itself — the C3 schema (extra="forbid", pinned by 40+ Buys' own fixtures) stays completely untouched; load_smoke_profile()/smoke_quad_hal_imu are unaffected.

  • New fixture this_quad.json (same C3-shaped declaration fields as smoke_quad_hal_imu.json — id/display_name/vehicle_class/rung/notes, no geometry/mass of its own) binds, via the new helper, to the desk's own hglrc_my5_5in catalog frame (library/frames/_datos.json) — verified mirroring that row's manufacturer="HGLRC"/model="MY5"/size_class_inch=5.0 verbatim.

  • An unknown SKU raises KeyError — the same error ComponentLibrary.get_frame already raises, not a second, competing "not found" type — never a silent empty bind.

  • Read-only, verified: the bind never writes library/frames/_datos.json (mtime/content unchanged across a bind call) or any craft workspace file.

  • Profile reads craft != Continuity drives firmware. Craft identity in RAM != flash != arm != flying. Directed seam != craft imports flight_software. No Continuity turn was added that flashes, arms, or submits into flight_software; submit_command/propose_command are never imported by bind.py.

  • Package 0.5.44 · tagged v0.5.44 · suite 3759 — ★ ACCEPT CLOSED — review

  • Next: after ★ ACCEPT, this closes the software-month arc — Assistant design-contract discussion may begin (still needs its own ★ to implement). Silicon parked.

What v0.5.29 includes

Fase C · C31 (B1-fase-c-dshot-encode-stub) — the DShot 16-bit frame, in RAM, still not a pin:

  • New src/jarvis/flight_software/flight_control/dshot.py + native/flight_control/include/jarvis/fc/dshot.hpp/src/dshot.cpp: encode_dshot_frame(throttle, telemetry=False) -> uint16 — value = (throttle << 1) | telemetry, checksum = (value ^ (value>>4) ^ (value>>8)) & 0xF, frame = (value << 4) | checksum, throttle a required [0, 2047] integer (out of range raises a typed error).

  • Vectors verified in both languages: throttle=0 -> 0x0000, throttle=48 -> 0x0606, throttle=2047 -> 0xFFEE.

  • Special range documented, not implemented: DShot's own protocol reserves 0..47 as commands (beep, 3D, etc.) — this module encodes the 11-bit field as given, no command table.

  • Optional encode_motor_forces_dshot(forces) -> 4x uint16 linearly maps force [0,1] onto throttle 48..2047 (never 0..2047) — a parallel path; encode_motor_forces (PWM-µs, C10/C14) and EscOutput/SimulatedEscSink (C26) both stay byte-unchanged.

  • mcu/hello_led.h/hello_led.c/stub_main.cpp (C30) stay byte-identical — idle is still PC13, not DShot.

  • DShot150/300/600 appear only as cited protocol names in comments, never as a claimed timer period or GPIO toggle rate.

  • DShot encode != pin != motors != flying. A 16-bit DShot packet computed in software exists. Nothing here makes an ESC see a waveform.

  • Package / tag v0.5.29 · suite 3635 · host ctest 55/55 — review

  • Next: C32 CLOSED @ v0.5.30. C33 B1-fase-c-spi-scripted-slave READY.

What v0.5.28 includes

Fase C · C30 (B1-fase-c-mcu-flash-observable) — a DFU-able LED image exists; it has not been seen on the desk:

  • Closes the two residuals C29 B1 left uncorrected in code: (a) stub_main's idle loop was an empty while (true) — a successful Reset_Handler looked identical to a dead board; (b) CMake had no dependency on linker_cortex_m4.ld, so editing it never triggered a relink (C29's own test workaround deleted the .elf first — not a real fix).

  • (a) New native/flight_control/mcu/hello_led.h/hello_led.c: bare volatile MMIO (no CMSIS, no HAL) toggling PC13, cited from Betaflight's own unified target for this FC (HGLR-HGLRCF405V2.config, resource LED 1 C13) — deliberately not PA8 (that target's own resource MOTOR 6 A08) and not PB1 (LED_STRIP). Register addresses cited from RM0090, transcribed by hand. The busy-wait between toggles is explicitly uncalibrated — a visible flicker at reset-default HSI 16 MHz, never a claimed millisecond period.

  • (b) set_property(TARGET fc_mcu_stub.elf APPEND PROPERTY LINK_DEPENDS .../linker_cortex_m4.ld) in CMakeLists.txt — verified by touching the .ld and rebuilding without deleting the prior .elf: the .elf's mtime advances, confirming a genuine relink.

  • A POST_BUILD step produces fc_mcu_stub.bin (load address 0x08000000, unchanged from C29) for USB DFU — documented in native/flight_control/README.md alongside the restore procedure (reflash target HGLRCF405V2 from Betaflight Configurator) and the props-off/battery-off warning.

  • uart.hpp/crsf_serial.py stay byte-identical; startup_cortex_m4.c/syscalls_stub.c/C16's own toolchain flags too. No NVIC/EXTI/IRQ/DMA, no motor-pin write anywhere.

  • Flashed LED blink != flying != DShot != USART live != Betaflight HGLRCF405V2. An image the Engineer can DFU onto the desk F405 whose idle loop toggles the cited status LED now exists; CMake will relink if the linker script changes.

  • Package / tag v0.5.28 · suite 3619 · host ctest 51/51 — review. Report §11: not flashed on desk.

  • Next: C31 B1-fase-c-dshot-encode-stub READY (DShot frame in RAM, not pin). C30 DFU smoke parked until bench.

What v0.5.27 includes

Fase C · C29 B1 (B1-fase-c-silicon-cited-flash-map) — the linker now cites the desk MCU's own datasheet, still not flashed:

  • C29 B0 (investigation) recommended parking until the Engineer named an MCU — on 2026-09-24 they did: desk stack HGLRC F460 6S V1, FC SKU HGLRC F405 8S V1, MCU line STM32F405 (printed in that FC's own manual). This B1 replaces C18's disclosed fiction with that citation.

  • native/flight_control/mcu/linker_cortex_m4.ld now uses FLASH 1024K at 0x08000000 / RAM 128K at 0x20000000 (SRAM1+SRAM2 contiguous) — taken from ST RM0090 Table 3 (STM32F405xx/07xx, "Memory map"), not from the HGLRC manual itself (which names the MCU line but prints no ORIGIN/LENGTH numbers).

  • CCM RAM (0x10000000, 64 KiB per RM0090) is deliberately excluded from the MEMORY block — folding it into a flat RAM region would be its own undisclosed simplification.

  • The linker's own honesty comment cites both the desk identity (HGLRC) and the map source (RM0090), plus the disclosed residual: the HGLRC manual doesn't print the STM32F405's exact order-code suffix, but RM0090 Table 3's figures apply to the whole xx/07xx line regardless.

  • mcu/stub_main.cpp was relinked and verified (readelf -l: VirtAddr 0x08000000, entry point 0x8000045) but stays byte-unchanged — no new peripheral was touched. cmake/toolchains/arm-none-eabi.cmake (C16's own CPU flags) also stays byte-unchanged.

  • No CMSIS, no STM32Cube, no OpenOCD/J-Link anywhere in the touched files.

  • Cited FLASH map != flashed != boots on FC != Betaflight HGLRCF405V2. The linker now uses ST's own published addresses for this MCU, but that remains a compile/link-time fact on the Mac, never a claim about the HGLRC stack itself.

  • Package / tag v0.5.27 · suite 3598 · host ctest 51/51 — review

  • Next: Engineer pick among parked axes — board flash · GPIO/DShot wire · craft↔FS

What v0.5.26 includes

Fase C · C28 (B1-fase-c-mcu-uart-hal-stub) — the MCU side got a UART-shaped port, still not a chip peripheral:

  • New native/flight_control/include/jarvis/fc/uart.hpp + src/uart.cpp: UartBytePort — an abstract base with a virtual destructor exposing read(dst, n)/write(src, n) (neither blocks nor throws on a full/empty port) — and LoopbackUart, the only implementation: an in-memory FIFO (default capacity 256).

  • A write past remaining capacity returns a short count (refuses the extra bytes) rather than growing unbounded or overwriting already-queued bytes — a chosen, tested overflow policy.

  • Zero USART registers, zero CMSIS, zero IOSSIOSPEED/termios, zero IRQ/DMA anywhere in either file — grep-verified in real code.

  • mcu/stub_main.cpp still does not reference UartBytePort/LoopbackUart — no UART poll loop was added to Reset_Handler/main; the .elf still links against the ARM toolchain when present.

  • capabilities/crsf_serial.py (Mac host serial, C22/C23) stays byte-identical — this Buy is C++-only on the MCU tree, no Python UART driver added.

  • The C21-C27 lock of zero CRSF/ELRS mentions anywhere under native/, even in comments, holds — re-verified tree-wide (proactive grep on the new header; this Buy ID contains no protocol tokens, so no rewrite was required).

  • MCU UART stub != chip USART != Darwin baud != live ELRS. A named byte port exists; an in-memory loopback implements it. Nothing here is a USART talking to a receiver, the Mac's own IOSSIOSPEED moved onto the chip, or ExpressLRS running on the MCU.

  • Package / tag v0.5.26 · suite 3584 · host ctest 51/51 — review

  • Next (one front at a time): C29 B1 silicon + cited FLASH map — IC

What v0.5.25 includes

Fase C · C27 (B1-fase-c-crsf-stream-timeout-failsafe) — stale sticks stop being treated as live, still not a motor cut:

  • New src/jarvis/capabilities/crsf_failsafe.py — a sixth separate module (never folded into radio.py; git diff confirms radio.py/crsf_stub.py/crsf_dual_role.py/crsf_stream.py/crsf_serial.py/intent.py/safety.py all byte-unchanged).

  • CrsfRcHoldWatch is an age watch, not a parser: note_rc(now_s) records the last time a valid 0x16 frame arrived; evaluate(now_s)/is_stale(now_s) compare that against timeout_s (default 0.5 s, illustrative, not sourced from any real ExpressLRS product spec) — age_s <= timeout_s is fresh, age_s > timeout_s is stale, never-noted is stale with reason="never".

  • Every method takes now_s as a caller-supplied argument — this module never calls time.time() as its own source of truth. A now_s earlier than the last noted time raises a typed ValueError.

  • failsafe_loop_inputs(t_s) returns C8's own level_setpoint(t_s) plus collective=0.0 — C25's own RcLoopInputs reused unchanged — and never calls FlightControlLoop.step, EscOutput.apply_forces, or any SafetyGate.evaluate.

  • The optional feed_and_note_rc(...) helper calls C21's own assembler.feed(data) unchanged, notes only on a completed 0x16 frame, and never calls ingest_stream_bytes — C21's own default helper stays untouched.

  • C++ twin: native/flight_control/include/jarvis/fc/rc_hold.hpp + src/rc_hold.cpp, added to jarvis_fc, deliberately protocol-agnostic — no radio-link protocol name anywhere in that tree, even in comments (same C21-C26 lock, re-verified tree-wide).

  • C20's CrsfDualRolePolicy and C25's map_rc_to_loop_inputs are untouched and not imported here.

  • Timeout failsafe != motors cut != live ELRS != Safety allow. An age watch exists; after 0.5 s without a noted RC sample, sticks are no longer treated as live; the recommended inputs are level attitude plus zero collective.

  • Package / tag v0.5.25 · suite 3572 · host ctest 45/45 — review

  • Next (one front at a time): C28 MCU UART HAL stub — IC

What v0.5.24 includes

Fase C · C26 (B1-fase-c-esc-output-hal) — the ESC output got a named port, still not a pin:

  • esc.py/esc.hpp (C10/C14's own module) gain EscOutput — Python abc.ABC, C++ abstract base with a virtual destructor — exposing apply_forces(forces: MotorForceCommand) -> EscApplyResult plus arm()/disarm()/armed (identical semantics to C10).

  • SimulatedEscSink is-a EscOutput in both languages. Its C10 apply(EscPwmCommand) path and arming behavior stay byte-identical — apply_forces is a thin wrapper: encode_motor_forces(forces) then apply(cmd).

  • The esc.cpp diff is purely additive — verified line-by-line: zero lines removed/changed, four lines added. esc.py/esc.hpp/esc.cpp are the only rung files touched this Buy; filter/attitude/controller/rate_torque/mixer/plant all stay git diff --stat empty.

  • The mixer still speaks forces only — mixer.py/mixer.hpp gain no PWM/DShot/pin knowledge, grep-verified in real code.

  • step still never calls apply/apply_forces/SimulatedEscSink — loop.py/loop.hpp/loop.cpp stay byte-unchanged, re-verified explicitly.

  • Only one implementation ships: SimulatedEscSink. No GPIO sink, no unimplemented pin class, no DShot this Buy.

  • EscOutput HAL != pin != motors != DShot. A named port exists; the simulated sink implements it; the mixer still does not know the wire protocol.

  • Package / tag v0.5.24 · suite 3553 · host ctest 39/39 — review

  • Next (one front at a time): C27 CRSF stream-timeout failsafe — IC

What v0.5.23 includes

Fase C · C25 (B1-fase-c-rc-setpoint) — RC channels got a language toward the tick, still not flying:

  • New src/jarvis/flight_software/flight_control/rc_setpoint.py: map_rc_to_loop_inputs(channels, *, t_s) -> RcLoopInputs — an illustrative AETR map (not a real TX model): roll/pitch/throttle = channel indices 0/1/2; the yaw channel is unused this Buy (no magnetometer anywhere in this tree, so no absolute heading a yaw stick could honestly command).

  • CRSF_CH_MIN/CRSF_CH_MID/CRSF_CH_MAX = 172/992/1811 (the same illustrative 11-bit convention already used around C20's policy). Throttle maps linearly onto collective ∈ [0, 1], clipped — mid-stick gives ≈0.5003, not exactly 0.5 (documented, not rounded away).

  • Roll/pitch deflection is measured from 992, scaled to reach exactly π/6 (30°) at either endpoint, clipped beyond it, composed into q_body_to_world_desired via the standard body 3-2-1 Euler-to-quaternion formula with yaw fixed at 0.

  • C++ twin: native/flight_control/include/jarvis/fc/rc_setpoint.hpp + src/rc_setpoint.cpp, added to jarvis_fc, same thresholds/formula, std::vector<int> in place of any CRSF-shaped type — the C21-C23 lock of zero CRSF/ELRS mentions anywhere under native/ stays intact (even in comments).

  • Optional step_with_rc(loop, sample, channels) maps then calls C24's own FlightControlLoop.step unchanged — loop.py/loop.hpp/loop.cpp all byte-unchanged; never calls a plant, SimulatedEscSink, or SafetyGate.evaluate.

  • C20's CrsfDualRolePolicy (aux → Authority kill) untouched and not imported here. radio.py still has no stick API. default_safety_gate() unchanged.

  • RC->setpoint != flying != sticks drive motors != Safety allow != yaw lock. A deterministic, documented map from already-decoded channel units to the two arguments step already accepted — no pilot flies anything, no motor spins, no heading-hold exists.

  • Package / tag v0.5.23 · suite 3538 · host ctest 36/36 — review

  • Next (one front at a time): C27 CRSF stream-timeout failsafe — IC

What v0.5.22 includes

Fase C · C24 (B1-fase-c-control-loop-tick) — the control loop got a name, still not flying:

  • New src/jarvis/flight_software/flight_control/loop.py: FlightControlLoop.step(sample, setpoint, collective) -> ControlTickResult — extracts the body C11/C13 already ran inlined into one named tick: filter_sample -> estimator.update -> controller.compute -> bridge.convert -> mixer.mix, exactly the existing order, calling each existing rung's own unmodified method. No new math.

  • C++ twin: native/flight_control/include/jarvis/fc/loop.hpp + src/loop.cpp, added to jarvis_fc, same order, same existing classes.

  • step does not call the plant, read a real HAL, or write a pin — the caller still supplies ImuSample and consumes MotorForceCommand/EscPwmCommand itself. dt is not an argument (no 1 kHz ISR claim); AttitudeSetpoint/collective are plain arguments (no RC decoding — mapping sticks to them is C25).

  • run_controlled_flight_sim_smoke (Python, C11) and fc_closed_loop_smoke (C++, C13) are refactored to call step instead of inlining the chain — verified bit-identical to the pre-refactor behavior: 15° → 0.252° in 200 steps, no gain retuned.

  • mcu/stub_main.cpp still has no ControlLoop/step( control cycle — no Reset_Handler spin.

  • radio.py/crsf_*.py/intent.py/safety.py/autonomy/ all byte-unchanged. default_safety_gate() unchanged.

  • Named control tick != flying != MCU ISR != motors != RC sticks. One named cycle, IMU+setpoint+collective in, four motor forces out, reusing C6-C12/C13 unchanged; plant, ESC pin, and RC mapping remain later cola.

  • Package / tag v0.5.22 · suite 3520 · host ctest 31/31 — review

  • Next (one front at a time): C27 CRSF stream-timeout failsafe — IC

What v0.5.21 includes

Fase C · C23 (B1-fase-c-crsf-host-baud) — Darwin host baud 420000, still not a live link:

  • Extends C22's own src/jarvis/capabilities/crsf_serial.py — no sixth module — with opt-in Darwin host baud configuration: configure_host_baud(fd, baud=420000) / CrsfHostSerialIngress.configure_baud(...).

  • On sys.platform == "darwin", applies raw 8N1 termios (disabling canonical mode and the CR/NL translations that would corrupt binary CRSF) then issues the real IOSSIOSPEED ioctl — the request number derived from the _IOW('T', 2, speed_t) macro, not copied from pyserial or any library; independently verified to equal 0x80085402.

  • On any other platform it fails closed with a typed CrsfHostSerialError — no Linux TCSETS2/BOTHER, no Windows serial stack.

  • attach_fd/attach_path still never auto-configure baud — C22's own "open = give me bytes" contract is unchanged, re-verified by re-running C22's own test suite unmodified.

  • Every PASS requires no hardware: the Darwin success path is proven entirely via a mocked fcntl.ioctl; the one unmocked ioctl call runs against a real POSIX pty and is asserted to fail (a pty is not a UART) — that failure is the honest, expected outcome, not something skipped around.

  • A successful ioctl is a host OS configuration fact, never proof a receiver exists. No pyserial dependency was added.

  • radio.py/crsf_stub.py/crsf_dual_role.py/crsf_stream.py/intent.py/safety.py all byte-unchanged. default_safety_gate() unchanged.

  • Host baud 420000 != live ELRS != RX connected != UART driver != Safety allow. Darwin can be asked to clock an attached FD at an ELRS-typical rate, proven without hardware via ioctl mock, nothing more.

  • Package file / tag v0.5.21 · suite 3504 — review

  • Next (one front at a time): C27 CRSF stream-timeout failsafe — IC

What v0.5.20 includes

Fase C · C22 (B1-fase-c-crsf-host-serial) — CRSF host serial ingest, still not a live link:

  • New src/jarvis/capabilities/crsf_serial.py — a fifth separate module (never folded into radio.py; git diff confirms radio.py/crsf_stub.py/crsf_dual_role.py/crsf_stream.py/intent.py/safety.py all byte-unchanged).

  • CrsfHostSerialIngress pulls bytes from an already-open host FD (attach_fd, caller-owned, never closed by this module) or an opt-in device path (attach_path, ingress-owned, closed on .close()); poll(...) performs exactly one non-blocking read then feeds C21's own CrsfByteStreamAssembler, unchanged — no background thread, no "connected" flag, no /dev/cu.* auto-scan.

  • Every PASS in this Buy's own test suite uses a POSIX pty as its loopback — no physical receiver or USB serial adapter is required, verified by running the full suite with nothing plugged in.

  • No pyserial dependency was added, and 420000 baud (the rate a real ELRS link runs at) is not configured anywhere — opening a path here means "give me bytes from this node," not "I configured an ELRS receiver"; custom-baud configuration is explicitly deferred to a later, platform-specific IC.

  • A real pty gotcha was found and disclosed while testing: POSIX ptys default to canonical (line-buffered) mode, which silently held binary CRSF bytes back from a reader until a newline appeared — fixed entirely in the test harness (tty.setraw(...)), not in the shipped module, which has no terminal-mode logic at all.

  • Optional poll_and_ingest(...) helper reuses C20's ingest_rc_channels(...) unchanged for any completed 0x16 frames.

  • RadioIntentAdapter.parse(...) still raises NotImplementedError. default_safety_gate() unchanged.

  • Host serial ingest != live ELRS != "RX connected" != UART driver != Safety allow. A pull-based FD/path reader proving bytes can be pulled from a host source and reassembled by C21's own assembler, verified entirely via pty loopback, nothing more.

  • Tag v0.5.20 · suite 3485 — review

  • Next (one front at a time — Engineer picks): deepen policy · board flash · craft↔FS · baud 420000

What v0.5.19 includes

Fase C · C21 (B1-fase-c-crsf-byte-stream) — CRSF byte-stream assembler, still not a UART:

  • New src/jarvis/capabilities/crsf_stream.py — a fourth separate module (never folded into radio.py; git diff confirms radio.py/crsf_stub.py/crsf_dual_role.py/intent.py/safety.py all byte-unchanged).

  • CrsfByteStreamAssembler.feed(data: bytes) -> list[CrsfFrame] reassembles frames from bytes delivered in arbitrary chunks (the shape a UART delivers data in) by slicing exact frame_len + 2 candidate windows and handing them, unmodified, to C19's own parse_crsf_frame — no second CRC8/envelope implementation (verified: no 0xD5 anywhere in the module).

  • Incomplete candidates wait in a bounded leftover buffer (default cap 256 bytes); invalid complete windows (bad CRC, or a declared frame_len outside the plausible [2, 64] range) are never raised to the caller — the assembler drops exactly one byte and resyncs, looping until it finds a valid frame or exhausts the buffer.

  • Optional ingest_stream_bytes(...) helper reuses C20's ingest_rc_channels(...) unchanged for any completed 0x16 frames — C20's policy (one aux channel, one threshold, AuthorityKind="kill" only) is neither deepened nor reconfigured here.

  • Zero I/O anywhere in the module — no serial/socket/pty/USB/open(), no class named like Serial/UartPort.

  • RadioIntentAdapter.parse(...) still raises NotImplementedError, even fed real assembled frames. default_safety_gate() unchanged.

  • Byte-stream assembler != UART open != live ELRS != a pilot link != Safety allow. A pure in-memory buffer proving bytes delivered in chunks reassemble into the same frames C19 already parses from a complete buffer, nothing more.

  • Tag v0.5.19 · suite 3465 — review

  • Next (one front at a time — Engineer picks): host serial ingest · deepen policy · board flash · craft↔FS

What v0.5.18 includes

Fase C · C20 (B1-fase-c-crsf-dual-role-bridge) — CRSF decode → dual-role bridge, still not a link:

  • New src/jarvis/capabilities/crsf_dual_role.py — a third separate module (never folded into radio.py; git diff confirms radio.py/intent.py/safety.py all byte-unchanged).

  • Bridges C19's decoded CrsfRcChannels into C5's typed RadioStubFrame/RadioDualRoleResult under one documented, deterministic policy: CrsfDualRolePolicy (one aux channel index + threshold, illustrative defaults channel 4/1500, not sourced from any real hardware) — at/above threshold emits AuthorityKind="kill" only (Authority-only, never Intent); below threshold, returns None.

  • Optional link_stats enrichment folds LQ/RSSI/SNR into RadioStubFrame.notes without ever changing whether Authority fires. ingest_rc_channels(...) optionally routes a produced frame through SimulatedRadioIngress.ingest(...).

  • Authority from this bridge stays trace-only relative to Safety — explicitly tested: wiring the bridge's own AuthoritySignal.id into a SafetyRequest.authority_signal_id still yields reject on both RejectAllSafetyGate and an armed ArmedAllowlistSafetyGate; default_safety_gate() is unchanged.

  • RadioIntentAdapter.parse(...) still raises NotImplementedError, even fed a real bridge-produced frame. The bridge never calls submit_command or imports the autonomy surface.

  • No I/O anywhere in the module — no serial/socket/pty/USB/subprocess.

  • Bridge != live ELRS != a pilot link != Safety allow. A deterministic, documented map from already-decoded bytes to a typed dual-role frame, nothing more.

  • Tag v0.5.18 · suite 3444 — review

  • Next (one front at a time — Engineer picks): UART stream · deepen policy · board flash · craft↔FS

What v0.5.17 includes

Fase C · C19 (B1-fase-c-crsf-link-stub) — first CRSF byte-fixture link stub (ELRS-shaped, host-only):

  • New src/jarvis/capabilities/crsf_stub.py — a separate module from C5's own radio.py on purpose (its no-decode-API lock stays byte-unchanged, confirmed via git diff).

  • Parses the CRSF envelope ([device_addr][frame_len][type][payload][crc8], CRC8 poly 0xD5 over type+payload) from checked-in fixture bytes into CrsfFrame, then decodes 0x16 RC_CHANNELS_PACKED (16 × 11-bit channels) and 0x14 LINK_STATISTICS (RSSI/LQ/SNR fields).

  • Truncated frames and bad-CRC frames both raise a typed CrsfParseError — no silent partial success — verified against four checked-in .bin fixtures under tests/fixtures/crsf/.

  • Zero I/O anywhere in the module — no serial/socket/pty/USB/subprocess, confirmed by inspecting real code with comments/docstrings stripped.

  • RadioIntentAdapter.parse(...) still raises NotImplementedError even when fed real CRSF fixture bytes; default_safety_gate() and ArmedAllowlistSafetyGate are untouched.

  • Nothing decoded here reaches SimulatedRadioIngress, autonomy submit_command, or any SafetyGate. No CRSF/ELRS token anywhere under native/.

  • Fixture CRSF parse != live ELRS != a pilot link != a CRSF driver product. This module can decode a byte sequence, nothing more: no receiver is "connected," no air protocol (RF/binding/telemetry) is implemented, no pilot's sticks drive anything.

  • Tag v0.5.17 · suite 3428 — review

  • Next (one front at a time — Engineer picks after C20 ACCEPT): UART stream · deepen policy · board flash · craft↔FS

What v0.5.16 includes

Fase C · C18 (B1-fase-c-cpp-mcu-freestanding-elf) — first freestanding linked MCU .elf:

  • New native/flight_control/mcu/ — generic Cortex-M4 linker script (FLASH at 0x00000000 / RAM at 0x20000000, the ARM-architected generic Code/SRAM regions, not any vendor's remapped boot address; 256 KiB/64 KiB illustrative sizes, explicitly fictional), a 16-entry ARMv7-M vector table + Reset_Handler startup, minimal newlib syscall stubs (no semihosting, no real I/O), and a thin entry point that links real jarvis_fc code.

  • Reuses C16's own toolchain file unchanged — produces fc_mcu_stub.elf alongside the existing libjarvis_fc.a.

  • A real link was performed and verified: readelf -h shows Machine: ARM, Type: EXEC, a real entry point, soft-float ABI; nm confirms jarvis::fc::ImuLowPassFilter::filter_sample and encode_motor_forces are linked in as defined (not merely referenced) symbols.

  • C++ exceptions kept enabled (option (a) over disabling them) — the rung sources' throw std::invalid_argument(...) calls are untouched; the C++ runtime resolves via the toolchain's own libstdc++/newlib plus this Buy's own syscall stubs.

  • A real build-system gap was hit and fixed, disclosed: the .c startup/syscall sources were silently never compiled (CXX-only project()) until C was added as a project language — the linker's cannot find entry symbol Reset_Handler warning is gone after the fix.

  • Host build re-verified unaffected: ctest still 28/28 green.

  • No GPIO/flash/OpenOCD/vendor BSP anywhere (grep-verified).

  • default_safety_gate() unchanged; autonomy submit_command still always rejects. Does not touch Continuity, orchestrator IDLE, Board, or library/.

  • Freestanding .elf != flashed / != boots on hardware / != motors / != GPIO. A linked, inspectable, host-only ARM executable, nothing more; this image has never run on any board.

  • Tag v0.5.16 · suite 3412 — review

  • Next (one front at a time — Engineer picks after C19 ACCEPT): board flash · craft↔FS · deepen link stub

What v0.5.15 includes

Fase C · C17 (B1-fase-c-safety-real-policy) — first real (non-RejectAll) Safety policy gate:

  • src/jarvis/capabilities/safety.py — ArmedAllowlistSafetyGate: opt-in, starts disarmed (always rejects, reason "disarmed"), and once explicitly arm()ed allows only HOLD/LAND (parsed from submit_command's own autonomy:{verb}:{id} action-id shape); any other verb or a malformed action_id is rejected too ("verb_not_allowed" / "unparseable_action_id" — neither reuses RejectAll's "not_implemented").

  • default_safety_gate() is byte-unchanged — git diff confirms zero lines touched in RejectAllSafetyGate or the factory function; the shipped product default remains "reject everything."

  • evaluate() never reads authority_signal_id at all — Authority (C5) stays trace-only, unable to flip a decision through this or any gate.

  • submit_command's allow branch, unreachable in shipped code since C4, already resolved to execution="not_implemented" — zero changes were needed to flight_software/autonomy/surface.py/types.py; verified at runtime via typing.get_args(ExecutionState) == {"not_attempted", "not_implemented"}.

  • New smoke_policy_gate_hold_and_land() helper demonstrates the armed allow path end-to-end (both verbs allow, execution still "not_implemented").

  • No AllowAllSafetyGate anywhere in src/, no SimulatedEscSink/GPIO coupling, no craft/CLI wiring.

  • Policy allow != flying / != executed autonomy / != hardware Safety / != "safe to fly". A real, opt-in, narrowly-scoped software gate now exists alongside the unchanged RejectAll default; no gate shipped in src/ can ever return execution="executed".

  • Tag v0.5.15 · suite 3403 — review

  • Next (one front at a time — Engineer picks): MCU freestanding .elf · link (ELRS) · craft↔FS

What v0.5.14 includes

Fase C · C16 (B1-fase-c-cpp-mcu-cross-compile) — first MCU cross-compile scaffold, a compile-time proof only:

  • New native/flight_control/cmake/toolchains/arm-none-eabi.cmake — CMAKE_SYSTEM_NAME Generic, generic Cortex-M4 (-mcpu=cortex-m4 -mthumb -mfloat-abi=soft, not a claim about any specific board's silicon).

  • Separate build (build/flight_control_mcu) produces only libjarvis_fc.a for that triple — gated entirely via CMake (no #ifdef in any rung source), so Catch2/the unit-test binary/both smokes never build on the MCU path.

  • A real cross-build was performed and verified: arm-none-eabi-objdump confirms file format elf32-littlearm, architecture: armv7e-m on the produced archive — genuine target code.

  • A real toolchain-completeness problem was hit and disclosed: the bare Homebrew arm-none-eabi-gcc formula has no bundled newlib/libstdc++ and fails to compile <optional>; the working build used the xPack arm-none-eabi-gcc v15.2.1-1.1 release instead. The new pytest wrapper handles both outcomes — skips with a specific install hint rather than hard-failing when a compiler is present but incomplete.

  • Host build re-verified unaffected: ctest still 28/28 green (unit suite + both smokes).

  • No GPIO/flash/OpenOCD/vendor BSP anywhere — STM32Cube, CMSIS device packs, ChibiOS, FreeRTOS, PX4, ArduPilot all absent (grep-verified).

  • default_safety_gate() unchanged; autonomy submit_command still always rejects. Does not touch Continuity, orchestrator IDLE, Board, or library/.

  • MCU cross-compile != flashed / != flying / != firmware runs on a flight controller / != GPIO-verified. A compile-time proof the steel-ladder sources build freestanding for a Cortex-M4-class target, nothing more; no board has run this code.

  • Tag v0.5.14 · suite 3389 — review

  • Next (one front at a time — Engineer picks): Safety-real · MCU freestanding .elf · link · craft↔FS

What v0.5.13 includes

Fase C · C15 (B1-fase-c-cpp-unit-tests) — a real C++ unit-test framework, deepening host verification:

  • Catch2 v3, pinned to release tag v3.7.1 (commit fa43b77429ba76c462b1898d6cd2f2d7a9416b14), fetched via CMake FetchContent — network needed once at first configure, none needed after (offline story documented in native/flight_control/README.md).

  • New native/flight_control/tests/ — 26 TEST_CASEs / ~494 assertions, at least one per steel rung: filter, attitude (including a C11 Amendment A regression guard), controller (sign-check on the dominant axis), rate_torque, mixer (documented X-geometry sign check), esc.

  • ctest now runs 28 entries total: the 26 unit cases (individually discovered via catch_discover_tests) plus both pre-existing smoke binaries — all green.

  • Behavior freeze honored exactly: git diff --stat on every pre-existing rung source (filter.cpp…esc.cpp/plant.cpp/all headers) is empty — this Buy added coverage only, no bug exposed, no Engineer-call needed.

  • Both smoke binaries remain unmodified; fc_closed_loop_smoke re-verified byte-identical: 15° → 0.252° in 200 steps.

  • No GPIO/pigpio//dev/mem/serial/DShot/socket anywhere in the new test sources (grep-verified).

  • default_safety_gate() unchanged; autonomy submit_command still always rejects. Does not touch Continuity, orchestrator IDLE, Board, or library/.

  • C++ unit tests != flying / != MCU verification / != production-hardened / != algorithm change. A host desktop ctest run with a real framework instead of two hand-rolled smoke mains, nothing more.

  • Tag v0.5.13 · suite 3380 — review

  • Next (one front at a time — Engineer picks): MCU cross-compile · Safety-real · link · craft↔FS

What v0.5.12 includes

Fase C · C14 (B1-fase-c-cpp-esc-pwm-stub) — steel-ladder parity for the ESC/PWM encoding stub:

  • native/flight_control/include/jarvis/fc/esc.hpp + src/esc.cpp — ports Python C10's encode_motor_forces/SimulatedEscSink into the C++ tree: same linear force→PWM-µs map (force=0 → min_us, force=1 → max_us, default 1000–2000, min_us < max_us enforced), same in-memory SimulatedEscSink (armed starts false; apply(cmd) always records the command, applied=true only while armed, otherwise applied=false/reason="disarmed").

  • New, separate fc_esc_pwm_smoke executable (18 checks, all passing) — kept apart from the C13 tip smoke on purpose, so that one stays focused.

  • fc_closed_loop_smoke (C13's own tip) re-verified byte-identical: 15° → 0.252° in 200 steps, unaffected by this Buy.

  • No GPIO/pigpio//dev/mem/serial/DShot/Oneshot/Multishot/socket anywhere in the new sources (grep-verified).

  • Python esc.py untouched — re-verified with the same before/after values.

  • default_safety_gate() unchanged; autonomy submit_command still always rejects. Does not touch Continuity, orchestrator IDLE, Board, or library/.

  • C++ ESC stub != hardware write / != ESC online / != motors spinning. Closes the C++ tree's module parity with the Python wooden ladder (filter/attitude/controller/rate_torque/mixer/esc, plus the plant tip).

  • Tag v0.5.12 · suite 3368 — review

  • Next (one front at a time — Engineer picks): MCU cross-compile · deepen C++ tests · real Safety · real link · craft↔FS

What v0.5.11 includes

Fase C · C13 (B1-fase-c-cpp-flight-control-scaffold) — the first material C++ scaffold for flight_control:

  • New native/flight_control/ tree (outside src/jarvis/, locked path) — C++17, host-only CMake ≥ 3.16 build. Not MCU firmware, not a board bring-up, no cross-compile requirement in this Buy.

  • Mirrors the Python wooden ladder module-for-module: filter/attitude/controller/rate_torque/mixer/plant, same formulas, same ENU convention, same honesty notes — including the C11 Amendment A accel-correction sign fix from day one.

  • fc_closed_loop_smoke executable (also runnable via ctest) runs the same closed-loop tip: seeded at 15° tilt, the true tilt error recovers to 0.252° after 200 steps — matching the Python ladder's own number, though bit-identity was not required by the IC.

  • No GPIO/pigpio//dev/mem/serial/DShot/socket anywhere in the tree (grep-verified) — pure host math and stdout. No PX4/ArduPilot vendored.

  • Python flight_software/ package untouched except a docstring pointer — it remains the design guide and the craft platform; this Buy does not delete or replace it.

  • default_safety_gate() unchanged; autonomy submit_command still always rejects. Does not touch Continuity, orchestrator IDLE, Board, or library/.

  • C++ scaffold != flying / != hardware flight controller / != firmware on any board / != replacing Python craft SoT. A host desktop build proving the same algorithmic ladder in a second language, nothing more.

  • Tagged v0.5.11 on Engineer ACCEPT (suite 3358) — review

  • Next (one front at a time): deepen C++ parity · MCU cross-compile · Safety-real · link · craft↔FS — Engineer prioritizes

What v0.5.10 includes

Fase C · C12 (B1-fase-c-rate-torque-bridge) — closes the rate ≠ torque honesty gap C9/C11 left open on purpose:

  • Same Engineer scaffold discipline: "Python scaffold / sim only — production flight_control runtime is C++ (future IC)." No C++ tree, no CMake created in this Buy.

  • src/jarvis/flight_software/flight_control/rate_torque.py — LinearRateTorqueBridge.convert(rates: BodyRateCommand) -> BodyTorqueCommand: one feedforward map only (tau_i = gain_i * omega_cmd_i per axis, scalar or per-axis gains, each finite and > 0) — not a cascaded rate PID (no kp * (omega_cmd - omega_measured) term, no integral/derivative state)

  • BodyTorqueCommand.tau_body is explicitly normalized/dimensionless, torque-like — never claimed as Newton-metres of any real vehicle

  • QuadXMixer.mix(collective, torques: BodyTorqueCommand) is migrated: it no longer accepts a bare BodyRateCommand at all — no silent dual API (passing a rate directly raises AttributeError, not a quiet misinterpretation)

  • C11's closed-loop tip re-verified through the bridge and unchanged: 15° → 0.252° in 200 steps, identical to before migration — no gain retune needed, since the bridge's default gain=1.0 is a mathematical no-op relative to the mixer's pre-migration direct pass-through

  • default_safety_gate() unchanged — the bridge is not actuation; autonomy submit_command still always rejects

  • Does not touch Continuity, orchestrator IDLE, Board, or library/

  • Bridge != rate loop product / != physical N·m / != flying. A single named feedforward step exists now instead of an implicit rate-as-torque assumption

  • Tagged v0.5.10 on Engineer ACCEPT (suite 3348) — review

  • Next: ★ C13 C++ scaffold IC (material change of the wooden ladder)

What v0.5.9 includes

Fase C · C11 (B1-fase-c-controlled-flight-sim-tip) — closes the C0 §7 wooden-ladder tip: toy closed-loop sim, no hardware:

  • Same Engineer scaffold discipline: "Python scaffold / sim only — production flight_control runtime is C++ (future IC)." No C++ tree, no CMake created.

  • src/jarvis/flight_software/flight_control/plant.py — ToyQuadAttitudePlant: toy, attitude-only, explicitly-not-product-physics dynamics. step(forces: MotorForceCommand, *, dt_s) -> ImuSample advances from C9 forces (not PWM) and emits an ImuSample consistent with its own true attitude — the C3→C10 chain now runs as a real closed loop

  • run_controlled_flight_sim_smoke(): from a documented 15° initial tilt, true tilt error drops below 2° within 200 steps. run_open_loop_baseline_smoke(): the same plant with zero correction stays at a constant 15° — no passive righting, proving the closed loop does real work

  • Rate ≠ torque remains open: C9's mixer still treats body rate as its mix channel; this plant does not silently insert a rate→torque controller — it uses its own, separately-documented toy force→angular-acceleration map

  • Also fixed, disclosed: a real sign bug in C7's ComplementaryAttitudeEstimator (tagged v0.5.5, ACCEPT CLOSED) — its accel correction had the cross-product argument order reversed, converging estimates away from the true tilt for any non-level input (confirmed even at 0.1°; every pre-existing C7 test only fed already-level accel, so this was never exercised). One-line fix + a new regression test in C7's own test file

  • default_safety_gate() unchanged — advancing the plant is not actuation; autonomy submit_command still always rejects

  • Does not touch Continuity, orchestrator IDLE, Board, or library/

  • Sim closed-loop tip @ 0.5.9 != flying / != hardware-verified flight / != physics-accurate sim. No motor spins, no real vehicle exists

  • Tagged v0.5.9 on Engineer ACCEPT (suite 3330) — review

What v0.5.8 includes

Fase C · C10 (B1-fase-c-esc-pwm-stub-rung) — sixth flight_control rung: ESC/PWM command encoding stub, in-memory sink only:

  • Same Engineer scaffold discipline: "Python scaffold / sim only — production flight_control runtime is C++ (future IC)." No C++ tree, no CMake created.

  • src/jarvis/flight_software/flight_control/esc.py — encode_motor_forces(forces, *, min_us=1000, max_us=2000) -> EscPwmCommand: linear map, force=0 → min_us, force=1 → max_us; min_us/max_us must be finite with min_us < max_us

  • Exactly one encoding — classic PWM-in-µs only; no DShot/Oneshot/Multishot shipped alongside it

  • SimulatedEscSink — armed starts False; apply(cmd) always records the command in memory but only reports applied=True while armed (applied=False, reason="disarmed" otherwise) — no RPi.GPIO, pigpio, /dev/mem, serial, or socket I/O anywhere

  • Not wired to C4: no auto-routing of AutonomyVerb.HOLD

  • run_esc_pwm_smoke() — full C3→C10 pipeline smoke path, disarmed by default (reuses the existing smoke_quad_hal_imu profile, no schema change)

  • default_safety_gate() unchanged — encoding/recording a PWM command is not actuation; autonomy submit_command still always rejects

  • Does not touch Continuity, orchestrator IDLE, Board, or library/

  • ESC/PWM stub @ 0.5.8 != hardware ESC / != flying. No pin, port, or socket exists anywhere, and no motor is claimed to spin

  • Tagged v0.5.8 on Engineer ACCEPT (suite 3315) — review

What v0.5.7 includes

Fase C · C9 (B1-fase-c-mixer-rung) — fifth flight_control rung: motor allocation, four numbers only:

  • Same Engineer scaffold discipline: "Python scaffold / sim only — production flight_control runtime is C++ (future IC)." No C++ tree, no CMake created.

  • src/jarvis/flight_software/flight_control/mixer.py — QuadXMixer: one documented quadrotor-X layout (motors 0..3 = FR/FL/RL/RR, 45° off body axes) via a fixed linear allocation matrix

  • mix(collective, rates: BodyRateCommand) -> MotorForceCommand — collective clamped to [0, 1]; output is four [0, 1]-clamped, dimensionless motor force numbers

  • Honesty-critical simplification, stated explicitly: C8's BodyRateCommand.omega_body_rad_s is a body rate, not a true body torque — this B1 mixer treats it directly as roll/pitch/yaw mix channels to teach allocation geometry, without claiming rate ≡ torque physically and without inventing a second controller to bridge that gap

  • roll_scale/pitch_scale/yaw_scale (default 0.05 each) must be finite and >= 0; a 0 disables that channel

  • Exactly one layout — no +/H/Y6/octo mixing matrix shipped alongside it

  • Not wired to C4: no auto-routing of AutonomyVerb.HOLD

  • hover_collective() and run_mixer_smoke() — tests/smoke only, no hover-thrust or hardware claim (reuses the existing smoke_quad_hal_imu profile, no schema change)

  • default_safety_gate() unchanged — mixing is not actuation; autonomy submit_command still always rejects

  • Does not touch Continuity, orchestrator IDLE, Board, or library/

  • Mixer stub @ 0.5.7 != ESC / != flying. No PWM, DShot, ESC UART, or GPIO exists anywhere, and no motor is claimed to spin

  • Tagged v0.5.7 on Engineer ACCEPT (suite 3297)

What v0.5.6 includes

Fase C · C8 (B1-fase-c-attitude-controller-rung) — fourth flight_control rung: attitude controller, body-rate output only:

  • Same Engineer scaffold discipline: "Python scaffold / sim only — production flight_control runtime is C++ (future IC)." No C++ tree, no CMake created.

  • src/jarvis/flight_software/flight_control/controller.py — PdAttitudeController: omega_cmd = kp * e_rot - kd * omega_measured (kp default 6.0, must be > 0; kd default 0.6, must be >= 0; both finite)

  • e_rot is the body-frame small-angle rotation vector from the C7 AttitudeState toward an AttitudeSetpoint, extracted from the shortest-path error quaternion; omega_measured is AttitudeState.omega_body_rad_s (damping term)

  • Exactly one controller — no cascaded rate PID, LQR, MPC, or INDI shipped alongside it

  • compute(setpoint, state) -> BodyRateCommand — body-rate number only: no motor thrust, no mixer matrix, no PWM/ESC, no collective-thrust channel, no position/velocity loop

  • Not wired to C4: AutonomyVerb.HOLD is never auto-routed into this controller; the module never calls submit_command

  • level_setpoint(t_s) — identity-quaternion setpoint for tests/smoke only, no hover-thrust claim

  • run_attitude_controller_smoke() — pipeline smoke path (reuses the existing smoke_quad_hal_imu profile, no schema change)

  • default_safety_gate() unchanged — computing a rate command is not actuation; autonomy submit_command still always rejects

  • Does not touch Continuity, orchestrator IDLE, Board, or library/

  • Controller stub @ 0.5.6 != flying / != motor commands. Mixer and ESC remain future ICs, each its own front

  • Tagged v0.5.6 on Engineer ACCEPT (suite 3280)

What v0.5.5 includes

Fase C · C7 (B1-fase-c-attitude-estimation-rung) — third flight_control rung: attitude estimation, sim-only, one algorithm:

  • Same Engineer scaffold discipline: "Python scaffold / sim only — production flight_control runtime is C++ (future IC)." No C++ tree, no CMake created.

  • src/jarvis/flight_software/flight_control/attitude.py — ComplementaryAttitudeEstimator: gyro integration fused with accel-derived tilt via a small-angle proportional correction (gain constructor param, default 0.02, rejects values outside (0, 1])

  • Exactly one algorithm — explicitly not Mahony/Madgwick/EKF/UKF/MEKF by name: no bias/integral state, no gradient descent, no covariance propagation

  • update(sample: ImuSample) -> AttitudeState — unit quaternion (w, x, y, z) mapping body → enu world frame (locked), plus body angular rate

  • Hard cut: no magnetometer, no GPS/baro, no online gyro-bias learning, no position/velocity — yaw is gyro-integrated only, with no absolute heading reference

  • Reuses C6's ImuLowPassFilter/ImuSample directly — no parallel filter reimplemented inside the estimator

  • read_attitude(hal, filt, estimator) pipes SimulatedImuHal → ImuLowPassFilter → estimator; vehicle_profiles.run_hal_imu_attitude_smoke() is the pytest-visible smoke path (reuses the existing smoke_quad_hal_imu profile, no schema change)

  • Known simulator limitation: SimulatedImuHal isn't attitude-aware — it always emits a fixed-direction gravity vector, so this Buy's tests validate against synthetic in-memory ImuSample sequences with known tilts, not solely via the shared sim HAL

  • default_safety_gate() unchanged — estimation is not actuation; autonomy submit_command still always rejects

  • Does not touch Continuity, orchestrator IDLE, Board, or library/

  • Attitude stub @ 0.5.5 != flight-verified attitude / != controlled flight. Controller, mixer, and ESC remain future ICs, each its own front

  • Tagged v0.5.5 on Engineer ACCEPT (suite 3264)

What v0.5.4 includes

Fase C · C6 (B1-fase-c-imu-filtering-rung) — second flight_control rung: IMU filtering, sensing post-process only:

  • Same Engineer scaffold discipline: "Python scaffold / sim only — production flight_control runtime is C++ (future IC)." No C++ tree, no CMake created.

  • src/jarvis/flight_software/flight_control/filter.py — ImuLowPassFilter: deterministic first-order EMA (alpha constructor param, default 0.2, rejects values outside (0, 1]), applied per-axis to accel_mps2/gyro_rad_s

  • filter_sample(raw: ImuSample) -> ImuSample reuses C3's ImuSample verbatim — no parallel type. First sample after construction/reset() seeds the filter unsmoothed

  • Sensing post-process, not estimation: no quaternion, no Euler angles, no Madgwick/Mahony/EKF output — that belongs to a later, separate estimation-class rung

  • read_filtered(hal, filt) pipes SimulatedImuHal.read_imu() through the filter; vehicle_profiles.run_hal_imu_filter_smoke() is the pytest-visible smoke path

  • default_safety_gate() unchanged — filtering is not actuation, no allow required; autonomy submit_command still always rejects

  • Does not touch Continuity, orchestrator IDLE, Board, or library/

  • Filter rung @ 0.5.4 != attitude / != controlled flight. State estimation, attitude/rate/position control, mixer, and ESC remain future ICs

  • Tagged v0.5.4 on Engineer ACCEPT (suite 3249)

What v0.5.3 includes

Fase C · C4 + C5 as one ACCEPT block (Engineer 2026-09-20) — package/tag v0.5.3. Intermediate tag v0.5.2 was never cut (see docs truth-sync).

C4 — autonomy command surface (B1-fase-c-autonomy-surface)

  • Same Engineer scaffold discipline: "Python scaffold / sim only — production flight_control runtime is C++ (future IC)." No C++ tree, no CMake created.

  • Opens src/jarvis/flight_software/autonomy/ — AutonomyVerb (TAKEOFF/HOLD/GO_TO/FOLLOW/RETURN_HOME/LAND/PATROL), propose_command(), submit_command()

  • Every submit_command call goes through SafetyGate.evaluate(...) first — with RejectAllSafetyGate, HOLD/LAND always outcome=reject / execution="not_attempted"

  • Even a test-local fake allow gate cannot make execution become "executed" — resolves to "not_implemented"

  • No flight_software/autonomy/executor.py; C3 IMU rung unchanged; registry still empty

  • Command surface != flyable autonomy

C5 — radio dual-role stub (B1-fase-c-radio-dual-role)

  • src/jarvis/capabilities/radio.py — RadioStubFrame → SimulatedRadioIngress → RadioDualRoleResult (Intent and/or AuthoritySignal)

  • RadioIntentAdapter.parse(...) still raises NotImplementedError (live path refuse)

  • Optional SafetyRequest.authority_signal_id — RejectAll unchanged; authority never implies allow

  • Dual-role stub != live ELRS — no CRSF/ELRS decode, no serial I/O, no radio→autonomy auto-submit

  • Does not touch Continuity, orchestrator IDLE, Board, or library/

  • Tagged v0.5.3 on Engineer ACCEPT (suite 3236)

What v0.5.1 includes

Fase C · C3 (B1-fase-c-first-fc-rung) — first flight_control rung, sensing-only:

  • Engineer amendment (on top of the C3 IC): "Python scaffold / sim only — production flight_control runtime is C++ (future IC)." Everything here is a Python platform scaffold, not the production flight controller; the real flight_control runtime/firmware will be C++ in later Buys with its own IC. No C++ tree, no CMake created in this Buy.

  • Opens src/jarvis/flight_software/flight_control/ and src/jarvis/vehicle_profiles/ on disk for the first time — exactly one rung: HAL + simulated IMU sample acquisition (ImuHal, SimulatedImuHal, ImuSample)

  • No filtering, no state estimation, no attitude/rate/position controller, no mixer, no ESC/PWM, no autonomy verbs — none of it exists in this package yet

  • SimulatedImuHal is deterministic (same seed → same sample sequence) and never touches real hardware, a bus, or a network socket

  • One pytest smoke VehicleProfile (smoke_quad_hal_imu) + run_hal_imu_smoke() in vehicle_profiles/ — not bound to any craft workspace/BOM/catalog SKU

  • Naming split (honesty-critical): craft catalog flight_controller (a BOM part) and flight_software.flight_control (this control spine) are different systems of record — never conflated

  • default_safety_gate() unchanged — still always RejectAllSafetyGate; no AllowAllSafetyGate anywhere in src/

  • Does not touch Continuity, orchestrator IDLE, Board, or library/

  • First rung stub @ 0.5.1 != controlled flight. Estimation/control/mixer/ESC/autonomy remain future ICs (C4+)

  • Tagged v0.5.1 on Engineer ACCEPT (see .jes/artifacts/implementation_review_fase_c_first_fc_rung_b1.md)

What v0.5.0 includes

Fase C · C1 scaffold (B1-fase-c-capability-registry-scaffold) — schemas only, no runtime:

  • src/jarvis/capabilities/ — typed CapabilityRecord / ProviderRecord / SkillRecord (Pydantic) + a CapabilityRegistry with a query-only API (list / get-by-id / "who offers capability X?")

  • CapabilityRegistry.load_default() is always empty — 0 capabilities, 0 providers, 0 skills. No path in the product marks flight/actuation as available; the availability enum only offers stub / not_implemented in C1

  • No execution path: no method or field named execute / dispatch / command_esc; nothing here turns a record into a motor/ESC/autonomy command

  • Does not touch Continuity, orchestrator IDLE, Board, or library/ — craft SoT stays the v0.4.3 surface

  • Scaffold @ 0.5.0 != Flight Software shipped. At C1, flight_software/ did not exist yet; C3 @ v0.5.1 opens the first Python scaffold rung (production FC runtime remains C++ / future IC)

  • Git tag v0.5.0 / checkpoint-fase-c-capability-registry (Engineer ACCEPT 2026-09-20)

What v0.4.3 includes (tag)

Last pre–Fase C checkpoint (closeout note):

  • Everything in v0.4.2 (Fase M mission craft), plus:

    • D1 docs/` truth-sync @ code · obsolete labeled

    • U1 Board Taller 3D default · Grafo tab · inspector + mount chain to plate · piece chips

    • IDLE first-acquire catalog scoped to vtx/cameras (FN-009 / terrestrial wizard preserved)

  • Live suite 3166 · UI 132

  • Next: Fase C @ 0.5.0 on first Engineer ★ Buy

What v0.4.2 included

Mission craft software checkpoint (Fase M CLOSED — M7 closeout):

  • Continuity mission intent + wizard vigilancia nudge + SYSTEM_DEFINITION B routing

  • Mission mass_g → AUW; Continuity ladder mount + autonomía objetivo; power_w declare + Phoenix catalog W

  • library/cameras Phoenix 2 + library/vtx Zeus 800 — fluid catalog help-choose / rebind / mass mirror (RF mW ≠ DC W)

  • BOM [sku] display for cameras / FC / sensors; craft-montage user guide sync

  • Live suite 3165 · UI 105

What v0.4.1 included

  • Everything in v0.4.0 (Continuity spatial assembly), plus Board Situar:

    • C-113 — Scene3D drag → board_pose_bridge → same set_component_declared_box_pose writer as CLI declara…

    • Free camera while situating (no forced cenital); screen-plane drag follows the cursor; Shift = profundidad (Y)

    • Situar UX: larger pane, zoom range, live preview

    • Standoff count gate B4-min (count==4 → corners; else omit)

  • Live suite 2669 · UI 80

  • Locks unchanged: Prop/Energy = HD-004 wall; System Optimization deferred until pain; screening AABB ≠ fit VERIFIED

What's landed between v0.4.1 and v0.4.2 (now tagged)

The craft montage + mission craft arc — empty project → montaje honesto + identidad/masa/montaje/potencia/VTX sin inventar física:

  • Checklist family, estimated-temporary dims, arm/disk Visor, FC/sensors library envelopes

  • Mission payload identity → cameras/radio → Continuity ladder → catalog seeds

  • See docs/IMPLEMENTATION_TASKS.md and USER_GUIDE_CRAFT_MONTAGE.md

Next

Tip tagged v0.6.0 — ontology explain CLOSED (spine solid+audited, ONTOLOGY_CROSSWALKS.md). See close note.

PRIORIDAD: Assistant 0.6.1+ — ★ DC-assistant-placement → intelligence/ scaffold → R2 retrieve → terminal canal. Silicon parked — bench note.

Parked (bags/lab): C30 DFU smoke · plate-box · Path N · HD-* · more camera/radio SKUs · Board inspector polish · GPIO/DShot wire · Linux baud.

See docs/IMPLEMENTATION_TASKS.md.

Docs

Doc

Role

VISION.md

Product contract (non-technical)

PRODUCT_SCOPE.md

v1 usable — when a project is “done”

docs/PROJECT_CONTINUITY.md

A' — Situation / Evidence / Next useful step

docs/ARCHITECTURE.md

How the system is built

docs/system_map/README.md

System Map — connections & authority

docs/USER_GUIDE_CRAFT_MONTAGE.md

Command-level walk: empty project → montaje honesto en Board

docs/IMPLEMENTATION_TASKS.md

Roadmap, gaps, software/product debt

docs/HARDWARE_DEBT.md

Physics debt gated on T1/T2 lab (ESC η, battery C-rate, sag, OP→consumo)

.jes/artifacts/cli_findings_post_catalog_bind_v1.md

Living CLI findings register (G9–G20)

src/jarvis/README.md

Deeper product / flow reference

Tags

v0.5.21 / checkpoint-fase-c-crsf-host-baud — Fase C C23: Darwin IOSSIOSPEED 420000 + raw 8N1 on C22 FD; suite 3504 · UI 132.
v0.5.20 / checkpoint-fase-c-crsf-host-serial — Fase C C22: CRSF host serial ingest (pty); suite 3485 · UI 132.
v0.5.11 / checkpoint-fase-c-cpp-scaffold — Fase C C13: first C++ flight_control host scaffold; suite 3358 · UI 132.
v0.5.10 / checkpoint-fase-c-rate-torque — Fase C C12: rate→torque honesty bridge; suite 3348 · UI 132.
v0.5.9 / checkpoint-fase-c-sim-tip — Fase C C11: toy closed-loop wooden-ladder tip (+ C7 accel sign fix); suite 3330 · UI 132.
v0.5.8 / checkpoint-fase-c-esc-pwm — Fase C C10: force→PWM µs + SimulatedEscSink; suite 3315 · UI 132.
v0.5.7 / checkpoint-fase-c-mixer — Fase C C9: quad-X mixer; suite 3297 · UI 132.
v0.5.6 / checkpoint-fase-c-controller — Fase C C8: PD attitude → body-rate command; suite 3280 · UI 132.
v0.5.5 / checkpoint-fase-c-attitude — Fase C C7: complementary attitude estimation rung; suite 3264 · UI 132.
v0.5.4 / checkpoint-fase-c-imu-filter — Fase C C6: IMU EMA/low-pass filter rung; suite 3249 · UI 132.
v0.5.3 / checkpoint-fase-c-autonomy-radio — Fase C C4+C5 one block: autonomy command surface + radio dual-role stub; suite 3236 · UI 132. (No v0.5.2 tag — see truth-sync note.)
v0.5.1 / checkpoint-fase-c-first-fc-rung — Fase C C3: first flight_control rung (HAL + simulated IMU), flight_software/+vehicle_profiles/ opened; Python scaffold, production FC runtime is C++ (future IC); suite 3206 · UI 132.
v0.5.0 / checkpoint-fase-c-capability-registry — Fase C open: empty Capability Registry scaffold; suite 3181 · UI 132.
v0.4.3 / checkpoint-board-taller-3d — pre–Fase C CLOSED: docs truth-sync + Board Taller 3D; suite 3166 · UI 132.
v0.4.2 / checkpoint-fase-m-mission-craft — Fase M CLOSED: mission mass + Continuity ladder + cameras/VTX seeds + B routing; suite 3165 · UI 105.
v0.4.1 / checkpoint-board-situar — Board Situar drag→pose (C-113) + free camera + standoff count gate; suite 2669 · UI 80.
v0.4.0 / checkpoint-continuity-spatial-assembly — Continuity spatial assembly (situar el mapa); suite 2652.
v0.3.8 / checkpoint-spatial-board-projector — ship spatial_board.py (was gitignored); /workspace/ ignore.
v0.3.7 / checkpoint-structure-representation-closed — Structure representation arc closed (catalog→parts→rebind→plates); suite 2294.
v0.3.6 / checkpoint-experimental-prop-energy-closed — experimental prop/energy/Structure A/fail-routing construction closed; knowledge-parity phase starts.
v0.3.5 / checkpoint-phase25-hover-energy — Phase 2.5 honest hover-regime autonomy.
v0.3.4 / checkpoint-motor-op-voltage-coherence — motor OP voltage gate + DSE live params.
v0.3.3 / checkpoint-validation-case-regression-gate — Validation Case probe/docs.
v0.3.2 / checkpoint-deferred-queue-cd — Deferred Queue C+D.
v0.3.1 / checkpoint-next-engineering-block — G24-A + P2-2 OP bridge.
v0.3.0 / checkpoint-propeller-catalog-bind — propeller help-choose → exact OP.
v0.2.0 — H1–H4 handoffs closed; System Map at 0 RED.
v0.1.0-prototype — first functional cut.

Related MCP Connectors

Related MCP Servers