devkit_ui.missions.store
Mission storage and scheduling for the Sowbot webui.
Owns the mission list, its YAML persistence, and the ACTIONS registry. Designed to be attached to a NiceGuiNode (see MissionStore.attach) so the node exposes a read-only snapshot and a version counter that the UI timer polls to decide when to rebuild. Does not inherit from rclpy.node.Node.
Unlike obstacles, missions are not broadcast on a ROS topic — they exist to drive the executor in ui_node.py, which dispatches sowbot_row_follow goals and toggles tool topics. The store knows nothing about ROS itself.
Threading model
self._missions is an immutable tuple. All writes replace it wholesale under self._lock, and bump self._version in the same critical section. Readers grab the tuple reference lock-free (CPython attribute reads are atomic) and walk a consistent snapshot. Same pattern as obstacles.py.
Persistence is serialised through a single background writer thread (self._write_q). Callers snapshot the current tuple inside the lock and enqueue (snapshot, status_msg). The writer drains the queue, keeping only the most-recent snapshot per wakeup, so rapid back-to-back writes do not pile up threads and the status message is never stale.
Scheduling
Each mission carries one optional integer field, repeat_every_hours:
None → one-shot: runs once when active, auto-deactivates on success. N → recurring: re-arms N hours after the last successful run.
today_queue() is the only scheduler primitive: it returns (mission_id, row_id, action, action_params) tuples for every active mission that is due right now. A mission is due if it has never run, if its last run failed (retry asap — operator-gated, no automatic loop because the executor is operator-triggered), or if its repeat interval has elapsed. There is no cron, no calendar, no time-of-day. If that’s ever asked for, it’s a separate scheduler, not this one.
record_run(mid, success) is the convergence point: both the scheduled executor and any manual “Run now” path call it. That keeps the repeat interval anchored on actual robot activity rather than wall-clock ticks that ignore manual runs.
reset(mid) is the re-arm path: clears last_run_at / last_run_success and re-activates. Use it to re-run a completed one-shot (e.g. crop re-planted) or to immediately retry a failed mission outside the normal scheduler pass.
Action registry
ACTIONS is a module-level dict for now. The tool_topic is the std_msgs/Bool topic the executor publishes True/False on to engage and disengage the implement; None means the action drives only. The dict shape is the documented seam — when a third action with parameters (RPM, depth, blade height) arrives, refactor to a proper class.
Timestamp format
Timestamps are written as ISO 8601 UTC: 2025-04-01T09:30:00Z. Records written by earlier versions of this module (dd-mm-yyyy_hh-mm-ss) are read transparently by _parse_ts and silently migrated to ISO 8601 on the next save.
next_due_in_hours() sentinels
Import DUE_NOW and DUE_FAILED rather than comparing against magic floats:
DUE_NOW (0.0) mission is active and has never run DUE_FAILED (-1.0) mission is active and last run failed > 0.0 hours until the recurring interval elapses None inactive, completed one-shot, or unknown id
Functions
|
Return an error string, or None if OK. |
Classes
|
Owns the mission list and its YAML persistence. |
- class devkit_ui.missions.store.MissionStore(path: str = '/workspace/maps/missions.db')[source]
Bases:
objectOwns the mission list and its YAML persistence.
After attach(node), the node exposes:
node.missions : tuple[dict, …] read-only snapshot node.missions_version : int bumps on every change node.mission_status : str last-action status
Mission record schema:
id: ‘MISSION_1’ (allocated, immutable) name: str (operator label; defaults to id) rows: list[str] (topo entry-node names) action: str (key in ACTIONS) action_params: dict (per-action parameter overrides) repeat_every_hours: int | None (None == one-shot) active: bool created_at: str (ISO 8601 UTC) last_run_at: str | None last_run_success: bool | None
- add(*, rows: list, action: str, action_params: dict | None = None, name: str = '', repeat_every_hours: int | None = None, active: bool = True) str | None[source]
Add a mission. Returns its allocated id, or None on failure. Status carries the reason either way.
- close() None[source]
Release backend resources (the SQLite connection). A no-op for the YAML backend.
- find(mid: str) dict | None[source]
Lock-free single-mission lookup by id. Returns the dict from the current snapshot — treat as read-only.
- find_by_name(name: str) dict | None[source]
Lock-free lookup by operator name. Returns the first match. Useful for collision checks before add() (e.g. the UI_RUN record the executor creates to anchor repeat intervals).
- next_due_in_hours(mid: str) float | None[source]
For the UI chip. Returns one of:
DUE_NOW (0.0) active, never run yet DUE_FAILED (-1.0) active, last run failed — retry sentinel > 0.0 hours until the recurring interval elapses None inactive, completed one-shot, or unknown id
Import DUE_NOW / DUE_FAILED from this module rather than comparing against magic floats. UI rendering pattern:
h = store.next_due_in_hours(mid) if h is None: label = ‘done’ elif h == DUE_FAILED: label = ‘retry’ elif h == DUE_NOW: label = ‘due now’ else: label = f’in {h:.1f}h’
- record_run(mid: str, success: bool) bool[source]
Record a run outcome. Called by the executor (and any ‘Run now’ path) so the repeat interval re-arms from actual robot activity regardless of who triggered the run.
Side effect: one-shot missions (repeat_every_hours is None) that complete successfully are auto-deactivated.
Returns False for an unknown ID; otherwise returns whether the backend accepted the update.
- reset(mid: str) bool[source]
Re-arm a mission: clear run history and re-activate.
- The canonical path for:
a completed one-shot that needs to run again (e.g. crop re-planted),
a failed mission the operator wants to retry without waiting for the next scheduled executor pass.
Does not modify other mission fields. Returns False for an unknown ID; otherwise returns whether the backend accepted the update.
- set_active(mid: str, active: bool) bool[source]
Convenience wrapper around update(). The common toggle.
- devkit_ui.missions.store.validate_mission(name: str, rows: list, action: str, repeat_every_hours: int | None) str | None[source]
Return an error string, or None if OK.
Expects name to already be cleaned (uppercase, only [A-Z0-9_]). MissionStore.add() and .update() clean before calling; external callers should do the same or pass name=’’ to use the id default.