Skip to content
44 changes: 20 additions & 24 deletions docs/contents/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ description: "Configuration models and file formats for connectors"

The `inorbit-connector` framework uses Pydantic models for configuration, providing validation and type safety.

## ConnectorConfig
## ConnectorRootConfig

The main configuration class is `ConnectorConfig`, which contains all settings for your connector. It includes a `fleet` field containing a list of `RobotConfig` entries.
The main configuration class is `ConnectorRootConfig`, which contains all settings for your connector. It is a `BaseSettings` subclass (from pydantic-settings) that resolves `INORBIT_*` environment variables and reads `config/.env` at instantiation time. It includes a `fleet` field containing a list of `RobotConfig` entries.

Connectors should subclass `inorbit_connector.models.ConnectorConfig` and define a `connector_config` field that contains the configuration for the connector. For more details see the [Creating a Custom Configuration](#creating-a-custom-configuration) section below.
Connectors parametrize `ConnectorRootConfig[T]` with a concrete `ConnectorSpecificConfig` subclass to get typed access to `connector_config`. For more details see the [Creating a Custom Configuration](#creating-a-custom-configuration) section below.

### Key Fields

- **`api_key`** (str | None): The InOrbit API key. Can be set via environment variable `INORBIT_API_KEY`
- **`api_key`** (str | None): The InOrbit API key. Can be set via environment variable `INORBIT_API_KEY`. Required unless `inorbit_robot_key` is provided
- **`api_url`** (HttpUrl): The URL of the InOrbit API endpoint. Defaults to InOrbit Cloud SDK URL. Can be set via environment variable `INORBIT_API_URL`
- **`connector_type`** (str): A string identifier for your connector type (e.g., "example_bot")
- **`connector_config`** (BaseModel): Your custom configuration model that inherits from Pydantic's `BaseModel`. This is where you define connector-specific fields
- **`connector_config`** (ConnectorSpecificConfig): Your custom configuration model that inherits from `ConnectorSpecificConfig`. Set the `CONNECTOR_TYPE` class variable to get automatic env-var loading with prefix `INORBIT_{CONNECTOR_TYPE}_`
- **`update_freq`** (float): Update frequency in Hz for the execution loop. Default is 1.0
- **`location_tz`** (str): The timezone of the robot location (e.g., "America/Los_Angeles", "UTC"). Must be a valid pytz timezone
- **`logging`** (LoggingConfig): Logging configuration (see below)
Expand All @@ -25,16 +25,18 @@ Connectors should subclass `inorbit_connector.models.ConnectorConfig` and define
- **`fleet`** (list[RobotConfig]): List of robot configurations (see below)
- **`user_scripts_dir`** (DirectoryPath | None): Path to directory containing user scripts for command execution
- **`account_id`** (str | None): InOrbit account ID, required for publishing footprints
- **`inorbit_robot_key`** (str | None): Robot key for InOrbit Connect robots. See [API documentation](https://api.inorbit.ai/docs/index.html#operation/generateRobotKey)
- **`inorbit_robot_key`** (str | None): Robot key for InOrbit Connect robots. Required unless `api_key` is provided. See [API documentation](https://api.inorbit.ai/docs/index.html#operation/generateRobotKey)
- **`metrics`** (MetricsConfig): Optional Prometheus metrics endpoint. Disabled by default. See [Metrics](usage/metrics) for the full guide and [`MetricsConfig`](#metricsconfig) for the field list.

### Environment Variables

The following environment variables are automatically read during configuration:
`ConnectorRootConfig` is a pydantic-settings `BaseSettings` subclass. Environment variables with the `INORBIT_` prefix are resolved at instantiation time (not import time) and `config/.env` is read automatically. Init kwargs (e.g. from YAML) take precedence over env vars.

- **`INORBIT_API_KEY`** (required): The InOrbit API key
- **`INORBIT_API_KEY`**: The InOrbit API key. Required unless `inorbit_robot_key` is provided
- **`INORBIT_API_URL`** (optional): The InOrbit API endpoint URL

When `connector_config` is passed as a dict (e.g. from YAML), the `_env_file` override is forwarded to the nested `ConnectorSpecificConfig` constructor. Passing `_env_file=None` to `ConnectorRootConfig` disables dotenv reading for both root and connector-specific fields.

## RobotConfig

Represents configuration for a single robot in the fleet:
Expand Down Expand Up @@ -78,29 +80,24 @@ Optional Prometheus metrics endpoint. When `enabled` is `false` (the default) no
(creating-a-custom-configuration)=
## Creating a Custom Configuration

To create a connector-specific configuration, subclass `ConnectorConfig`:
Subclass `ConnectorSpecificConfig` for your vendor-specific fields, then parametrize `ConnectorRootConfig` with it directly:

```python
from pydantic import BaseModel
from inorbit_connector.models import ConnectorConfig, RobotConfig
from inorbit_connector.models import ConnectorRootConfig, ConnectorSpecificConfig

class MyConnectorConfig(ConnectorSpecificConfig):
"""Custom fields for your connector."""
CONNECTOR_TYPE = "my_connector"

class MyRobotConfig(BaseModel):
"""Custom fields for your robot."""
api_version: str
hardware_revision: str
custom_setting: str

class MyConnectorConfig(ConnectorConfig):
"""Configuration for your connector."""
connector_config: MyRobotConfig

@field_validator("connector_type")
def check_connector_type(cls, connector_type: str) -> str:
if connector_type != "my_connector":
raise ValueError(f"Expected connector type 'my_connector'")
return connector_type
config = ConnectorRootConfig[MyConnectorConfig](**yaml_data)
```

`ConnectorSpecificConfig` automatically loads environment variables with the prefix `INORBIT_{CONNECTOR_TYPE}_` and reads `config/.env`. For example, with `CONNECTOR_TYPE = "my_connector"`, setting `INORBIT_MY_CONNECTOR_API_VERSION=v2` will populate the `api_version` field.

## Configuration Files

Configuration is typically loaded from YAML files. See:
Expand All @@ -113,6 +110,5 @@ Use `inorbit_connector.utils.read_yaml()` to load configuration from YAML files:
from inorbit_connector.utils import read_yaml

yaml_data = read_yaml("config.yaml")
config = MyConnectorConfig(**yaml_data)
config = ConnectorRootConfig[MyConnectorConfig](**yaml_data)
```

3 changes: 2 additions & 1 deletion docs/contents/specification/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ The table below lists package-defined symbols meant for direct use (call) or ext
| Call | `Connector.publish_pose()` / `publish_map()` | Publish pose/map for the current robot (map handling included). | [Details](connector.md#spec-connector-connector-publishing) |
| Call | `Connector.publish_odometry()` / `publish_key_values()` / `publish_system_stats()` | Publish telemetry for the current robot. `publish_system_stats()` defers publishing; defaults published if not called. | [Details](connector.md#spec-connector-connector-publishing) |
| Call (advanced) | `Connector._get_session()` | Access the underlying Edge SDK `RobotSession` for the current robot. | [Details](connector.md#spec-connector-connector-get-session) |
| Type | `ConnectorConfig` | Base configuration model for connectors. | [Details](models.md#spec-models-connectorconfig) |
| Type | `ConnectorRootConfig` | Top-level configuration model for connectors (`BaseSettings`). | [Details](models.md#spec-models-connectorrootconfig) |
| Type | `ConnectorSpecificConfig` | Base for per-connector vendor config; derives env prefix from `CONNECTOR_TYPE`. | [Details](models.md#spec-models-connectorspecificconfig) |
| Type | `RobotConfig` | Per-robot configuration (robot_id + cameras). | [Details](models.md#spec-models-robotconfig) |
| Type | `MapConfig` / `MapConfigTemp` | Map configuration (file-backed vs in-memory bytes) used by map publishing/fetching. | [Details](models.md#spec-models-mapconfig) |
| Type | `LoggingConfig` / `LogLevels` | Logging configuration and log-level enum. | [Details](logging.md#spec-logging-loggingconfig) |
Expand Down
36 changes: 29 additions & 7 deletions docs/contents/specification/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,42 @@ description: "Configuration models specification"

This page specifies the configuration models defined by `inorbit_connector.models`.

(spec-models-connectorconfig)=
## `ConnectorConfig`
(spec-models-connectorrootconfig)=
## `ConnectorRootConfig`

Base configuration model for connectors.
Top-level configuration model for connectors. Subclasses `BaseSettings` from pydantic-settings and is generic over `T: ConnectorSpecificConfig`.

Key points:

- You typically **subclass** this to define your connector-specific `connector_config` model.
- The base model reads `INORBIT_API_KEY` and `INORBIT_API_URL` from environment variables by default.
- **Parametrize** with a concrete `ConnectorSpecificConfig` subclass to get typed `connector_config` access: `ConnectorRootConfig[MyConfig](**yaml_data)`.
- Resolves `INORBIT_*` environment variables and reads `config/.env` at instantiation time via pydantic-settings. Init kwargs (typically values from a YAML file) take precedence over env vars.
- `fleet` must contain at least one `RobotConfig`, and robot IDs must be unique.
- When `connector_config` arrives as a dict, the model validator constructs it via `__init__` (not `model_validate`) to preserve env-var resolution. The `_env_file` init kwarg is forwarded to the nested constructor for consistent dotenv behavior.

### `to_singular_config(robot_id) -> ConnectorConfig`
### `to_singular_config(robot_id) -> Self`

Returns a config instance of the same subclass type, with `fleet` filtered down to exactly the requested robot.
Returns a config instance of the same type, with `fleet` filtered down to exactly the requested robot.

(spec-models-connectorspecificconfig)=
## `ConnectorSpecificConfig`

Base class for per-connector vendor configuration. Subclasses `BaseSettings` from pydantic-settings.

Subclass this and set the `CONNECTOR_TYPE` class variable. The framework automatically configures env-var loading with the prefix `INORBIT_{CONNECTOR_TYPE}_` and reads `config/.env`.

```python
from inorbit_connector.models import ConnectorSpecificConfig

class AcmeConfig(ConnectorSpecificConfig):
CONNECTOR_TYPE = "acme"

fleet_host: str
fleet_api_key: str = "default"
```

With this definition, `INORBIT_ACME_FLEET_HOST` and `INORBIT_ACME_FLEET_API_KEY` are resolved from the environment. Init kwargs take precedence over env vars.

Connectors with custom env-loading needs (e.g. per-robot prefixes) can subclass `BaseSettings` directly instead.
Comment thread
b-Tomas marked this conversation as resolved.

(spec-models-robotconfig)=
## `RobotConfig`
Expand Down
4 changes: 2 additions & 2 deletions docs/contents/usage/fleet.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ Subclass `inorbit_connector.connector.FleetConnector` to manage multiple robots
## Constructor

```python
def __init__(self, config: ConnectorConfig, **kwargs) -> None
def __init__(self, config: ConnectorRootConfig, **kwargs) -> None
```

**Parameters:**
- `config` (ConnectorConfig): The connector configuration containing the fleet
- `config` (ConnectorRootConfig): The connector configuration containing the fleet

**Keyword Arguments:**
- `register_user_scripts` (bool): Automatically register user scripts. Default: `False`
Expand Down
2 changes: 1 addition & 1 deletion docs/contents/usage/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The framework ships an OpenTelemetry-based metrics subsystem that connectors can
When `metrics.enabled = true` in your connector configuration, the framework starts a Prometheus HTTP server and exposes:

Every framework metric is namespaced by `connector_type` at the source.
With `connector_type="acme"` (set on `ConnectorConfig`), the four
With `connector_type="acme"` (set on `ConnectorRootConfig`), the four
framework signals come out as:

| Metric | Type | Attributes | Meaning |
Expand Down
4 changes: 2 additions & 2 deletions docs/contents/usage/single-robot.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ Subclass `inorbit_connector.connector.Connector` to create a connector for a sin
## Constructor

```python
def __init__(self, robot_id: str, config: ConnectorConfig, **kwargs) -> None
def __init__(self, robot_id: str, config: ConnectorRootConfig, **kwargs) -> None
```

**Parameters:**
- `robot_id` (str): The InOrbit robot ID
- `config` (ConnectorConfig): The connector configuration
- `config` (ConnectorRootConfig): The connector configuration

**Keyword Arguments:**
- `register_user_scripts` (bool): Automatically register user scripts. Default: `False`
Expand Down
8 changes: 5 additions & 3 deletions examples/fleet-connector/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from inorbit_connector.models import MapConfigTemp

# Local
from datatypes import ExampleBotConnectorConfig
from inorbit_connector.models import ConnectorRootConfig
from datatypes import ExampleBotConfig
from fleet_client import FleetManager, FleetManagerAPIWrapper

# Path to the example map image
Expand All @@ -38,10 +39,11 @@ class ExampleBotFleetConnector(FleetConnector):

Args:
robot_ids (list[str]): List of robot IDs in the fleet
config (ExampleBotConnectorConfig): The configuration for the connector
config (ConnectorRootConfig[ExampleBotConfig]):
The configuration for the connector
"""

def __init__(self, config: ExampleBotConnectorConfig) -> None:
def __init__(self, config: ConnectorRootConfig[ExampleBotConfig]) -> None:
super().__init__(config)

# Setup any other initialization things here
Expand Down
47 changes: 5 additions & 42 deletions examples/fleet-connector/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,22 @@
#
# SPDX-License-Identifier: MIT

from pydantic import BaseModel, field_validator
from inorbit_connector.models import ConnectorSpecificConfig

from inorbit_connector.models import ConnectorConfig


CONNECTOR_TYPE = "example_bot"


class ExampleBotConfig(BaseModel):
class ExampleBotConfig(ConnectorSpecificConfig):
"""The configuration for the example bot.

This is where you would define and validate additional custom fields for the fleet.

Attributes:
example_bot_api_version (str): An example field for the API version of the fleet manager
example_bot_api_version (str): API version of the fleet manager
example_bot_hw_rev (str): An example field for the HW revision of the fleet
example_bot_custom_value (str): An example field for a custom value of the fleet
"""

CONNECTOR_TYPE = "example_bot"

example_bot_api_version: str
example_bot_hw_rev: str
example_bot_custom_value: str


class ExampleBotConnectorConfig(ConnectorConfig):
"""The configuration for the example bot fleet connector.

Each connector should create a class that inherits from ConnectorConfig.

Attributes:
connector_config (ExampleBotConfig): The config with custom fields for the fleet
"""

connector_config: ExampleBotConfig

# noinspection PyMethodParameters
@field_validator("connector_type")
def check_connector_type(cls, connector_type: str) -> str:
"""Validate the connector type.

This should always be equal to the pre-defined constant.

Args:
connector_type (str): The defined connector type passed in

Returns:
str: The validated connector type

Raises:
ValueError: If the connector type is not equal to the pre-defined constant
"""
if connector_type != CONNECTOR_TYPE:
raise ValueError(
f"Expected connector type '{CONNECTOR_TYPE}' not '{connector_type}'"
)
return connector_type
7 changes: 4 additions & 3 deletions examples/fleet-connector/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

from inorbit_connector.utils import read_yaml
from connector import ExampleBotFleetConnector
from datatypes import ExampleBotConnectorConfig
from inorbit_connector.models import ConnectorRootConfig
from datatypes import ExampleBotConfig

"""
This is the main entry point for the fleet connector.
Expand All @@ -29,7 +30,7 @@ def error(self, message):


def start():
"""Parses arguments, processes the configuration file and starts the fleet connector."""
"""Parse arguments, process config file and start the fleet connector."""
parser = CustomParser(prog="fleet_connector")
parser.add_argument(
"-c",
Expand All @@ -48,7 +49,7 @@ def start():
yaml_data = read_yaml(config_filename)

# Create the connector configuration
config = ExampleBotConnectorConfig(**yaml_data)
config = ConnectorRootConfig[ExampleBotConfig](**yaml_data)

# Extract robot IDs from the fleet configuration for logging purposes
robot_ids = [robot.robot_id for robot in config.fleet]
Expand Down
12 changes: 9 additions & 3 deletions examples/robot-connector/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from inorbit_connector.models import MapConfigTemp

# Local
from datatypes import ExampleBotConnectorConfig
from inorbit_connector.models import ConnectorRootConfig
from datatypes import ExampleBotConfig
from robot import Robot, ExampleBotAPIWrapper

# Path to the example map image
Expand All @@ -36,10 +37,15 @@ class ExampleBotConnector(Connector):

Args:
robot_id (str): The ID of the InOrbit robot
config (ExampleBotConnectorConfig): The configuration for the connector
config (ConnectorRootConfig[ExampleBotConfig]):
The configuration for the connector
"""

def __init__(self, robot_id: str, config: ExampleBotConnectorConfig) -> None:
def __init__(
self,
robot_id: str,
config: ConnectorRootConfig[ExampleBotConfig],
) -> None:
super().__init__(robot_id, config)

# Setup any other initialization things here
Expand Down
45 changes: 4 additions & 41 deletions examples/robot-connector/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@
#
# SPDX-License-Identifier: MIT

from pydantic import BaseModel, field_validator
from inorbit_connector.models import ConnectorSpecificConfig

from inorbit_connector.models import ConnectorConfig


CONNECTOR_TYPE = "example_bot"


class ExampleBotConfig(BaseModel):
class ExampleBotConfig(ConnectorSpecificConfig):
"""The configuration for the example bot.

This is where you would define and validate additional custom fields for the robot.
Expand All @@ -21,40 +16,8 @@ class ExampleBotConfig(BaseModel):
example_bot_custom_value (str): An example field for a custom value of the robot
"""

CONNECTOR_TYPE = "example_bot"

example_bot_api_version: str
example_bot_hw_rev: str
example_bot_custom_value: str


class ExampleBotConnectorConfig(ConnectorConfig):
"""The configuration for the example bot connector.

Each connector should create a class that inherits from ConnectorConfig.

Attributes:
connector_config (ExampleBotConfig): The config with custom fields for the robot
"""

connector_config: ExampleBotConfig

# noinspection PyMethodParameters
@field_validator("connector_type")
def check_connector_type(cls, connector_type: str) -> str:
"""Validate the connector type.

This should always be equal to the pre-defined constant.

Args:
connector_type (str): The defined connector type passed in

Returns:
str: The validated connector type

Raises:
ValueError: If the connector type is not equal to the pre-defined constant
"""
if connector_type != CONNECTOR_TYPE:
raise ValueError(
f"Expected connector type '{CONNECTOR_TYPE}' not '{connector_type}'"
)
return connector_type
Loading
Loading