Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/contents/publishing.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,13 @@ publish_system_stats(**kwargs) -> None
publish_robot_system_stats(robot_id: str, **kwargs) -> None
```

Stores system stats (CPU, RAM, disk usage) to be published at the end of the execution loop. If no stats are stored for a robot during the loop iteration, default values are published automatically.
Stores system stats (CPU, RAM, disk usage) to be published at the end of the execution loop. If no stats are stored for a robot during the loop iteration, default values are published automatically. Nothing is published for a robot whose online check reports it offline (see below).

All percentage values should be floats between 0.0 and 1.0 (e.g., 0.45 for 45%).

This ensures that system stats are always published for all robots in the fleet, even if the connector does not explicitly provide values. This is to ensure stability of the online status of the robot in the UI, as it forces state requests if the robot was to appear offline.
This ensures that system stats are published for every online robot in the fleet, even if the connector does not explicitly provide values. This is to ensure stability of the online status of the robot in the UI, as it forces state requests if the robot was to appear offline.

Robots reported offline by `_is_robot_online()` (single-robot) / `_is_fleet_robot_online(robot_id)` (fleet) are skipped, as otherwise their state would be interpreted as online.

By default, zeroed values are used. To use the connector host's actual system stats as defaults, set `publish_connector_system_stats=True` when initializing the connector. See [FleetConnector constructor](specification/connector#spec-connector-fleetconnector-constructor) for details.

Expand Down
8 changes: 5 additions & 3 deletions docs/contents/specification/connector.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ Return `None` if the map can’t be fetched.

The Edge SDK uses this callback to determine if a robot should be considered online. It is invoked when InOrbit sends a `get_state` request, which happens automatically when the robot is marked as offline but system stats are still being received. Default implementation returns `True`.

The framework also calls it once per robot on every execution loop iteration, to decide whether to publish system stats at all: while it returns `False`, no system stats are published for that robot, keeping the robot offline in InOrbit. Keep the implementation cheap and non-blocking (read cached state; no network or other blocking I/O), or the connector's event loop will stall. If it raises, the framework logs a warning and publishes anyway.

<a id="spec-connector-fleetconnector-lifecycle"></a>
### `start()` / `join()` / `stop()`

Expand Down Expand Up @@ -148,6 +150,8 @@ Publishes pose for one robot. If the `frame_id` differs from the last published

If no stats are stored for a robot during the loop iteration, default values are published automatically. By default, zeroed values are used. To use the connector host's actual system stats as defaults, set `publish_connector_system_stats=True` in the [constructor](#spec-connector-fleetconnector-constructor).

Stats stored for a robot whose [`_is_fleet_robot_online()`](#spec-connector-fleetconnector-is-online) returns `False` are dropped rather than published.

If immediate publishing is required, use `_get_robot_session(robot_id)` to access the underlying `RobotSession` and call `publish_system_stats()` directly.

<a id="spec-connector-fleetconnector-get-robot-session"></a>
Expand Down Expand Up @@ -189,7 +193,7 @@ Single-robot convenience for map fetching. The framework uses it by delegating `

**Optional override.**

Single-robot convenience for online status. The fleet-level online check delegates to this method. Called when InOrbit requests state due to a discrepancy between the robot's offline status and incoming system stats.
Single-robot convenience for online status. The fleet-level online check delegates to this method. Called when InOrbit requests state due to a discrepancy between the robot's offline status and incoming system stats, and once per execution loop iteration to decide whether to publish system stats at all. See [`_is_fleet_robot_online()`](#spec-connector-fleetconnector-is-online) for the constraints that puts on the implementation.

<a id="spec-connector-connector-publishing"></a>
### Publishing wrappers
Expand All @@ -207,5 +211,3 @@ Single-robot convenience for online status. The fleet-level online check delegat
**Callable (advanced).**

Returns the underlying Edge SDK session for the current robot.


