ADR 0041 Spot Runs From The Dock Undock Transit Return - lukislp/mowdeck GitHub Wiki

ADR 0041: Spot runs from the dock: undock, transit and return

Status

Accepted (2026-10-15); amended by ADR 0042 (2026-10-15): the transit counts towards the progress-based time limit like the plan.

Context

A spot run (ADR 0039) started only from an idle mower on the lawn, in the same area as the spot and at most 10 m away from it. The owner had to drive the mower out of its charging station with the joystick first and bring it near the spot, and afterwards drive it home or send it home in the vendor app. The owner wants a run to start by itself from the charging station, drive to the spot, mow it and go back to the dock, also when the spot lies in another garden behind several transfer paths.

The map already holds everything needed, in the neutral form of MowerMap: the areas, the transfer paths between them (driven with the blades off), the no-go zones, the obstacles and the dock pose. The mower can leave its charging station by itself (on Mammotion: PyMammotion's leave_dock, the one-touch undocking of the app) and return to it by itself (the generic ReturnToDock).

Decision

A spot run gets three new phases around the spot itself: it undocks, drives a transit with the blades off and sends the mower home. Everything except the undocking call is generic.

Capability undock. MowerCapabilities.undock says that a connection can make the mower leave its charging station by itself; the connection then implements the optional protocol UndockCapable with leave_dock(). It only sends: the mower drives a short way out of the dock and reports idle, the run watches the state. The simulator implements it (from the dock it drives undock_distance_m, 1 m, straight out along dock.heading_deg and stands idle; option undock, default on). The Mammotion adapter sends PyMammotion's leave_dock (MctlNav(todev_one_touch_leave_pile=1)) once with Priority.USER, only from the dock, and boosts its refresh loop like after the other job commands. On a dual link it goes over the cloud: only the drive channel uses the bridge (ADR 0038, link roles).

The transit planner (mowdeck_control/transit.py, pure Python, no I/O, no geometry library; mowdeck-control keeps its standard-library-only rule). plan_transit(map, start, goal) finds the route from the mower to the first point of the plan:

  • Drivable space (DrivableSpace): the areas inset by 0.3 m, the transfer paths widened to corridors of ±0.5 m, a disc of 1.5 m around the dock (where an undocked mower stands), minus every no-go zone and obstacle buffered by 0.3 m.
  • Graph: the start and the goal; the path vertices (simplified by 5 cm, then at most 1 m apart); the concave corners of every area and the convex corners of every zone, pushed 0.2 m beyond the margin into the free side; and portals, the nearest point inside an area's inset for a node just outside it (a path end on the lawn's edge, the mower in front of the dock). Edges: consecutive path vertices (the corridor's centre line, clear of zones); straight connections inside one area between its corners, portals, path entries, the start and the goal (an exact test: the segment keeps 0.3 m from the area's edge and from every zone); and connectors of at most 2 m between any two nodes that stay in the drivable space (sampled every 5 cm): a path end to its portal, two paths that meet, the start to a nearby path.
  • Search: A* (Dijkstra with the straight-line distance as the lower bound) with lazily checked edges. A route may chain several transfer paths and pass through intermediate areas. No route, a start or goal outside the drivable space, or a route longer than max_length_m (default 300 m) raises NoRouteError; the run is then refused before anything moves, with the reason no_route: ....

The run's phases (SpotRunStatus.state): preparing, undocking, waiting_rtk, transit, spot_mowing (with paused_rtk as before), returning, then done, aborted or failed.

  • Undocking. A docked or charging mower on a connection with undock gets leave_dock (no drive frames, so no drive session). The run waits until the mower reports idle for 3 s with two consecutive reports closer than 15 cm (undocking), then for a fixed RTK position no older than 10 s (waiting_rtk). After 90 s (undock_timeout_s) it aborts with undock_failed, or rtk_lost when it was already out and waiting for the fix. While undocking only an error, an update or a lost mower end the run; a mower leaving its dock may report anything else. Without undock a docked mower is refused as before.
  • Transit. Before the start the transit is planned from the dock (a docked mower) or from the reported position (any other mower, which still needs a fixed RTK position at the start). After undocking it is planned again from the real pose. The route is prepended as a blades-off leg driven by the existing pure-pursuit loop at transit_speed_mps (0.3 m/s at most). Its geofence is the drivable space, checked on every report and every dead-reckoned estimate; the spot's own geofence (areas inset 0.3 m minus the zones) applies again from the plan's first point. The drive session (Bluetooth on demand through the bridge) opens with the first transit frame, after undocking. The 10 m approach limit is gone: the transit replaces the old straight approach leg (a straight line is simply the shortest route when it is clear).
  • Spot mowing. As in ADR 0039. The run-time limit counts from the start of the transit: min(2 × estimated + 60 s, 20 min) for the plan plus twice the transit's driving time, so a long transit gets its own share.
  • Returning. With SpotMow.return_to_dock (default true) the run sends the generic ReturnToDock to the connection after done and after every abort that left the mower healthy (geofence, rtk_lost, timeout, heading_mismatch, frame_rejected, link_lost from a refused frame, no_route, undock_failed), unless the mower already stands in its dock. Never after a stop by the user (spot_stop: the mower just stops), an emergency stop, a sensor stop, a change of activity or an internal error. The mower navigates home by itself; the run is returning while the command is sent (at most 30 s) and then ends. The report says sent_home or why the return was not sent.

