mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-21 21:11:09 +00:00
bc40ea7300
Greenfield Steps 1-6 baseline for the autopilot rewrite from legacy Qt/C++ to a Rust workspace. - Remove legacy Qt/C++ tree (ai_controller, drone_controller, misc/camera, python_scaffold, root Dockerfile, autopilot.pro, legacy main.py / requirements.txt). - Add _docs/00_problem (problem, restrictions, acceptance criteria, security approach, input data + fixtures). - Add _docs/01_solution/solution_draft01. - Add _docs/02_document (architecture, system-flows, data_model, glossary, decision-rationale, deployment, 13 component descriptions, tests/ specs, FINAL_report, module-layout). - Add _docs/02_tasks/todo with 47 task specs (AZ-640..AZ-686, one bootstrap + 46 component tasks) and _dependencies_table.md. - Add .cursor/rules/artifact-srp.mdc (single-responsibility rule for canonical _docs artifacts). - Track autodev state in _docs/_autodev_state.md (Step 6 completed, ready for Step 7 Implement). Jira: bootstrap AZ-626; component epics AZ-627..AZ-639; tasks AZ-640..AZ-686. Total complexity 173 points across 12 epics. Co-authored-by: Cursor <cursoragent@cursor.com>
22 KiB
22 KiB
Module Layout
Language: rust
Layout Convention: crates-workspace
Root: crates/
Last Updated: 2026-05-19
Layout Rules
- Each component owns ONE top-level directory under
crates/. The directory name matches the component name in_docs/02_document/components/. - Shared code lives in a single
crates/shared/crate. Cross-cutting concerns are modules inside it (shared/models/,shared/config/,shared/error/,shared/health/,shared/observability/,shared/clock/,shared/contracts/). Other crates re-export fromshared::; they MUST NOT duplicate types. - Public API surface per component = the files in
Public APIbelow. Everything undersrc/internal/(and any other module not listed inPublic API) is internal and other crates MUST NOT use it. - Tests live in each crate's own
tests/directory (Rust convention). Workspace-level end-to-end tests live attests/e2e/(the workspace root, not under any crate). - Stream-based wiring: tokio channels carrying shared data types are passed into actor constructors by the composition root (
crates/autopilot). This keeps Layer 2 actors free of sibling imports — they receiveReceiver<Frame>,Receiver<GimbalState>, etc. fromshared::modelswithout importing the crate that produces them. - Sink traits in shared: where one component must push into another's transport (e.g.
operator_bridgepushes POIs throughtelemetry_stream), the receiving side implements a trait defined inshared::contracts(TelemetrySink,MavlinkSink, etc.). The producing side depends only on the trait, not on the receiving crate.
Per-Component Mapping
Component: shared
- Epic: AZ-626 (Bootstrap & Initial Structure — shared crate lands as part of AZ-640 initial structure task)
- Directory:
crates/shared/ - Public API:
crates/shared/src/lib.rs(re-exports the submodules listed below)crates/shared/src/models/mod.rs(Frame,BoundingBox,Detection,DetectionBatch,MovementCandidate,Tier2Evidence,VlmAssessment,POI,MapObject,MapObjectObservation,MapObjectsBundle,IgnoredItem,Coordinate,Geofence,MissionItem,MissionWaypoint,OperatorCommand,GimbalState)crates/shared/src/config/mod.rs(Config,ConfigLoader, per-component typed sections)crates/shared/src/error.rs(AutopilotError,Result<T>)crates/shared/src/health.rs(ComponentHealth,AggregatedHealth,HealthLevel)crates/shared/src/observability/mod.rs(tracinginit, log-field constants perobservability.md §2)crates/shared/src/clock.rs(MonoClock,WallClock,ClockSource)crates/shared/src/contracts/mod.rs(TelemetrySink,MavlinkSink,VlmProvider,OperatorCommandSink)
- Internal: none — shared has no
internal/subtree; everything in shared is part of its public API by design. - Owns (exclusive write during implementation):
crates/shared/** - Imports from: (none — Layer 1)
- Consumed by: every other component crate + the
autopilotbinary
Component: mavlink_layer
- Epic: AZ-637
- Directory:
crates/mavlink_layer/ - Public API:
crates/mavlink_layer/src/lib.rs(MavlinkLayer,MavlinkHandle,MavlinkConnection, public message types re-exported fromshared::models)
- Internal:
crates/mavlink_layer/src/internal/codec/*(MAVLink v2 encode/decode for the §7.7 surface only)crates/mavlink_layer/src/internal/transport/udp.rscrates/mavlink_layer/src/internal/transport/serial.rscrates/mavlink_layer/src/internal/heartbeat.rscrates/mavlink_layer/src/internal/retry.rs
- Owns:
crates/mavlink_layer/** - Imports from:
shared - Consumed by:
mission_executor,telemetry_stream(via constructor-injectedReceiver<MavlinkTelemetry>or via theMavlinkSinktrait)
Component: mission_client
- Epic: AZ-638
- Directory:
crates/mission_client/ - Public API:
crates/mission_client/src/lib.rs(MissionClient,MissionClientHandle::pull_mission(),post_middle_waypoint(),pull_mapobjects(),push_mapobjects(),health())
- Internal:
crates/mission_client/src/internal/missions_api/*(REST client + retry + auth)crates/mission_client/src/internal/mapobjects_sync/*(pre-flight GET + post-flight POST bundles)crates/mission_client/src/internal/schema/*(schema-version validation againstmission-schema)
- Owns:
crates/mission_client/** - Imports from:
shared - Consumed by:
mission_executor(for mission lifecycle),mapobjects_store(for hydrate/dump indirectly throughmission_executororchestration)
Component: frame_ingest
- Epic: AZ-627
- Directory:
crates/frame_ingest/ - Public API:
crates/frame_ingest/src/lib.rs(FrameIngest,FrameIngestHandle::subscribe() -> Receiver<Frame>,health())
- Internal:
crates/frame_ingest/src/internal/rtsp_client.rscrates/frame_ingest/src/internal/decoder.rscrates/frame_ingest/src/internal/timestamp.rs
- Owns:
crates/frame_ingest/** - Imports from:
shared - Consumed by:
detection_client,movement_detector,telemetry_stream(all via composition-root-wiredReceiver<Frame>)
Component: detection_client
- Epic: AZ-628
- Directory:
crates/detection_client/ - Public API:
crates/detection_client/src/lib.rs(DetectionClient,DetectionClientHandle::request(Frame) -> Result<DetectionBatch>,health())
- Internal:
crates/detection_client/build.rs(tonic-buildfor the gRPC proto)crates/detection_client/proto/detections.proto(vendored copy of../detectionscontract perarchitecture.md §10)crates/detection_client/src/internal/grpc/*(bi-directional streaming client, version handshake)
- Owns:
crates/detection_client/** - Imports from:
shared - Consumed by:
scan_controller(handle for direct request),telemetry_stream(via constructor-injectedReceiver<DetectionBatch>for operator overlay)
Component: gimbal_controller
- Epic: AZ-634
- Directory:
crates/gimbal_controller/ - Public API:
crates/gimbal_controller/src/lib.rs(GimbalController,GimbalControllerHandle::set_pose(...),zoom(level),state() -> GimbalState,state_stream() -> Receiver<GimbalState>,health())
- Internal:
crates/gimbal_controller/src/internal/a40_protocol/*(ViewPro A40 UDP vendor protocol — encode, decode, CRC)crates/gimbal_controller/src/internal/smooth_pan.rs(smooth-pan path-tracking primitive)
- Owns:
crates/gimbal_controller/** - Imports from:
shared - Consumed by:
scan_controller(handle),movement_detector(via constructor-injectedReceiver<GimbalState>),frame_ingest(constructor-injectedReceiver<GimbalState>for timestamp annotation if needed)
Component: semantic_analyzer
- Epic: AZ-630
- Directory:
crates/semantic_analyzer/ - Public API:
crates/semantic_analyzer/src/lib.rs(SemanticAnalyzer,SemanticAnalyzerHandle::analyze(roi) -> Result<Tier2Evidence>,health())
- Internal:
crates/semantic_analyzer/src/internal/primitive_graph/*(path, branch-pile, entrance, road graph reasoner)crates/semantic_analyzer/src/internal/roi_cnn.rs(TensorRT ROI CNN wrapper)crates/semantic_analyzer/src/internal/scoring/*(path-freshness, endpoint, concealment)
- Owns:
crates/semantic_analyzer/** - Imports from:
shared - Consumed by:
scan_controller(handle)
Component: vlm_client
- Epic: AZ-631
- Directory:
crates/vlm_client/ - Public API:
crates/vlm_client/src/lib.rs(VlmClientimplementingshared::contracts::VlmProvider;VlmClient::with_default()returns the no-op impl returningVlmAssessment { status: vlm_disabled }; real impl is gated behindfeature = "vlm")
- Internal:
crates/vlm_client/src/internal/uds_client.rs(Unix-domain socket IPC + peer-credential check)crates/vlm_client/src/internal/schema_validate.rs(VlmAssessmentschema validation)crates/vlm_client/src/internal/prompt.rs(bounded prompt + payload size enforcement)
- Owns:
crates/vlm_client/** - Imports from:
shared - Consumed by:
scan_controller(via theshared::contracts::VlmProvidertrait — never directly)
Component: mapobjects_store
- Epic: AZ-633
- Directory:
crates/mapobjects_store/ - Public API:
crates/mapobjects_store/src/lib.rs(MapObjectsStore,MapObjectsStoreHandle::classify(Detection) -> Classification,apply_decline(POI),dump_pending() -> MapObjectsBundle,hydrate(MapObjectsBundle),set_sync_state(SyncState),health())
- Internal:
crates/mapobjects_store/src/internal/h3_index/*(h3rswrapper + k-ring queries)crates/mapobjects_store/src/internal/engine/mod.rs(StorageEnginetrait — pluggable for Q3)crates/mapobjects_store/src/internal/engine/in_memory_snapshot.rs(default impl: in-memory + JSON snapshot on flush)crates/mapobjects_store/src/internal/diff.rs(NEW / MOVED / EXISTING / REMOVED-CANDIDATE classification)crates/mapobjects_store/src/internal/ignored.rs
- Owns:
crates/mapobjects_store/** - Imports from:
shared - Consumed by:
scan_controller,operator_bridge,mission_executor(for hydrate at pre-flight + dump_pending at post-flight)
Component: movement_detector
- Epic: AZ-629
- Directory:
crates/movement_detector/ - Public API:
crates/movement_detector/src/lib.rs(MovementDetector,MovementDetectorHandle::candidates() -> Receiver<MovementCandidate>,health(); constructor takesReceiver<Frame>,Receiver<GimbalState>,Receiver<MavlinkTelemetry>)
- Internal:
crates/movement_detector/src/internal/ego_motion.rs(homography-based ego-motion estimate)crates/movement_detector/src/internal/optical_flow/*(classical CV path)crates/movement_detector/src/internal/learned_cv/*(fallback per Q14 — behindfeature = "learned_cv")crates/movement_detector/src/internal/zoom_bands.rs(per-zoom-band threshold tables)crates/movement_detector/src/internal/telemetry_sync.rs(frame ↔ gimbal ↔ UAV skew gate)
- Owns:
crates/movement_detector/** - Imports from:
shared - Consumed by:
scan_controller(consumes theMovementCandidatestream)
Component: telemetry_stream
- Epic: AZ-639
- Directory:
crates/telemetry_stream/ - Public API:
crates/telemetry_stream/src/lib.rs(TelemetryStreamimplementingshared::contracts::TelemetrySink;TelemetryStreamHandle::commands() -> Receiver<OperatorCommand>,health(); constructor takesReceiver<Frame>,Receiver<DetectionBatch>,Receiver<MavlinkTelemetry>,Receiver<BboxOverlay>)
- Internal:
crates/telemetry_stream/src/internal/uplink/*(modem push: protocol per../_docs/04_system_design_clarifications.md— Q2)crates/telemetry_stream/src/internal/downlink/*(operator-command receive path)crates/telemetry_stream/src/internal/encode/*(frame + telemetry + bbox-overlay serialisation)
- Owns:
crates/telemetry_stream/** - Imports from:
shared - Consumed by:
operator_bridge(via theTelemetrySinktrait inshared::contracts; commands consumed via constructor-injectedReceiver<OperatorCommand>)
Component: operator_bridge
- Epic: AZ-635
- Directory:
crates/operator_bridge/ - Public API:
crates/operator_bridge/src/lib.rs(OperatorBridge,OperatorBridgeHandle::surface_poi(POI) -> OperatorDecision,middle_waypoint_hints() -> Receiver<MiddleWaypointHint>,target_follow_events() -> Receiver<TargetFollowEvent>,health(); constructor takesArc<dyn TelemetrySink>andReceiver<OperatorCommand>)
- Internal:
crates/operator_bridge/src/internal/auth/*(OperatorCommandenvelope validation — signature, replay protection, session validation; scheme stubbed pending Q9)crates/operator_bridge/src/internal/audit_log.rs(persistent audit log writer for/var/lib/autopilot/audit/)crates/operator_bridge/src/internal/decision_window.rs(confidence-scaled timeout: 40 % → 30 s, 100 % → 120 s linear)
- Owns:
crates/operator_bridge/** - Imports from:
shared,mapobjects_store - Consumed by:
scan_controller(forsurface_poi),mission_executor(consumesmiddle_waypoint_hintsstream)
Component: mission_executor
- Epic: AZ-636
- Directory:
crates/mission_executor/ - Public API:
crates/mission_executor/src/lib.rs(MissionExecutor,MissionExecutorHandle::start(Mission),insert_middle_waypoint(Coordinate),failsafe_trigger(FailsafeKind),state() -> ExecutorState,health(); constructor takesReceiver<MiddleWaypointHint>from operator_bridge)
- Internal:
crates/mission_executor/src/internal/multirotor/fsm.rs(DISCONNECTED → … → LAND)crates/mission_executor/src/internal/fixed_wing/fsm.rs(DISCONNECTED → … → WAIT_AUTO → LAND)crates/mission_executor/src/internal/geofence/*(INCLUSION + EXCLUSION enforcement)crates/mission_executor/src/internal/failsafe/ladder.rs(lost-linkLinkOk → LinkDegraded → LinkLost → LinkLostInFollow)crates/mission_executor/src/internal/battery_thresholds.rs(RTL floor, hard floor)crates/mission_executor/src/internal/bit.rs(pre-flight built-in self-test; orchestrates pre-flightmapobjects_store.hydrate(mission_client.pull_mapobjects(...)))crates/mission_executor/src/internal/middle_waypoint.rs(re-upload sequence on operator confirm)crates/mission_executor/src/internal/post_flight.rs(orchestrates post-flightmission_client.push_mapobjects(mapobjects_store.dump_pending()))
- Owns:
crates/mission_executor/** - Imports from:
shared,mavlink_layer,mission_client,mapobjects_store - Consumed by:
scan_controller(forfailsafe_triggerandinsert_middle_waypoint)
Component: scan_controller
- Epic: AZ-632
- Directory:
crates/scan_controller/ - Public API:
crates/scan_controller/src/lib.rs(ScanController,ScanControllerHandle::tick(),submit_operator_cmd(OperatorCommand),state() -> ScanState,health(); constructor takesReceiver<DetectionBatch>,Receiver<MovementCandidate>,Receiver<Frame>plus handles formapobjects_store,gimbal_controller,mission_executor,semantic_analyzer,operator_bridge, andArc<dyn VlmProvider>)
- Internal:
crates/scan_controller/src/internal/state_machine/mod.rs(ZoomedOut,ZoomedIn { roi, hold_started_at },TargetFollow { target_id, started_at })crates/scan_controller/src/internal/state_machine/transitions.rscrates/scan_controller/src/internal/poi_queue/*(priority queue +≤5 POIs/mincap + confidence × proximity × age ordering)crates/scan_controller/src/internal/behaviour_tree/*(persystem-flows.md §F4)crates/scan_controller/src/internal/timeouts.rs(operator-decision window, POI timeouts, VLM waits)crates/scan_controller/src/internal/frame_rate_guard.rs(suppress zoom-in transitions below ≥10 fps; surface yellow health)
- Owns:
crates/scan_controller/** - Imports from:
shared,mapobjects_store,gimbal_controller,mission_executor,semantic_analyzer,operator_bridge - Consumed by:
autopilot(composition root)
Component: autopilot (binary, composition root)
- Epic: AZ-626 (Bootstrap & Initial Structure — the binary scaffold is part of AZ-640)
- Directory:
crates/autopilot/ - Public API: this is a
[[bin]]crate — it exposes no library API. - Internal:
crates/autopilot/src/main.rs(CLI parse, config load,tracinginit, build component instances, run)crates/autopilot/src/runtime.rs(build channels, wire actors, owns theVec<JoinHandle>, shutdown orchestration)crates/autopilot/src/health_server.rs(HTTP/healthendpoint percontainerization.md §7)crates/autopilot/src/bit_runner.rs(invokesmission_executor.bit()and gates startup)
- Owns:
crates/autopilot/** - Imports from:
shared+ every Layer 2 actor crate + every Layer 3 coordinator +scan_controller - Consumed by: nothing — this is the binary
Shared / Cross-Cutting
All cross-cutting concerns live as modules inside the single crates/shared/ crate (Rust convention prefers a single shared crate over many tiny ones; the module boundaries inside shared:: enforce conceptual separation).
shared::models
- Path:
crates/shared/src/models/ - Purpose: the canonical entity catalogue from
_docs/02_document/data_model.md. One submodule per entity grouping (frame.rs,detection.rs,movement.rs,tier2.rs,vlm.rs,poi.rs,mapobject.rs,mission.rs,operator.rs,gimbal.rs). - Owned by: AZ-640 initial structure task (under epic AZ-626).
- Consumed by: every component crate + the
autopilotbinary.
shared::config
- Path:
crates/shared/src/config/ - Purpose: TOML loader (per
containerization.md §6), typed per-component sections, environment-variable overlay, secrets resolution (via path toEnvironmentFile=). - Owned by: AZ-640 initial structure task.
- Consumed by: every component crate.
shared::error
- Path:
crates/shared/src/error.rs - Purpose:
AutopilotErrorenum +Result<T> = std::result::Result<T, AutopilotError>alias. - Owned by: AZ-640 initial structure task.
- Consumed by: every crate.
shared::health
- Path:
crates/shared/src/health.rs - Purpose:
ComponentHealth,HealthLevel { Green, Yellow, Red, Disabled },AggregatedHealth— each component exposes its ownhealth() -> ComponentHealth;autopilot::health_serveraggregates percontainerization.md §7. - Owned by: AZ-640 initial structure task.
- Consumed by: every component + the binary's health server.
shared::observability
- Path:
crates/shared/src/observability/ - Purpose:
tracing-subscriberinit (JSON to stdout); log-field constants for the §2 fields inobservability.md; span helpers for frame trace + POI trace. - Owned by: AZ-640 initial structure task.
- Consumed by: every component (for spans and counters).
shared::clock
- Path:
crates/shared/src/clock.rs - Purpose:
MonoClock(monotonic, authoritative for telemetry-skew compensation and tick budgets),WallClock(bound to GPS time once locked, NTP at boot),ClockSource { Gnss, Host, Coast }. Drift > 200 ms → yellow health. - Owned by: AZ-640 initial structure task.
- Consumed by: every component that timestamps anything (frame_ingest, movement_detector, scan_controller, operator_bridge audit log, mapobjects_store).
shared::contracts
- Path:
crates/shared/src/contracts/ - Purpose: trait definitions for cross-component coupling that we want to keep import-free:
TelemetrySink—push_frame,push_telemetry,push_overlay(impl:telemetry_stream)MavlinkSink—send(impl:mavlink_layer; letsmission_executordepend on a trait rather than the concrete crate when convenient)VlmProvider—assess(roi) -> VlmAssessment(impl:vlm_client; default no-op impl returnsvlm_disabled)OperatorCommandSink—dispatch(OperatorCommand)(lets the composition root forward decoded commands fromtelemetry_streamtooperator_bridgewithout coupling them)
- Owned by: AZ-640 initial structure task.
- Consumed by:
operator_bridge(TelemetrySink),scan_controller(VlmProvider),mission_executor(may use MavlinkSink),telemetry_stream+vlm_client(implement the traits).
Allowed Dependencies (layering)
Read top-to-bottom; an upper layer may import from a lower layer but NEVER the reverse. Same-layer imports are explicitly listed in each component's Imports from.
| Layer | Components | May import from |
|---|---|---|
| 5. Composition | autopilot (binary) |
1, 2, 3, 4 |
| 4. Brain | scan_controller |
1, 2, 3 |
| 3. Coordinators | operator_bridge, mission_executor |
1, 2 |
| 2. Actors / Transports / Storage | mavlink_layer, mission_client, frame_ingest, detection_client, movement_detector, semantic_analyzer, vlm_client, mapobjects_store, gimbal_controller, telemetry_stream |
1 |
| 1. Shared / Foundation | shared/* |
(none) |
Violations of this table are Architecture findings in code-review and are High severity. Specifically:
- A Layer 2 actor MAY NOT import a sibling Layer 2 actor. Stream dependencies (e.g.
movement_detectorconsumingFrame) are wired via constructor-injected channels by the composition root; sink dependencies (e.g.operator_bridgepushing intotelemetry_stream) are bridged via a trait inshared::contracts. - A Layer 3 coordinator MAY import any Layer 2 actor whose handle it directly calls.
operator_bridgeimportsmapopjects_storeforapply_decline.mission_executorimportsmavlink_layer,mission_client, andmapobjects_store. - A Layer 3 coordinator MAY NOT import another Layer 3 coordinator.
mission_executorconsumesMiddleWaypointHintfromoperator_bridgevia a constructor-injectedReceiver<MiddleWaypointHint>wired by the composition root.
Layout Conventions (reference)
| Language | Root | Per-component path | Public API file | Test path |
|---|---|---|---|---|
| Rust | crates/ |
crates/<component>/ |
crates/<component>/src/lib.rs |
crates/<component>/tests/ (crate-level) + tests/e2e/ (workspace-level) |
Self-verification
- Every component in
_docs/02_document/components/has a Per-Component Mapping entry (13 components +shared+autopilotbinary). - Every shared / cross-cutting concern has a Shared section entry (
models,config,error,health,observability,clock,contracts). - Layering table covers every component, with
sharedat the bottom andautopilotbinary at the top. - No component's
Imports fromlist points at a higher layer. (scan_controllerLayer 4 → Layers 1, 2, 3; coordinators Layer 3 → Layers 1, 2; actors Layer 2 → Layer 1 only.) - Paths follow Rust's
crates/<component>/convention. - No two components own overlapping paths — each
Ownsglob is rooted at a distinctcrates/<component>/**.