6 changes: 4 additions & 2 deletions docs/contents/usage/fleet.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ All publishing methods require a `robot_id` parameter. See the [Publishing Guide
- `publish_robot_pose(robot_id, x, y, yaw, frame_id)`: Publish pose for a specific robot
- `publish_robot_odometry(robot_id, **kwargs)`: Publish odometry for a specific robot
- `publish_robot_key_values(robot_id, **kwargs)`: Publish key-values for a specific robot
- `publish_robot_system_stats(robot_id, **kwargs)`: Defer publishing of system stats for a specific robot; defaults are published if not called
- `publish_robot_system_stats(robot_id, **kwargs)`: Defer publishing of system stats for a specific robot; defaults are published if not called, and nothing is published while the robot is offline
- `publish_robot_map(robot_id, frame_id, is_update=False)`: Publish map for a specific robot

## Background tasks
Expand Down Expand Up @@ -178,7 +178,9 @@ def _is_fleet_robot_online(self, robot_id: str) -> bool:
return self._fleet_manager.is_robot_online(robot_id)
```

This callback is invoked when InOrbit sends a `get_state` request, which happens automatically when the robot is marked as offline but system stats are still being received. The connector framework always publishes system stats for all robots (even zeroed defaults), ensuring that any online/offline discrepancy is detected and corrected.
This callback is invoked when InOrbit sends a `get_state` request, which happens automatically when the robot is marked as offline but system stats are still being received. The connector framework publishes system stats for every online robot, ensuring that any online/offline discrepancy is detected and corrected.

It is also called once per robot on every execution loop iteration, to decide whether to publish system stats at all. While it returns `False`, no system stats are published for that robot, keeping the robot offline in InOrbit. Keep it cheap and non-blocking (read cached state; no API call in the hot path), or the connector's event loop will stall.

## Example Execution Loop

Expand Down
3 changes: 2 additions & 1 deletion docs/contents/usage/single-robot.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ async def _execution_loop(self) -> None:

Override this method to provide custom robot health checks. The default implementation assumes the robot is online if the connector is running. This callback is invoked when InOrbit sends a `get_state` request, which happens automatically when the robot is marked as offline but system stats are still being received.

It is also called on every execution loop iteration, to decide whether to publish system stats at all. While it returns `False`, no system stats are published, keeping the robot offline in InOrbit. Keep it cheap and non-blocking (read cached state; no API call in the hot path), or the connector's event loop will stall.

```python
def _is_robot_online(self) -> bool:
"""Check if the robot is online.
Expand Down Expand Up @@ -216,4 +218,3 @@ Scripts are automatically registered and can be executed from InOrbit.
- **Simple connector**: [examples/simple-connector/connector.py](https://github.com/inorbit-ai/inorbit-connector-python/blob/main/examples/simple-connector/connector.py)
- **Robot connector (CLI)**: [examples/robot-connector/](https://github.com/inorbit-ai/inorbit-connector-python/tree/main/examples/robot-connector)
- **Examples index**: [examples/README.md](https://github.com/inorbit-ai/inorbit-connector-python/blob/main/examples/README.md)

47 changes: 39 additions & 8 deletions inorbit_connector/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,9 @@ def publish_robot_system_stats(self, robot_id: str, **kwargs) -> None:
System stats are stored and published after the execution loop completes. If no
stats are stored for a robot, default zeroed values are published instead.

Stats stored for a robot whose _is_fleet_robot_online() returns False are
dropped rather than published. @see __publish_pending_system_stats.

Note:
If immediate publishing is required, use `_get_robot_session(robot_id)` to
access the underlying RobotSession and call `publish_system_stats()`
Expand Down Expand Up @@ -1104,15 +1107,22 @@ def __publish_pending_system_stats(self) -> None:

This method is called automatically at the end of each execution loop iteration.
For each robot in the fleet:
- If system stats were stored via publish_robot_system_stats(), those are
published
- If _is_fleet_robot_online(robot_id) returns False, nothing is published for
that robot and any stats stored for it are dropped
- Else if system stats were stored via publish_robot_system_stats(), those are
published
- Otherwise, default values are published (connector host stats if
publish_connector_system_stats is enabled, zeroed values otherwise).

The reason publishing system stats is deferred is to ensure at least one system
stats message is published for each robot, even if the connector does not
explicitly provide values. This ensures stability of the online status of the
robot in the UI, as it forces state requests if the robot was to appear offline.

Offline robots are skipped because that forced state request is answered with
the robot's offline status, which refreshes its offline timestamp in InOrbit on
every loop iteration. Nothing is lost: the state request only helps when the
robot is online but InOrbit believes otherwise.
"""
default_values = (
self.__get_connector_system_stats()
Expand All @@ -1130,10 +1140,22 @@ def __publish_pending_system_stats(self) -> None:
self.__pending_system_stats = {}

for robot_id, session in sessions.items():
if pending_status := pending.get(robot_id):
session.publish_system_stats(**pending_status)
else:
session.publish_system_stats(**default_values)
try:
online = self._is_fleet_robot_online(robot_id)
except Exception as e:
# Match the edge-sdk's get_state fallback: assume online on error, so
# a broken health check keeps publishing instead of muting the robot.
self._logger.warning(f"Online check failed for '{robot_id}': {e}")
online = True
if not online:
# Stats for an offline robot make InOrbit request state, and the
# offline reply refreshes the robot's offline timestamp on every
# loop iteration. Any stats stored for it are dropped.
self._logger.debug(
f"Skipping system stats publish for '{robot_id}': robot is offline"
)
continue
session.publish_system_stats(**(pending.get(robot_id) or default_values))

# Methods meant to be extended by subclasses
@abstractmethod
Expand Down Expand Up @@ -1210,7 +1232,11 @@ def _is_fleet_robot_online(self, robot_id: str) -> bool:
checks (e.g., API connectivity, robot state, etc.).

NOTE: State will automatically be requested from InOrbit if the robot is marked
as offline but system stats are sent.
as offline but system stats are sent. Because of that, the framework also calls
this method once per robot on every execution loop iteration to decide whether
to publish system stats at all (see __publish_pending_system_stats). Keep the
implementation cheap and non-blocking: read cached state, do not perform
network or other blocking I/O here, or the connector's event loop will stall.

Args:
robot_id (str): The robot ID to check
Expand Down Expand Up @@ -1303,7 +1329,12 @@ def _is_robot_online(self) -> bool:
health checks (e.g., API connectivity, robot state, etc.).

NOTE: State will automatically be requested from InOrbit if the robot is marked
as offline but system stats are sent.
as offline but system stats are sent. Because of that, the framework also calls
this method on every execution loop iteration to decide whether to publish
system stats at all: while it returns False, no system stats are published for
the robot, so its offline timestamp in InOrbit stops being refreshed. Keep the
implementation cheap and non-blocking: read cached state, do not perform
network or other blocking I/O here, or the connector's event loop will stall.

Returns:
bool: True if robot is online, False otherwise.
Expand Down
47 changes: 47 additions & 0 deletions tests/test_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,53 @@ def test_publish_pending_system_stats_mixed_stored_and_default(
hdd_usage_percentage=0.0,
)

def test_publish_pending_system_stats_skips_offline_robots(
self, fleet_connector, mock_robot_session_pool
):
"""Test that nothing is published for robots reported offline."""
# TestRobot2 is offline: publishing stats for it would make InOrbit request
# state and refresh its offline timestamp on every loop iteration.
fleet_connector._is_fleet_robot_online = lambda robot_id: (
robot_id != "TestRobot2"
)
# Stored stats must be dropped too, not just the defaults.
fleet_connector.publish_robot_system_stats(
"TestRobot2", cpu_load_percentage=0.5
)

fleet_connector._FleetConnector__publish_pending_system_stats()

session1 = fleet_connector._get_robot_session("TestRobot1")
session2 = fleet_connector._get_robot_session("TestRobot2")
session1.publish_system_stats.assert_called_once_with(
cpu_load_percentage=0.0,
ram_usage_percentage=0.0,
hdd_usage_percentage=0.0,
)
session2.publish_system_stats.assert_not_called()
assert len(fleet_connector._FleetConnector__pending_system_stats) == 0

def test_publish_pending_system_stats_publishes_when_online_check_fails(
self, fleet_connector, mock_robot_session_pool
):
"""Test that a raising online check falls back to publishing."""

def boom(robot_id):
raise RuntimeError("health check exploded")

fleet_connector._is_fleet_robot_online = boom

fleet_connector._FleetConnector__publish_pending_system_stats()

for robot_id in ("TestRobot1", "TestRobot2"):
fleet_connector._get_robot_session(
robot_id
).publish_system_stats.assert_called_once_with(
cpu_load_percentage=0.0,
ram_usage_percentage=0.0,
hdd_usage_percentage=0.0,
)

def test_publish_connector_system_stats_uses_psutil(
self, base_model, mock_robot_session_pool
):
Expand Down
Loading