Safety stays first: the blades are off during undocking, transit and return in every case (only mow and turn segments of the plan may switch them on, as before); every end of the drive part sends a zero frame, switches the blades off and closes the drive session; rule 12 keeps every other command out until the run ended.

Start conditions (controller rule 12 and spot_start_problem). A spot_mow may now be sent to a docked or charging mower when the connection has undock (and implements UndockCapable); RTK is then not required at the start because the run waits for it after undocking. Unchanged: more than 20 % battery, a fresh state, no job, no drive session or pending confirmation, the plan inside the geofence, and while a run is active only emergency_stop, spot_stop and set_light. The reachability check (no_route) runs in the Device-Gateway's controller before the command is accepted, so Fleet's record and the web app get a clear rejected with the reason.

The web app and the gateway agree on the route. The sheet's condition "Route to the spot found" replaces "same area, within 10 m". It is computed with the same algorithm as the gateway: the Planner's POST /plans/preview takes an optional transit ({from_dock: true} for a docked mower that can undock, else {position: [x, y]} in local metres, rounded to 0.5 m) and returns transit{found, from_dock, length_m, route, reason} from mowdeck_control.transit.plan_transit on the mower's map snapshot, the same map the gateway's controller holds. This respects ADR 0030: the browser asks the Planner over its public API as before, no service calls another over HTTP, and the gateway plans on its own. The preview draws the found route as a transfer before the start; during the run the gateway's path snapshot carries the transit as the first transfer polyline. A docked mower that can undock counts as ready and its RTK row as green (the run waits for the fix after undocking). The run bar shows the new states, and the reasons undock_failed and no_route in German and English.

Owner script. try_real_mower.py --spot-dry-run --from-dock runs the whole cycle on the real mower with the blades off: it prints the planned transit, sends leave_dock, drives the transit and a 1 m circle around --spot-at X,Y (or the centre of --spot-area, or of the area nearest to the dock) and sends the mower home.

Manufacturer independence

Generic, the same for every manufacturer: the capability flag undock and the protocol UndockCapable, the transit planning on the neutral MowerMap (plan_transit, DrivableSpace), the run phases (undocking wait, RTK wait, transit with its geofence and time share, return), the start conditions in the controller, the Planner's transit preview, the web conditions and labels, and the simulator's implementation. The spot-run and transit tests in mowdeck-control, the gateway's and the Planner's tests import no manufacturer package.

Adapter work, once per manufacturer: the leave_dock() call that makes the mower leave its charging station, and capabilities.undock. The return uses the generic ReturnToDock every adapter already has. For Mammotion that is one PyMammotion command sent over the cloud.

Consequences

Easier: a spot anywhere in the mapped gardens can be mown with one tap from the charging station, also behind several transfer paths, and the mower comes home by itself. The web app shows the route before the start and refuses a start the gateway would refuse. Harder: the run lasts longer and depends on the map's paths being drivable; a transit is driven at 1 Hz reports like the spot itself, so it is slow (300 m take about 17 minutes at 0.3 m/s). The transit's corridor of ±0.5 m around a path leaves little room for steering errors; a geofence abort then sends the mower home with its own navigation.

To check on hardware before the first real run: what the LUBA reports while it leaves the dock (activity, how far it drives out, how long until idle), whether the undocked pose lies within 1.5 m of the mapped dock pose (the dock apron of the drivable space), the report rate of an idle mower right after undocking without a drive session (the boost after leave_dock should give a report every few seconds; the transit pauses until reports are fresh), whether the dock's heading_deg points out of the dock, and the pure-pursuit tracking along a real transfer path corridor. A spot_stop while the mower undocks ends the run but does not interrupt the mower's own undocking manoeuvre (about a metre, no blades); whether the emergency stop halts it on the LUBA is part of the same check.


Mirrored from docs/adr/0041-spot-runs-from-the-dock-undock-transit-return.md at commit a117684672263ba4f0b7e2e3538233456122d24e. Edit the file in the repository, not this wiki page.