External ROS 2 adapter, deterministic control, simulation, safety, and evidence toolkit for Flyto2.
A robot is standard ROS 2 equipment. It is not a Flyto2 appliance.
This library is the host-side driver layer next to the equipment. It runs on
the execution host or on a companion computer beside the robot and drives the
robot's own native ROS 2 / Nav2 stack. It is never firmware, and nothing from
Flyto2 is installed on the robot: the lab robots stay stock TurtleBot3. It does
not declare capabilities to Flyto2 either; the flyto-modules-robotics pack
does that through flyto-core's @register_module capability contract. See
DECISIONS.md (2026-10-04).
Production topology:
Flyto2 Cloud / War Room
|
| goal, policy, approved capability
v
AI Space execution host or companion computer
(Mac / laptop / Steam Deck / mini PC)
|
| flyto-modules-robotics pack (declares capabilities)
| Generic ROS 2 Adapter (this library: drives the robot)
v
DDS / Zenoh / rosbridge / standard ROS 2
|
v
TurtleBot3 / other ROS 2 robot
|
| odom / scan / camera / TF / action result
v
external adapter evidence
|
v
Flyto2 independent verification
The Raspberry Pi / robot should contain only its normal operating system, ROS 2, upstream robot packages, sensors, Nav2/SLAM when needed, and generic ROS transport. It does not need a Flyto2 credential, job runner, gateway, scheduler, agent, task database, evidence database, or Flyto2-specific ROS node.
- deterministic ROS 2 motion/controller utilities;
- Nav2 / standard ROS 2 adapter contracts;
- fail-closed sensor, clearance, stop, and execution-grant logic;
- versioned execution/evidence contracts;
- Gazebo Harmonic worlds and adversarial lab scenarios;
- physical-robot regression and acceptance helpers;
- camera/resource adapters intended for an external execution computer;
- simulation and hardware evidence tooling.
The repository does not own Flyto2 scheduling or task-completion authority. A ROS action completing successfully is execution evidence, not proof that the user's objective was achieved.
Equipment adapters are installed on the AI Space execution host, or on a
companion computer beside the robot, and loaded through a host-neutral adapter
boundary. The built-in Python AI Space host uses
flyto2.external_adapters / flyto2.resource_discoverers entry points; process
hosts such as optional Flyto2 Runtime can use the versioned
flyto2.adapter-provider.v1 protocol. Adapter identity maps to the provider,
for example ros2.generic maps to flyto2-adapter-provider-ros2-generic on the
process-host path. The adapter owns ROS 2, rosbridge, OpenRMF, camera-stream and
other transport details; the selected execution host owns assignment-scoped
authority and lifecycle; Cloud owns resource/capability inventory, approval,
routing, evidence and verification. Capabilities reach Flyto2 through the
flyto-modules-robotics pack's @register_module declarations, not through
this repository.
Provider discovery is passive. An installed provider may report a
flyto.resource-manifest.v1, but discovery never grants motion or other
effects. A host that passes manifest_extensions=["module_pack"] to the
ros2.generic discoverer (or --manifest-extension module_pack to
flyto2-adapter-provider-ros2-generic --discover) also receives
"module_pack": "robotics", the flyto.modules entry-point name of the pack
that drives the resource. The discoverer offers the same name as its
module_pack attribute. Without the request the manifest keeps its released
shape, because released hosts reject unknown fields.
A host that asks for live_channels also receives the robot's read-only
flyto.live-channels.v1 block (flyto_robotics/live_channels.py): scan
(LiDAR, ws-json, 5 Hz, with the clearance floor), pose (odometry,
ws-json, 5 Hz) and camera (compressed frames, ws-jpeg, 5 Hz), each a
rosbridge://<bridge>/<topic>?type=<type> source on the bridge the adapter
uses. A channel is declared only when the graph publishes its topic with its
type, and only over rosbridge; with nothing to watch the field is left out.
The host relay only subscribes to these topics.
For execution, Runtime binds the exact commanded resource and approved capability allowlist to one assignment and exposes that authority only through a loopback endpoint to Flyto2 Core. The robot itself remains standard equipment with no Flyto2 credential or scheduler.
The current source tree contains no Flyto2 Pi job runner, robot lifecycle installer, recovery portal, doctor, delivery gateway, robot credential provisioner, or robot-side scheduler.
Those appliance components are intentionally gone from the current tree. Git history and dated handoffs retain the old failure evidence; current product code does not retain an executable path back to that topology.
Do not reintroduce Flyto2 runtime as a TurtleBot3 requirement.
A clean TurtleBot3 may be installed from upstream ROS 2 / TurtleBot3 documentation. The physical lab currently uses ROS 2 Jazzy and a Burger base with LDS-03 lidar.
Example upstream-only systemd units are in deploy/native_ros2/:
turtlebot3-bringup.servicecamera-v4l2.serviceslam-toolbox.servicenav2.service
They invoke /opt/ros/jazzy packages directly and contain no Flyto2 code.
They are site-configuration examples, not a proprietary robot runtime.
Install this package on the computer that can see the robot's ROS 2 graph:
python3 -m venv .venv
.venv/bin/pip install -e .Useful installed commands are external/lab tools such as:
flyto-robotics --help
flyto-ros2-readiness-probe --help
ros2_closed_loop_lab --help
gazebo_lab_driver --help
flyto-camera-gateway --help
flyto-resource-agent --helpThe Generic ROS 2 Adapter on the selected AI Space execution host consumes standard interfaces such as:
nav2_msgs/action/NavigateToPosenav2_msgs/action/DriveOnHeadingnav2_msgs/action/BackUpnav2_msgs/action/Spingeometry_msgs/msg/Twist/TwistStampednav_msgs/msg/Odometrysensor_msgs/msg/LaserScan- standard camera, TF, and map topics
Besides the motions above it declares, when the graph has them:
| Capability | Interface | What it does |
|---|---|---|
vision.observe |
sensor_msgs/msg/CompressedImage (FLYTO_ROS2_CAMERA_COMPRESSED_TOPIC, default /camera/image_raw/compressed) |
returns one JPEG frame, at most 2 MB |
sensing.map |
nav_msgs/msg/OccupancyGrid (FLYTO_ROS2_MAP_TOPIC, default /map) |
returns the latched map's cells, size, resolution and origin |
places.list |
host file (declared with motion.navigate) |
returns the named places saved for this robot's map, as evidence.places and a places JSON artifact |
places.mark |
host file + map_pose |
saves the robot's current map-frame pose (the same map_pose navigation arrival is judged on) under a name |
One computer's ros2.generic adapter serves exactly one resource: its
FLYTO_ROS2_RESOURCE_ID (else ros2-<host>-<ROS_DOMAIN_ID>), in its
FLYTO_ROS2_DEPLOYMENT_MODE (simulation, anything else is hardware). The
physical robot and its twin can sit behind the same endpoint, so neither the
id a host asks for nor the configured mode alone says which machine a job
would move.
adapter_provider.build_adapter(resource_id)(and the process protocol's--resource-id) refuses any other id withResourceNotServed, before any transport is opened, instead of labelling the adapter with the id it was handed.GenericROS2Adapter.served_identity()(process opserved_identity) returns{"resource_id", "deployment_mode"}: the configured resource andsimulationorreal, confirmed against the live graph (a simulator iff it publishesFLYTO_ROS2_SIM_MARKER_TOPIC, default/clock). It raisesServedIdentityErrorwhen the mode and the graph disagree, when the graph shows no interfaces, or when simulation is claimed with the marker check disabled. A Flyto2 Cloud host asks it before every job and refuses unless both match what Cloud scored.
Everything the robot serves (rosbridge, camera) binds to 127.0.0.1 on the
robot, so the SSH local forward is the transport, not a workaround. The
adapter owns it (flyto_robotics/ssh_transport.py); nobody starts a tunnel
by hand, and a robot reboot or a Wi-Fi drop is recovered on its own.
| Variable | Meaning |
|---|---|
FLYTO_ROS2_SSH_HOST |
[user@]host, e.g. ubuntu@flyto-robot.local. Setting it turns the forward on and implies FLYTO_ROS2_TRANSPORT=rosbridge (an explicit rclpy is refused). |
FLYTO_ROS2_SSH_IDENTITY |
optional private key (-i, IdentitiesOnly=yes) |
FLYTO_ROS2_SSH_KNOWN_HOSTS |
optional known_hosts file (default: ssh's own) |
FLYTO_ROS2_SSH_FORWARDS |
remote loopback ports by name, default rosbridge=9090; e.g. rosbridge=9090,camera=8080. rosbridge is always forwarded. |
FLYTO_ROS2_SSH_LOCAL_PORTS |
optional pinned local ports, e.g. rosbridge=19090; otherwise an ephemeral 127.0.0.1 port |
FLYTO_ROS2_SSH_READY_TIMEOUT_S |
how long the first build waits for the forward (10) |
FLYTO_ROS2_SSH_LINGER_S |
how long the forward outlives its last adapter (30), so per-job adapters reuse it |
FLYTO_ROSBRIDGE_URL is derived from the forward (ws://127.0.0.1:<local>);
an explicit value that differs is refused rather than silently ignored.
Security: BatchMode=yes with password and keyboard-interactive auth off
(key only); StrictHostKeyChecking=yes, never accept-new — an unknown or
changed host key is a failed state whose last_error names how to trust the
robot once (ssh-keyscan compared with ssh-keygen -lf on the robot); every
forward is 127.0.0.1 -> 127.0.0.1 and any other address on either side is
refused; -N (no remote command). A private ControlMaster socket lets a
restarted host stop a master a crashed one left holding the ports.
Lifecycle: the adapter starts ssh (ExitOnForwardFailure, ServerAliveInterval),
supervises it, re-resolves the host (mDNS) on every attempt and reconnects
with exponential backoff and jitter (1 s doubling to 30 s, each delay drawn
from its upper half). Host-key and authentication failures do not retry on
their own; they wait for an operator reconnect(). The forward stops with
the last adapter that holds it (after the linger) and when the process exits.
While the forward is down every capability call fails fast — refused, with
evidence.reason_code: "transport_unavailable" and evidence.transport —
and nothing is sent; safe_stop() and cancel() return failed with the
same evidence.
Status: GenericROS2Adapter.transport_status() (process op
transport_status; adapter_provider.transport_status(), also
discover_resource_manifests.transport_status; manifest extension
transport on request) returns
{transport, state: connected|reconnecting|failed|stopped, since, attempts, last_error, error_code, host, resolved_address, forwards, accepting_calls, rosbridge_ok}. The presence watch notifies the host transport_<state> on
every change and retries the robot at once when the forward returns.
Operator refresh: GenericROS2Adapter.reconnect() (process op reconnect;
adapter_provider.reconnect_resource(resource_id), also
discover_resource_manifests.reconnect) pauses new calls on the link, safe
stops any actuating call in flight (zero velocity, then cancel) and refuses
with reason_code: "safe_stop_unconfirmed" if that stop is not confirmed;
otherwise tears the forward and the rosbridge session down, establishes both
again, re-reads the graph and served_identity(), and returns the status
above plus refused, topics_seen, served_identity /
served_identity_error, rosbridge_error and safe_stop. It never raises
and may be repeated.
Places are kept on the execution host, never on the robot and never in Flyto2
Cloud (flyto_robotics/places.py). Each is {name, frame: "map", x, y, yaw};
names are free text (any script, 1 to 64 characters, no control characters),
unique per map ignoring case. Every write is atomic. A file that cannot be
read exactly as written is refused, never guessed at or overwritten.
| Setting | Default | Meaning |
|---|---|---|
FLYTO_ROS2_PLACES_FILE |
unset | the places file, named outright |
FLYTO_ROBOTICS_DATA_DIR |
$XDG_DATA_HOME/flyto-robotics (~/.local/share/...; %LOCALAPPDATA%\flyto-robotics on Windows) |
data dir; the file is places/<resource id>/<map id>.json under it |
FLYTO_ROS2_MAP_ID |
default |
which map the places belong to. The SLAM map lives on the robot, so the host names it; change it when the map is rebuilt |
motion.navigate takes either x and y (with optional yaw_radians) or
place, never both. A place is resolved before anything moves: an unknown
name is refused with evidence.known_places, and an unreadable file is
refused, both with no motion. The goal sent to Nav2 is the stored pose,
heading included. Every navigate result carries evidence.navigation_target
(the goal, in the map frame); a call by place also carries
evidence.resolved_arguments (x, y, yaw_radians), the arguments its
arrival evidence is judged against.
A robot that starts closer to an obstacle than Nav2's inflation_radius plus
its own radius sits in the costmap's cost gradient, and the stock controller
can fail to make progress from there ("Failed to make progress", recoveries,
an abort minutes later). Before sending a navigate goal the adapter decides
this from facts (flyto_robotics/inflation_escape.py): the nearest return in
the LiDAR's front sector against both radii, read from the costmaps' own
parameters. A pinned start backs off (only as far as the rear keeps the
clearance floor), passes one lateral waypoint on the side with more room, then
goes to the original goal, which stays the final pose. If no escape keeps the
floor the call is refused at once with reason_code: no_escape_room and the
measured clearances. The floor itself is never lowered.
The floor also holds where a navigation ends and while it runs (0.6.7; real robot 2026-10-09, run c6d8d79d: a detour's goal leg drove to 0.135 m of a person's shins while Nav2's collision monitor only slowed it):
- Before every leg (the call's goal, the escape's waypoint, the goal leg after
it) the adapter places each LiDAR return in the robot frame and measures it
against the destination (
path_clearance.judge_destination). A return closer than the floor refuses the leg: before anything moves the call isrefused, after an escape leg it isfailed, both withevidence.reason_code: destination_blockedandevidence.destination_clearance. A sweep or map pose that cannot be read refuses withpath_unreadable. Nevercompleted. - While a leg runs, the backend's floor guard reads every scan
(
braking_envelope.leg_floor_check): a return inside the floor in any direction, or the room the way the base is moving within its stopping distance, sends zero, cancels the goal and sends zero again. The result isfailedwithmotion_outcome.reason: obstacle_blockedandmotion_outcome.braking.tripped: floor_breach(kind: navigation_leg,trip.cause: inside_floor | no_room_to_stop), even when Nav2 then reports the goal succeeded. The escape's waypoint is placed the floor plus the margin beside the obstacle, so the way past keeps the floor the guard holds it to.
| Setting | Default | Meaning |
|---|---|---|
FLYTO_ROS2_INFLATION_ESCAPE |
on |
off sends every goal unchanged |
FLYTO_ROS2_COSTMAP_NODES |
/local_costmap/local_costmap,/global_costmap/global_costmap |
costmaps whose plugins, inflation layer and robot_radius/footprint are read; the largest radius of each kind is used |
FLYTO_ROS2_ESCAPE_MAX_BACKOFF_M |
0.30 |
longest back-off |
FLYTO_ROS2_ESCAPE_MARGIN_M |
0.10 |
clearance past the obstacle's edge beyond the clearance floor (the leg past it is held to the floor) |
FLYTO_ROS2_ESCAPE_MAX_LATERAL_M |
1.0 |
widest lateral offset before the escape is refused |
FLYTO_ROS2_NAVIGATE_THROUGH_ACTION |
/navigate_through_poses |
used when present; otherwise the legs are sequential NavigateToPose goals |
Captures are read-only, read one message through rosbridge (the rclpy backend
refuses them), and return it as evidence.capture; the execution host keeps
and shows it.
A result keeps every key it always had (capture, odom, motion_outcome)
and adds what a host used to work out on its own (flyto_robotics/provider_evidence.py):
| Key | When | What |
|---|---|---|
evidence_items |
every motion that ran | passage.clearance: the clearance the motion started with against this adapter's own floor (FLYTO_ROS2_MIN_CLEARANCE_M, 0.35 m) |
| a completed motion | robot.arrival: odometry before, after and once settled (advance, retreat, rotate; usable: false, the host judges it), or the pose a navigation ended at. Emitted only when odometry says the robot settled; otherwise the host observes it as before |
|
artifacts |
a completed capture | [{"kind", "media_type", "data_base64"}], the flyto.capability-contract.v1 artifact transport: the photo as the camera sent it, and the map drawn as a picture (free white, walls black, unknown grey, north up, at least 600 px wide). JPEG with Pillow installed (pip install flyto-robotics[capture]), PNG otherwise |
recovery_context |
an advance or retreat that failed or timed out | why it stopped, distance asked / travelled along its heading / remaining, the nearest return at the stop, the floor, and the LiDAR sweep at the stop. The pack's declared recovery names it |
The item shapes are the ones Flyto2 Desktop projected until now, so a host that passes them through reaches the same verdicts. Nothing waits on a failed motion: the host's safe stop comes next.
open_rmf.fleet is a second flyto2.external_adapters entry point (and
flyto2.resource_discoverers, active once FLYTO_RMF_API_URL is set). The
commanded resource is a fleet, fleet:<name>; the request names that fleet and
never a robot, so Open-RMF's dispatcher still runs the bid. A call waits, up to
its deadline, for Open-RMF to report the task completed (completed),
failed/canceled/killed (failed) or still running (timeout; the host's
cancel withdraws it). A finished motion.navigate or motion.dock reports a
robot.arrival item naming the fleet's own claim. Open-RMF has no fleet-wide
stop, so safe_stop is refused and the capabilities declare
requires_safe_stop: false: stop a machine on its own path (motion.halt).
The steps are the fleet pack in flyto-modules-robotics.
Each robot declares what its motions rest on, with FLYTO_ROS2_SAFETY_BASIS on
the execution host:
| Value | Before a motion | Bounds |
|---|---|---|
lidar_clearance (default) |
LiDAR clearance of at least FLYTO_ROS2_MIN_CLEARANCE_M (0.35 m) along the path the motion sweeps |
the declared argument ranges |
operator_present |
no LiDAR; odometry is still required so Cloud can verify the motion | 0.05 m/s and 0.3 m per advance or retreat; π/2 per turn, at Nav2's own rotation speed (Spin takes no speed); navigation refused |
The floor is checked before a motion starts along the path that motion
sweeps (flyto_robotics/path_clearance.py), chosen by the capability
contract's motion_kind, not its name:
motion_kind |
Capability | Swept path | Starts when |
|---|---|---|---|
advance / retreat |
motion.advance / motion.retreat |
corridor in the direction of travel: returns with x > 0 and abs(y) < floor + FLYTO_ROS2_ROBOT_RADIUS_M (0.1) |
the nearest of them is at least the floor away |
rotate |
motion.rotate |
the footprint turning in place, all around | every return is at least floor + FLYTO_ROS2_ROBOT_RADIUS_M away |
planned |
motion.navigate |
the path is Nav2's; every leg (back-off, waypoint, goal) is held to the floor on every scan by the floor guard | every LiDAR return is at least the floor away from the leg's destination |
A refusal carries evidence.reason_code: "path_blocked" and
evidence.path_clearance (motion_kind, shape, threshold_m,
clearance_m, limiting_side, limiting_bearing_rad, and sectors: the
nearest return ahead, left, behind, right, null where there is
none), and its detail names the side, e.g. LiDAR clearance 0.334m behind on the retreat path is below the 0.350m motion safety minimum. Without a sweep
the nearest return in any direction is judged as before.
Under lidar_clearance a
straight drive (motion.advance, motion.retreat) is then guarded on every
scan by its braking envelope (flyto_robotics/braking_envelope.py). A moving
robot cannot stop where it is told to: it covers v * t before the stop takes
effect and v^2 / (2a) while braking, so the clearance it has to stop at is
floor + v * t_latency + v^2 / (2 * a_decel)
along the way it is going. The room it measures is, for every return at
(x, y) ahead in the frame of travel, x - sqrt(floor^2 - y^2) (a return beside
the path that it would pass closer than the floor counts too). The drive starts
at no more than 0.8 * v_max(room), with v_max(room) = a(-t + sqrt(t^2 + 2 room / a)). How its speed is governed after that is decided once per drive
(braking_envelope.decide_governance) from what the running server says
implements it, looked up in PREEMPTION_BY_IMPLEMENTATION: the behavior plugin
class it loaded (<behavior>.plugin on FLYTO_ROS2_BEHAVIOR_SERVER, default
/behavior_server), else the action type it serves; and from whether the
transport can send a second goal:
planned_before_send: the server cannot take a preempting goal. Nav2'sDriveOnHeadingandBackUpcannot in any release (its behavior server answers one with "feature is currently not implemented. Aborting and stopping", then restarts from rest), nor can any undeclared type or the rclpy transport. The distance is planned with the speed, asmin(requested, room - stopping_distance(v)), so the drive ends by itself where it can still come to rest at the floor. Nothing is sent while it runs. A drive that ends there, short of the request, reportsobstacle_blockedlike a guard stop.preemptive_resend: a server declared preemptible gets the whole distance and is re-sent slower (a preempting goal with the rest of its distance) as the room shrinks.
Either way the drive is stopped (zero velocity, cancel, zero) while the room is
still at least its stopping distance, or when the LiDAR is unreadable or quiet
for longer than its staleness limit. That limit (braking_envelope.scan_stale_limit,
generic_ros2_adapter.SCAN_STALENESS) is the smaller of a rate cap (15 nominal
scan periods, never below FLYTO_ROS2_STOP_SCAN_TIMEOUT_S or 3 x the worst
recent interval, never above 2.0 s) and a floor bound: the scan age at which a
stop decided then still brings the robot to rest at or beyond the floor from
the room the last scan showed (credit + (room - stopping_distance(v)) / v - poll, where credit is the scan period the measured latency already holds).
Ages are measured from receipt on this computer's monotonic clock. Navigation
legs keep max(FLYTO_ROS2_STOP_SCAN_TIMEOUT_S, 3 x worst interval). A drive that could not stop at the floor
even at 0.02 m/s, or would have under 0.01 m left to drive, is refused before
it moves.
| Setting | Default | Meaning |
|---|---|---|
FLYTO_ROS2_STOP_LATENCY_S |
0.5 |
end-to-end stop latency. The measured budget (worst recent scan interval + scan delivery delay, twice, for the scan in and the cancel out + FLYTO_ROS2_STOP_CONTROL_PERIOD_S 0.1 + FLYTO_ROS2_STOP_ACTUATION_S 0.1) replaces it only when larger |
FLYTO_ROS2_STOP_DECEL_MPS2 |
0.5 |
achievable deceleration. The robot's declared limit (FLYTO_ROS2_DECEL_PARAMETER, default /velocity_smoother:max_decel) replaces it only when smaller |
FLYTO_ROS2_STOP_SCAN_TIMEOUT_S |
0.5 |
the shortest staleness limit a guarded motion gets (see above); a straight drive's limit is still capped at 2.0 s and by the floor bound |
A straight drive's motion_outcome carries braking (the profile and where
each value came from, the requested and commanded speeds, governance (mode,
action type, whether it preempts, and the fact that decided it), plan (the
speed and distance sent against those asked, from the room at send), every
slowdown, ended: planned_stop for a drive that ended at its planned stop
point, and the trip with its room, speed, stopping distance and the distance
the robot actually covered after it) and every motion's carries stop_clearance: the
nearest return at rest, its bearing and side (ahead, left, right,
behind) from the way the robot was going, and whether the floor held, over
every direction and along the path.
The basis is part of each motion capability's declared observations (/scan
or operator:present), so it is shown when the capability is approved and a
changed basis needs a new approval. operator_present relies on Cloud holding
every actuating capability for an operator's Run. Any other value refuses
motion.
No adapter URL or Flyto2 credential is stored on the robot. The installed
package exposes ros2.generic to the built-in AI Space plugin host and also
ships flyto2-adapter-provider-ros2-generic for process-based hosts such as
optional Flyto2 Runtime.
The self-contained Gazebo world remains a first-class test surface. It lets the same controller/safety/evidence code be exercised without claiming physical acceptance.
Common checks:
make verify
make soak
make gazebo-lab
make gazebo-matrix
make gazebo-shortcut
make ai4all-showcaseGenerated reports and simulation output are evidence about simulation only.
A physical closure is stronger than "nodes are running".
The acceptance ladder is:
- robot contains zero Flyto2 runtime;
- native ROS 2 survives cold boot and exposes fresh odom/lidar/camera/TF;
- Nav2/SLAM standard action surface is active without fabricated localization;
- an external computer discovers the standard capabilities;
- in a physically clear area, one bounded motion is executed;
- interruption/cancel produces a safe stop;
- odometry/LiDAR/camera evidence is collected independently;
- Flyto2 Cloud verifies the original objective;
- only then may War Room mark the task complete.
Current physical status is recorded in the 2026-09-21 handoff in
flyto-cloud. No new motion should be sent merely to prove source code.
The supported production-facing boundary is semantic ROS 2 execution and
evidence, not Pi lifecycle management. Start with
flyto_robotics.ros2_action_executor, flyto_robotics.ros2_execution,
flyto_robotics.ros2_execution_evidence, flyto_robotics.ros2_pairing,
and the versioned JSON contracts under contracts/. Robot-appliance
lifecycle, delivery and recovery modules have been removed from the current
source tree.
Use the repository gates before landing behavior or packaging changes:
make verify
flyto-index verify . --strictFor physical work, source checks never substitute for hardware acceptance. Generated ROS/Gazebo/evidence output stays untracked.
Read AGENTS.md, ARCHITECTURE.md, DECISIONS.md, and STATE.md
before changing runtime boundaries. New integrations must preserve the split
between execution computer and commanded equipment and must not require Flyto2
software on the robot.
| Document | Purpose |
|---|---|
| Architecture | current external-adapter boundary |
| Installation | standard robot + external computer setup |
| Capabilities | capability/controller vocabulary |
| Contracts | versioned execution/evidence contracts |
| Demo | Gazebo and lab workflows |
| Virtual Robot Lab | simulation matrix |
| Showcase evidence | evidence interpretation |
| Historical handoffs | old Pi/runtime work and findings |
Dated handoffs and Git history preserve retired-appliance evidence; current installation documentation describes only the external-adapter architecture.
This is a research/lab integration toolkit, not a certified safety controller. Real deployments need an independent emergency stop and site-specific safety engineering.
Flyto2 policy, permission, leases, evidence binding, and objective verification remain separate from low-level ROS control. The model never earns authority by claiming that it has a capability, and the robot never earns task-completion authority by reporting action success.
Apache-2.0. See LICENSE.
