Commands Handling¶
Commands from InOrbit are automatically routed to your _inorbit_robot_command_handler() method in the case of a fleet connector, or to your _inorbit_command_handler() method in the case of a single-robot connector.
The fleet handler _inorbit_robot_command_handler() receives the following parameters:
robot_id(str): The ID of the robot receiving the commandcommand_name(str): The name of the commandargs(list): Command argumentsoptions(dict): Options includingresult_functionto report results
While the single-robot handler _inorbit_command_handler() omits the robot_id parameter.
@override
async def _inorbit_robot_command_handler(
self, robot_id: str, command_name: str, args: list, options: dict
) -> None:
"""Handle InOrbit commands for a specific robot."""
if command_name == "start_mission":
result = await self._fleet_manager.send_command(
robot_id, "start_mission", args[0]
)
A more complete example including all provided utilities is available at the bottom of this page.
Reporting Command Results¶
The options dictionary contains a result_function with the following signature:
options['result_function'](
result_code: CommandResultCode,
execution_status_details: str | None = None,
stdout: str | None = None,
stderr: str | None = None,
) -> None
The
result_codeparameter must be set toCommandResultCode.SUCCESSorCommandResultCode.FAILUREto report the command result.The
execution_status_detailsandstderrparameters are optional and can be used to provide specific error details.The
stdoutparameter is optional and can be used to provide specific output details.
Reporting Success¶
The commands handler can call the result_function with a CommandResultCode.SUCCESS to report a successful command execution before returning. The stdout parameter is optional and can be used to provide specific output details.
options["result_function"](
CommandResultCode.SUCCESS,
stdout="Mission dispatched with ID 123456",
)
Reporting Failure¶
There are three ways to report a failed command execution:
(Recommended) Raise a
CommandFailureexception.Raise any other exception.
Call the
result_functionwith aCommandResultCode.FAILUREand provide theexecution_status_detailsandstderrparameters.
Unhandled exceptions raised during the execution of the command handler will be caught and reported with a generic error message in its execution details, and the exception message attached to the stderr field. All exceptions are logged.
The CommandFailure exception can be used to intentionally indicate a failure:
Error details are automatically passed to InOrbit’s result function
execution_status_detailsis displayed in alert messages when commands are dispatched from the actions UIBoth
execution_status_detailsandstderrare available in audit logs and through the action execution details API
Example usage:
from inorbit_connector.connector import CommandResultCode, CommandFailure
@override
async def _inorbit_robot_command_handler(
self, robot_id: str, command_name: str, args: list, options: dict
) -> None:
"""Handle InOrbit commands for a specific robot."""
if command_name == "start_mission":
try:
if robot_id not in self.robot_ids:
raise CommandFailure(
execution_status_details=f"Robot {robot_id} not found in fleet",
stderr="Invalid robot ID"
)
result = await self._fleet_manager.send_command(
robot_id, "start_mission", args[0]
)
if not result:
raise CommandFailure(
execution_status_details=f"Failed to start mission for {robot_id}",
stderr="Fleet manager returned error"
)
options["result_function"](CommandResultCode.SUCCESS)
except ValueError as e:
raise CommandFailure(
execution_status_details=f"Invalid parameters for {robot_id}",
stderr=str(e)
)
Parsing Script Arguments¶
When handling custom script commands, the Edge SDK delivers arguments in the form:
args[0]: script file name (e.g.,"script.sh")args[1]: a flat list of alternating keys and values (e.g.,["x", "1.0", "y", "2.0"])
In the case of InOrbit actions of RunScript type (identified with the command_name COMMAND_CUSTOM_COMMAND),
the script name corresponds to the filename field of the action definition,
and the arguments key-value pairs.
For details on how to configure actions with arguments, refer to the InOrbit Actions Definitions documentation.
Use the helper parse_custom_command_args() to turn these into a script name and a parameters dictionary:
from inorbit_connector.connector import (
parse_custom_command_args,
CommandResultCode,
CommandFailure,
)
from inorbit_edge.commands import COMMAND_CUSTOM_COMMAND
@override
async def _inorbit_robot_command_handler(
self, robot_id: str, command_name: str, args: list, options: dict
) -> None:
if command_name == COMMAND_CUSTOM_COMMAND:
# args format: [file_name, [k1, v1, k2, v2, ...]]
script, params = parse_custom_command_args(args)
# Example: script == "script.sh", params == {"x": "1.0", "y": "2.0"}
# Use script and params as needed
options["result_function"](
CommandResultCode.SUCCESS,
stdout=f"Ran {script} with params {params}",
)
It is recommended to complement its use with the CommandModel class (see Using CommandModel for Type-Safe Argument Parsing) for safe type validation and parsing.
Using CommandModel for Type-Safe Argument Parsing¶
For structured command arguments that require validation and type safety, use the CommandModel base class. This is particularly useful when commands have multiple parameters with specific types and validation rules.
CommandModel provides:
Automatic type validation and conversion using Pydantic
Conversion of validation errors to
CommandFailureexceptionsProtection against extra fields (forbids unknown parameters)
Optionally, you can combine CommandModel with ExcludeUnsetMixin to exclude unset fields from model dumps, which is useful for API calls where you only want to send non-default values.
Basic Usage¶
Define a command model by subclassing CommandModel:
from inorbit_connector.commands import CommandModel, parse_custom_command_args
from inorbit_connector.connector import CommandResultCode, CommandFailure
class CommandQueueMission(CommandModel):
"""Command model for queue_mission command."""
mission_id: str
robot_id: int | None = None
priority: int | None = None
description: str | None = None
Using with ExcludeUnsetMixin¶
To exclude unset fields from model dumps (useful for API calls), inherit from both ExcludeUnsetMixin and CommandModel. The mixin must come first in the inheritance list:
from inorbit_connector.commands import CommandModel, ExcludeUnsetMixin
class CommandQueueMission(ExcludeUnsetMixin, CommandModel):
"""Command model for queue_mission command."""
mission_id: str
robot_id: int | None = None
priority: int | None = None
description: str | None = None
Using with parse_custom_command_args¶
CommandModel works seamlessly with parse_custom_command_args():
from inorbit_edge.commands import COMMAND_CUSTOM_COMMAND
@override
async def _inorbit_robot_command_handler(
self, robot_id: str, command_name: str, args: list, options: dict
) -> None:
if command_name == COMMAND_CUSTOM_COMMAND:
script_name, script_args = parse_custom_command_args(args)
if script_name == "queue_mission":
# Validation happens automatically - raises CommandFailure on error
command = CommandQueueMission(**script_args)
# If using ExcludeUnsetMixin, model_dump() excludes unset fields
# Useful for API calls where you only want to send non-default values
await self._fleet_client.schedule_mission(**command.model_dump())
options["result_function"](CommandResultCode.SUCCESS)
Automatic Error Handling¶
If validation fails, CommandModel automatically raises a CommandFailure with appropriate error details:
# If script_args contains invalid data:
# script_args = {"mission_id": "test", "priority": "not_an_int"}
command = CommandQueueMission(**script_args)
# Raises CommandFailure with execution_status_details="Bad arguments"
# and stderr containing the validation error details
The exception is automatically handled by the connector’s command execution framework, so you don’t need to catch ValidationError exceptions.
See the Reporting Failure section for more details.
Excluding Unset Fields¶
When using ExcludeUnsetMixin, model_dump() excludes fields that weren’t explicitly set, which is useful when making API calls where you only want to send non-default values:
# With ExcludeUnsetMixin
command = CommandQueueMission(mission_id="test123", priority=5)
command.model_dump()
# Returns: {"mission_id": "test123", "priority": 5}
# Note: robot_id and description are excluded since they weren't set
# Without ExcludeUnsetMixin
class SimpleCommand(CommandModel):
mission_id: str
priority: int | None = None
command = SimpleCommand(mission_id="test123")
command.model_dump()
# Returns: {"mission_id": "test123", "priority": None}
# All fields are included, even if they have default values
Example Usage¶
Here’s a concrete example of using CommandModel with ExcludeUnsetMixin to handle a custom command.
The command is a RunScript action, whose filename is schedule_mission.
# ActionDefinition.yaml
apiVersion: v0.1
kind: ActionDefinition
metadata:
id: dock
scope: tag/my-account-id/my-collection-id # Or any applicable scope
spec:
type: RunScript
arguments:
- name: filename
type: string
value: schedule_mission
- name: mission_id
type: string
value: 4eaa3a62-7a17-11ed-9f3c-0001299981c4
description: Sends robot to charging dock
label: Dock
Once applied using the InOrbit CLI or REST APIs, the action can be executed through the InOrbit UI or through the REST APIs.
# Apply the action definition
inorbit apply -f ActionDefinition.yaml
# Execute the action
curl --location 'https://api.inorbit.ai/robots/<robot-id>/actions' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'x-auth-inorbit-app-key: <app-key>' \
--data '
{
"actionId": "dock"
}'
The connector implements the command handler for the schedule_mission command and passes the arguments to an API client to schedule the mission.
from enum import StrEnum
from inorbit_connector.commands import CommandModel, ExcludeUnsetMixin, parse_custom_command_args
from inorbit_connector.connector import CommandResultCode
from inorbit_edge.commands import COMMAND_CUSTOM_COMMAND
class CommandScheduleMission(ExcludeUnsetMixin, CommandModel):
"""Command model for scheduling a mission."""
mission_id: str
robot_id: int | None = None
priority: int | None = None
class CustomScripts(StrEnum):
"""Custom scripts supported by the connector."""
SCHEDULE_MISSION = "schedule_mission"
# Add other custom scripts here
class ExampleConnector(Connector):
... # other methods
@override
async def _inorbit_command_handler(
self, command_name: str, args: list, options: dict
) -> None:
if command_name == COMMAND_CUSTOM_COMMAND:
script_name, script_args = parse_custom_command_args(args)
# script_name = "schedule_mission"
# script_args = {"mission_id": "4eaa3a62-7a17-11ed-9f3c-0001299981c4"}
if script_name == CustomScripts.SCHEDULE_MISSION:
# Validation and type conversion happen automatically
command = CommandScheduleMission(**script_args)
# Only explicitly set fields are included in the dump
await self._api_client.schedule_mission(**command.model_dump())
else:
raise CommandFailure(
execution_status_details=f"Command not implemented",
stderr=f"Command '{script_name}' not yet implemented",
)
# Call the result function to indicate success
options["result_function"](CommandResultCode.SUCCESS)
else:
raise CommandFailure(
execution_status_details=f"Command not implemented",
stderr=f"Command '{command_name}' not yet implemented",
)