devkit_ui.obstacles

Obstacle management for the Sowbot webui.

Owns the obstacle list, its YAML persistence, the /obstacles publisher, and the UI cards rendered on the Nav and Mission tabs. Designed to be attached to a NiceGuiNode (see ObstacleManager.attach) so it can read the node’s GPS/odom state and publish on its behalf, without inheriting from rclpy.node.Node itself.

Threading model

self._obstacles 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.

Two races are deliberately accepted:

  1. Status string: two concurrent writers can race on obstacle_status; last writer wins. The string is human-readable progress only, so a late “saved” message can briefly mask an earlier “writing…” — never misleading enough to matter.

  2. Topic vs file: _persist_and_publish snapshots under the lock, then writes the file, then publishes — both lock-free. An external topic observer can therefore briefly see a state newer than what’s on disk (by milliseconds). The next save converges.

Both are commented at the relevant call sites.

Coordinate frames

Obstacles are stored in WGS84 lat/lon — matches NavSatFix and Leaflet. They are projected to the local map frame at publish time using the (odom_xy ↔ gps_latlon) anchor from the host node’s latest_odom and latest_gps. Without an anchor we cannot project, so we don’t publish — a costmap subscriber would otherwise treat raw lat/lon as metric and place obstacles thousands of kilometres from the origin.

Functions

attach_mission_obstacle_panel(handle)

Renders the obstacle list + Leaflet rendering panel in the current ui context.

attach_mission_sidebar_controls(node, ...)

Renders the Field/Obstacle draw-mode toggle, shape sub-toggle, radius input, obstacle padding input, and Finish-polygon button in the current ui context (the Mission sidebar column).

attach_nav_card(node, manager)

Render the “Mark Obstacle” card and keep its status display synchronized with the obstacle manager.

circle_to_ring_ll(center_lat, center_lon, ...)

Tessellate a circle into a closed WGS84 ring.

latlon_to_xy(lat, lon, lat0, lon0)

Convert (lat, lon) to local ENU (x, y) metres relative to (lat0, lon0).

obstacle_to_ring_ll(obs)

Single obstacle dict → closed lat/lon ring, or [] for degenerate input.

obstacles_to_rings_ll(obstacles)

All obstacles → F2C-ready list of rings.

validate_obstacle(kind, lat, lon, radius_m, ...)

Validate add() inputs.

xy_to_latlon(x, y, lat0, lon0)

Inverse of latlon_to_xy.

Classes

MissionDrawHandle(node, manager, ...)

State + accessors shared between the sidebar controls and the list panel.

ObstacleManager([path])

Owns the obstacle list, persistence, and the ROS publisher.

class devkit_ui.obstacles.MissionDrawHandle(node, manager: ObstacleManager, mission_map, obstacle_pad_widget)[source]

Bases: object

State + accessors shared between the sidebar controls and the list panel. Created by attach_mission_sidebar_controls; consumed by attach_mission_obstacle_panel.

clear_in_progress() → None[source]

Tear down any in-progress polygon. Called by the outer Clear button so user intent (‘clear everything’) actually clears.

class devkit_ui.obstacles.ObstacleManager(path: str = '/workspace/maps/obstacles.yaml')[source]

Bases: object

Owns the obstacle list, persistence, and the ROS publisher.

After attach(node), the node exposes:

node.obstacles : tuple[dict, …] read-only snapshot node.obstacles_version : int bumps on every change node.obstacle_status : str last-action status node.default_obstacle_radius : float shared by UI bindings

add(kind: str, *, lat: float | None = None, lon: float | None = None, radius_m: float = 0.5, points_ll: list | None = None, name: str = '') → str | None[source]

Add a circle or polygon obstacle. Returns the allocated name on success, None on failure (status carries the reason either way).

attach(node) → None[source]

Wire into a NiceGuiNode. Creates publisher, kicks off background load, starts the cold-start anchor watcher.

delete(name: str) → bool[source]
rings_ll() → list[list[tuple[float, float]]][source]

F2C-ready snapshot. Safe to call from any thread.

devkit_ui.obstacles.attach_mission_obstacle_panel(handle: MissionDrawHandle) → None[source]

Renders the obstacle list + Leaflet rendering panel in the current ui context. Call from below the map (wide layout).

devkit_ui.obstacles.attach_mission_sidebar_controls(node, manager: ObstacleManager, mission_map, boundary_click: Callable[[float, float], None]) → MissionDrawHandle[source]

Renders the Field/Obstacle draw-mode toggle, shape sub-toggle, radius input, obstacle padding input, and Finish-polygon button in the current ui context (the Mission sidebar column).

Also wires the mission_map’s ‘map-click’ handler to dispatch between boundary-drawing (callback) and obstacle-drawing (manager.add).

Returns a handle the caller passes to attach_mission_obstacle_panel.

devkit_ui.obstacles.attach_nav_card(node, manager: ObstacleManager) → None[source]

Render the “Mark Obstacle” card and keep its status display synchronized with the obstacle manager.

Parameters:
  • node – UI node used to store the default radius, GPS data, and status.

  • manager (ObstacleManager) – Manager used to add and remove obstacles.

devkit_ui.obstacles.circle_to_ring_ll(center_lat: float, center_lon: float, radius_m: float) → list[tuple[float, float]][source]

Tessellate a circle into a closed WGS84 ring. Side count scales with radius, clamped to [16, 64].

devkit_ui.obstacles.latlon_to_xy(lat: float, lon: float, lat0: float, lon0: float) → tuple[float, float][source]

Convert (lat, lon) to local ENU (x, y) metres relative to (lat0, lon0).

devkit_ui.obstacles.obstacle_to_ring_ll(obs: dict) → list[tuple[float, float]][source]

Single obstacle dict → closed lat/lon ring, or [] for degenerate input.

devkit_ui.obstacles.obstacles_to_rings_ll(obstacles) → list[list[tuple[float, float]]][source]

All obstacles → F2C-ready list of rings. Drops degenerate entries.

devkit_ui.obstacles.validate_obstacle(kind: str, lat, lon, radius_m, points_ll) → str | None[source]

Validate add() inputs. Returns an error string or None.

Module-level so tests don’t have to instantiate ObstacleManager.

devkit_ui.obstacles.xy_to_latlon(x: float, y: float, lat0: float, lon0: float) → tuple[float, float][source]

Inverse of latlon_to_xy.