4 Спецификация API¶
4.1 Поверхность платформы и слои интеграции¶
4.1.1 Три уровня API: Unity HTTP, backend HTTP/SignalR, plugin SDK¶
Внешняя поверхность платформы uav-simulator распадается на три различных по уровню слоя интеграции, каждый из которых обслуживает собственный сценарий взаимодействия и адресован собственной целевой аудитории. Самый низкий слой — Unity HTTP JSON API, реализованный сервером HttpJsonSimulatorApiServer (см. src/UnityProject/uav-simulator/Assets/Scripts/Api/HttpJsonSimulatorApiServer.cs) — фиксирует контракт между runtime-ом симулятора и любым его клиентом, будь то операторский backend, тренировочный скрипт на Python или инженерная утилита rusim. Этот слой работает с понятиями физической симуляции напрямую — reset, step, state, frame — и не несёт операторской семантики (сессии, журналы, реестр моделей). Второй слой — backend на ASP.NET Core (см. src/ks0223-web-mac/backend/Program.cs) — поднимается над runtime и вводит понятия пользовательской сессии, активного клиента, активной модели и записанного демо. Backend выступает единственным транспортом для веб-интерфейса и одновременно публикует семантически совместимый интерфейс над двумя различными подложками — симулятором и физическим роботом. Третий слой — plugin SDK — представляет собой .NET-библиотеку (packages/com.uav-simulator.plugin-sdk/Runtime/), которая компилируется в плагин и определяет внутреннюю поверхность расширяемости платформы.
Различие между тремя слоями отражает различные временные горизонты обращения: к Unity HTTP API обращается training loop с частотой до 30 Hz, к backend — оператор в темпе человеческого взаимодействия, к plugin SDK — разработчик плагина однократно при сборке. Это различие диктует и стиль контрактов: на runtime-уровне применяется минимальный набор маршрутов с компактными DTO без вложенных метаданных; на backend-уровне маршрутов на порядок больше и они обогащены диагностикой, журналом и информацией о привязках; на SDK-уровне поверхность задана не маршрутами, а наследованием от абстрактных базовых классов и набором атрибутов на полях ScriptableObject-дескрипторов.
flowchart TB
subgraph Clients["Клиенты"]
WebUI["Web UI"]
CLI["rusim CLI"]
Train["Python training"]
end
subgraph Backend["Backend layer (ASP.NET Core)"]
BackHTTP["HTTP /api/*"]
SignalR["SignalR /hub/telemetry"]
end
subgraph Runtime["Runtime layer (Unity)"]
UnityAPI["HTTP JSON /reset /step /state /contract /health"]
end
subgraph SDK["Plugin SDK layer"]
PluginBase["VehicleBase, TrackBase, PluginDescriptorBase"]
Contracts["UavSimulator.Contracts"]
end
WebUI --> BackHTTP
WebUI --> SignalR
CLI --> BackHTTP
CLI --> UnityAPI
Train --> UnityAPI
BackHTTP --> UnityAPI
PluginBase -.compiled into.-> Runtime
Contracts -.shared types.-> Runtime
Contracts -.shared types.-> Backend
Рисунок 4.1 — Три слоя API платформы и направления обращений между ними.
4.1.2 Контракты как точка единственной истины¶
Все три слоя делят между собой набор сериализуемых типов данных, объявленных в namespace UavSimulator.Contracts (см. src/UnityProject/uav-simulator/Assets/Scripts/Contracts/SimulatorContracts.cs) и продублированных в plugin SDK как packages/com.uav-simulator.plugin-sdk/Runtime/SimulatorContracts.cs. Именно эти типы — ControlCommand, VehicleState, CameraFrame, SimulationConfig, StepResult, DeviceContractDescriptor — образуют единственный источник истины для платформы. Любое изменение поля в этих типах автоматически попадает в публичную поверхность runtime, в backend через клиентский JsonSerializer, в плагин через прямое использование класса и в Python через JSON-десериализацию ответа сервера.
Такой подход избран сознательно как альтернатива двум распространённым практикам — генерации DTO из OpenAPI-описания и ручному поддержанию параллельных моделей на каждом языке. Генерация из OpenAPI вносит в проект промежуточный артефакт, который требует отдельного конвейера и постоянно отстаёт от изменений; ручное поддержание двойников приводит к дрейфу контрактов и тонким несовместимостям, которые проявляются только во время выполнения. Унификация на одной C#-модели возможна постольку, поскольку и runtime, и backend, и SDK — это .NET-проекты, а Python-клиент работает с JSON в виде словарей и не нуждается в типизированных моделях вне исследовательского цикла.
4.1.3 Версионирование API¶
Версионирование API организовано на двух различных уровнях. Уровень runtime-контракта обозначается строковым полем contractVersion в дескрипторе SimulatorContractDescriptor (SimulatorContracts.cs:192) и читается клиентами при инициализации соединения через GET /contract. Это глобальная версия поверхности симулятора, изменяемая при структурном обновлении DTO или поведения базовых маршрутов. Уровень контрактов плагинов обозначается отдельной структурой ContractVersion (ContractVersion.cs) с тремя целочисленными полями major, minor, patch и реализацией IComparable<ContractVersion> для сравнения. Тип используется в PluginDescriptorBase.version и описывает версию конкретного робота или трассы, а не платформы в целом.
Семантика семантического версионирования соблюдается явно. Несовместимое изменение порождает увеличение major и в случае плагина — новый идентификатор с суффиксом .vN+1 (например, vehicle.ks0223.v2); это позволяет двум версиям одного и того же плагина сосуществовать в реестре одновременно. Совместимое расширение увеличивает minor; исправление поведения без изменения поверхности — patch. На уровне runtime API соответствующая стратегия описана в разделе 4.6 настоящей главы.
4.2 Unity HTTP JSON API¶
4.2.1 Конфигурация: хост, порт, переменные среды¶
Unity HTTP API поднимается классом HttpJsonApiHost (HttpJsonApiHost.cs) в каждой загружаемой сцене. Поведение хоста параметризуется тремя путями: значениями полей в инспекторе Unity Editor (port = 8000, host = "127.0.0.1", autoStart = true), переменными окружения UAVSIM_API_HOST и UAVSIM_API_PORT, читаемыми в Awake через ApplyEnvironmentOverrides (HttpJsonApiHost.cs:50-63), и значениями, переданными в конструктор HttpJsonSimulatorApiServer напрямую. Приоритет переменных среды над полями инспектора выбран сознательно: тренировочные запуски часто параллелизуются на одной машине и каждому из них требуется свой свободный порт; в этих случаях оркестратор передаёт UAVSIM_API_PORT=8001, UAVSIM_API_PORT=8002 без перенастройки сцены Unity.
Когда в качестве хоста указан 127.0.0.1 или localhost, метод GetPrefixes (HttpJsonSimulatorApiServer.cs:177-191) регистрирует одновременно три префикса в HttpListener: http://127.0.0.1:port/, http://localhost:port/ и http://*:port/. Третий префикс гарантирует, что любой клиент с локальной машины достучится до сервера независимо от того, какой alias он использует. На macOS и Windows такой бинд не требует прав администратора, поскольку речь идёт о non-privileged-портах в диапазоне 1024-65535. Если в качестве хоста указан конкретный публичный адрес, регистрируется только он — в этом случае предполагается осознанный выбор оператором сетевого интерфейса.
4.2.2 Маршрут /reset — инициализация сцены¶
Маршрут POST /reset принимает тело с JSON-структурой SimulationConfig и возвращает первое наблюдение в виде StepResult. Семантически это атомарная операция: к моменту возврата ответа сцена пересобрана, плагины проверены через SimulationConfigValidator, агенты заспавнены, и первое состояние с кадром камеры доступно клиенту. Атомарность критична для тренировочного цикла: устранение race condition между завершением reset и первым step позволяет training-обёртке писать прямой код без явных синхронизаций.
Пример запроса:
{
"seed": 42,
"timeScale": 1.0,
"selectedTrackId": "track.cardboard_corridor.v1",
"selectedVehicleId": "vehicle.ks0223.v1",
"trackParams": [
{ "key": "obstacle_density", "value": "0.3" }
],
"vehicleParams": [],
"flags": [
{ "key": "agents.isolated", "value": "false" }
],
"agents": [
{ "agentId": "ego", "vehicleId": "vehicle.ks0223.v1", "isPrimary": true }
]
}
Пример ответа (укороченный, с пропуском бинарных полей кадра):
{
"activeAgentId": "ego",
"activeVehicleId": "vehicle.ks0223.v1",
"state": {
"pose": { "position": { "x": 0.0, "y": 0.05, "z": 0.0 }, "rotation": { "x": 0, "y": 0, "z": 0, "w": 1 } },
"linearVelocity": { "x": 0, "y": 0, "z": 0 },
"angularVelocity": { "x": 0, "y": 0, "z": 0 },
"speed": 0.0,
"timestamp": 1714780000123,
"timeBase": "unix_ms",
"telemetry": []
},
"reward": 0.0,
"done": false,
"info": [],
"frame": { "frameId": "...", "width": 96, "height": 96, "format": "RGB24", "encoding": "base64", "dataBase64": "..." },
"agents": []
}
Возможные коды состояния — 200 OK при успешной инициализации, 400 Bad Request при невалидной конфигурации (неизвестный selectedTrackId, отсутствующий плагин, повторяющиеся agentId), 500 Internal Server Error при сбое инстанцирования префаба. Все ошибки оборачиваются в JSON-объект {"error": "<message>"} обработчиком исключений в HandleContextAsync (HttpJsonSimulatorApiServer.cs:97-118).
4.2.3 Маршрут /step — управление и наблюдение¶
Маршрут POST /step — основная рабочая точка training-цикла. Принимает тело со структурой ControlCommand, применяет её к указанному агенту и возвращает StepResult с новым состоянием, опциональным кадром камеры, скалярной наградой и флагом done. Семантика шага синхронна и атомарна: запрос блокируется до завершения одного шага физического движка Unity, поэтому клиент получает наблюдение, точно соответствующее применённой команде, без необходимости отдельных вызовов для чтения состояния.
Пример минимального запроса:
В multi-agent-сценариях команда адресуется конкретному агенту через targetAgentId или targetVehicleId:
Поле extensions несёт device-specific каналы управления, описанные в DeviceContractDescriptor плагина — раздельные PWM на колёсах робота, сервоприводы поворота камеры и подобные. Пример с прямым PWM-управлением:
{
"throttle": 0.0, "steer": 0.0, "brake": 0.0,
"extensions": [
{ "key": "drive.left_pwm_norm", "value": "0.7" },
{ "key": "drive.right_pwm_norm", "value": "0.5" }
]
}
Возвращаемый StepResult включает четыре основных раздела: state агента (поза, скорости, скаляры в telemetry), кадр камеры (опционально, при наличии attached-камеры на vehicle-плагине), скалярная награда reward и флаг done. Поле agents массив — содержит снимок состояния всех агентов в multi-agent-сцене и используется обёртками среды MultiAgentVisionVecEnv для одновременного чтения наблюдений всех агентов в одной операции вместо n параллельных HTTP-запросов.
4.2.4 Маршрут /state и /health — состояние и диагностика¶
Текущая реализация Unity HTTP API не выделяет /state как отдельный маршрут — снимок состояния доставляется в составе ответа /step или может быть прочитан через SimulationManager.ReadSnapshot без отправки команды управления. Сам ReadSnapshot доступен внутри runtime, но в HTTP-поверхность не вынесен; доступным аналогом служит «холостой» step с нулевыми командами. Это консервативное проектное решение: добавление лишнего маршрута увеличивает площадь контракта без явной выгоды, поскольку любой step и так возвращает текущее состояние.
Маршрут GET /health (HttpJsonSimulatorApiServer.cs:122-126) возвращает компактную диагностическую структуру SimulatorHealthStatus. Типичный ответ:
{
"status": "ok",
"pluginRegistrySource": "RegistryAsset",
"availableVehicles": 6,
"availableTracks": 3,
"activeAgentId": "ego",
"activeVehicleId": "vehicle.prometeo.sport.v1",
"activeTrackId": "track.roadsystem_realistic.v2",
"activeVehicleCount": 1
}
/health используется в трёх различных сценариях. Backend читает его при попытке connect-а к Unity для проверки готовности runtime до отправки реальных команд. CLI rusim discover использует его для сканирования диапазона портов и определения работающих экземпляров Unity. Тренировочные скрипты пингуют /health в начале запуска, чтобы отказаться от попыток отправки reset к не запущенному Unity и выдать пользователю диагностическое сообщение немедленно.
Маршрут GET /contract (HttpJsonSimulatorApiServer.cs:128-132) возвращает структуру SimulatorContractDescriptor со списком всех доступных vehicle- и track-плагинов вместе с их DeviceContractDescriptor. Этот маршрут — точка discovery каталога: CLI команды rusim list vehicles, rusim list tracks и rusim inspect <id> читают его и выводят пользователю человеко-читаемый список без необходимости знать о реестре плагинов на стороне Unity.
4.2.5 Маршруты model lifecycle¶
Жизненный цикл моделей в текущей версии платформы реализован полностью на стороне backend — Unity-runtime не хранит и не активирует ONNX-артефакты самостоятельно. Это сознательное решение: runtime отвечает за физику и наблюдения, а autopilot и inference loop живут в operator stack, поскольку требуют истории сессии, версионирования и интеграции с операторской поверхностью. Такое разделение исключает дублирование model registry между runtime и backend и сохраняет за Unity HTTP API минимальный набор маршрутов (/health, /contract, /reset, /step).
Полный набор маршрутов Unity HTTP API на текущий момент исчерпывающе перечислен в таблице 4.1.
Таблица 4.1 — Маршруты Unity HTTP JSON API
| Метод | Путь | Назначение | Тело запроса | Тело ответа |
|---|---|---|---|---|
| GET | /health |
Диагностическая сводка | — | SimulatorHealthStatus |
| GET | /contract |
Каталог плагинов и контрактов | — | SimulatorContractDescriptor |
| POST | /reset |
Инициализация сцены | SimulationConfig |
StepResult |
| POST | /step |
Шаг управления | ControlCommand |
StepResult |
Именно такая компактная поверхность определяет философию runtime-уровня: четыре маршрута, четыре сериализуемых типа, ноль внешних зависимостей помимо System.Net.HttpListener. Любое расширение требует осознанного решения и попадает в раздел breaking changes (см. 4.6.2).
4.2.6 Сериализация: Unity JsonUtility и его особенности¶
Сериализация на стороне Unity выполняется встроенным сериализатором UnityEngine.JsonUtility (вызовы ToJson/FromJson в SimulatorApiFacade). Этот выбор продиктован двумя обстоятельствами. Во-первых, классы DTO в UavSimulator.Contracts помечены атрибутом [Serializable] и одновременно используются как поля в MonoBehaviour-ах сцены: использование того же сериализатора, что и для Editor-инспектора, гарантирует совпадение поведения между сценой и сетевым контрактом без дополнительной разметки. Во-вторых, JsonUtility не вносит в проект сторонней зависимости и сокращает размер сборки.
Особенности JsonUtility следует учитывать на стороне клиента. Сериализатор не поддерживает Dictionary<TKey, TValue> напрямую — именно поэтому все «карты» в DTO сделаны массивами ConfigKeyValue[]. Сериализатор не различает null и значение по умолчанию: отсутствующее в JSON поле инициализируется default(T), что особенно важно для опциональных полей targetAgentId и targetVehicleId в ControlCommand. Сериализатор полностью игнорирует свойства (get/set) и работает только с public-полями, поэтому DTO в платформе сделаны чистыми data-классами без логики.
Клиенты на Python используют стандартный модуль json без типизированных моделей: HTTP-клиент SimClient (python/sim_client/http_client.py) принимает и возвращает Dict[str, Any], не валидирует структуру и оставляет соответствие именам полей на ответственности вызывающего кода. Это упрощает быстрое прототипирование тренировочных обёрток и не требует поддержания параллельной модели типов в исследовательском контуре.
4.3 Транспортные DTO¶
4.3.1 ControlCommand¶
ControlCommand (SimulatorContracts.cs:67-82) — структура управляющей команды от клиента к runtime. Минимальная команда задаётся тремя нормализованными скалярами throttle, steer, brake; этого достаточно для большинства colon-style роботов класса KS0223 и универсально работает с встроенными vehicle-плагинами.
[Serializable]
public sealed class ControlCommand
{
public float throttle;
public float steer;
public float brake;
public string targetAgentId;
public string targetVehicleId;
public long timestamp;
public string timeBase;
public ConfigKeyValue[] extensions;
}
Таблица 4.2 — Поля ControlCommand
| Поле | Тип | Единица/диапазон | Обязательно | Описание |
|---|---|---|---|---|
throttle |
float | [-1, 1] | да | Нормализованная тяга. Положительное — вперёд, отрицательное — назад |
steer |
float | [-1, 1] | да | Нормализованный угол поворота. -1 — крайнее левое, +1 — крайнее правое |
brake |
float | [0, 1] | да | Нормализованное торможение |
targetAgentId |
string | — | нет | Адресация команды конкретному агенту в multi-agent-сцене |
targetVehicleId |
string | — | нет | Альтернатива targetAgentId через vehicleId плагина |
timestamp |
long | unix_ms | нет | Метка времени отправки. Используется журналом и записью демо |
timeBase |
string | "unix_ms" | нет | База времени метки |
extensions |
ConfigKeyValue[] | — | нет | Device-specific каналы управления (PWM, серво, LED) |
Приоритет адресации — сначала targetAgentId, затем targetVehicleId, в случае пустых обоих полей — primary-агент сцены. Это позволяет одиночному клиенту обращаться к простой однокамерной сцене без указания идентификаторов и тому же клиенту — управлять конкретным агентом в multi-agent-конфигурации без изменения формата команды.
4.3.2 VehicleState¶
VehicleState (SimulatorContracts.cs:50-64) — структура состояния транспортного средства, возвращаемая в каждом StepResult и доступная через VehicleBase.ReadState (VehicleBase.cs:17-45).
[Serializable]
public sealed class VehicleState
{
public Posef pose;
public Vector3f linearVelocity;
public Vector3f angularVelocity;
public float speed;
public long timestamp;
public string timeBase;
public ConfigKeyValue[] telemetry;
}
Поле pose содержит позицию и ориентацию через Posef (вложенные Vector3f и Quaternionf). Поле linearVelocity — линейная скорость в системе координат сцены, angularVelocity — угловая скорость в радианах в секунду. Поле speed — скалярная величина скорости, удобная для использования в качестве компонента наблюдения в reinforcement-learning без вычисления нормы вектора на стороне клиента. Поле telemetry несёт device-specific скаляры: показания линейных датчиков, ультразвука, заряда батареи; набор ключей задаётся плагином и формально описан в DeviceContractDescriptor.sensors.
4.3.3 CameraFrame¶
CameraFrame (SimulatorContracts.cs:30-47) описывает кадр с камеры робота, возвращаемой как часть StepResult.frame или AgentStepResult.frame.
[Serializable]
public sealed class CameraFrame
{
public string frameId;
public int width;
public int height;
public string format;
public string encoding;
public long timestamp;
public string timeBase;
public int bytesLength;
public string dataRef;
public string dataBase64;
}
Таблица 4.3 — Поля CameraFrame
| Поле | Тип | Описание |
|---|---|---|
frameId |
string | Уникальный идентификатор кадра, монотонно возрастает |
width, height |
int | Размер кадра в пикселях |
format |
string | Формат пикселей: RGB24, RGBA32, JPEG |
encoding |
string | Кодирование payload-а: base64 для inline или ref для внешнего хранилища |
bytesLength |
int | Длина полезных данных в байтах до кодирования |
dataBase64 |
string | Inline-данные в base64 (заполняется при encoding == "base64") |
dataRef |
string | Указатель на внешнее хранилище (заполняется при encoding == "ref") |
Двухвариантное кодирование через dataBase64 или dataRef отражает компромисс между простотой и пропускной способностью. Для тренировочных сценариев на одном хосте inline-base64 проще: HTTP-клиенту достаточно прочитать тело и декодировать поле напрямую. Для production-сценариев и удалённого исполнения в дальнейшем планируется добавление транспорта dataRef, при котором кадр публикуется в shared memory или по отдельному WebRTC-каналу, а HTTP-ответ несёт только идентификатор.
4.3.4 SensorDescriptor + ActuatorDescriptor¶
Дескрипторы каналов сенсоров и актуаторов формализуют контракт устройства в machine-readable форме и применяются для валидации совместимости плагина и сценария обучения.
[Serializable]
public sealed class SensorDescriptor
{
public string id;
public string sensorType;
public string format;
public string unit;
public int[] shape;
public float rateHz;
}
[Serializable]
public sealed class ActuatorDescriptor
{
public string id;
public string actuatorType;
public string unit;
public float min;
public float max;
}
В SensorDescriptor поле sensorType принимает значения camera, ultrasonic, line_array, imu и подобные; format уточняет тип данных (RGB24 для камеры, float32 для ультразвука); shape — форма тензора в случае массивных данных; rateHz — целевая частота обновления. В ActuatorDescriptor поля min и max задают физические границы канала, что используется obstacle-driven рандомизацией параметров на стороне backend и сценарным валидатором.
Дескрипторы агрегируются в DeviceContractDescriptor (SimulatorContracts.cs:165-175), который связывает идентификатор устройства с его типом и наборами сенсоров и актуаторов:
public sealed class DeviceContractDescriptor
{
public string deviceId;
public string deviceType;
public SensorDescriptor[] sensors;
public ActuatorDescriptor[] actuators;
public string observationSchemaJson;
public string actionSchemaJson;
}
Поля observationSchemaJson и actionSchemaJson несут JSON-схему наблюдений и действий в текстовом виде. Это сделано осознанно: schema по своей природе вложенная и динамическая, и попытка её структурного представления через [Serializable]-DTO противоречила бы парадигме JsonUtility. Хранение схемы как текста позволяет автоматически валидировать наблюдения на стороне Python через jsonschema и одновременно сохраняет компактность runtime-DTO.
4.3.5 ConfigKeyValue и параметры сценария¶
Структура ConfigKeyValue (SimulatorContracts.cs:84-89) — самая простая в платформе и самая часто встречающаяся в DTO:
Все «карты ключ-значение» в платформе сериализуются как массивы ConfigKeyValue[] — это вынужденная мера, обусловленная отсутствием поддержки Dictionary в JsonUtility. Структура применяется в шести различных контекстах: trackParams и vehicleParams в SimulationConfig для параметров плагинов, flags для логических переключателей сцены, extensions в ControlCommand для device-specific каналов, telemetry в VehicleState для скалярных датчиков и info в StepResult для произвольной диагностической информации. Конвенция значений — все значения хранятся как строки, парсинг в нужный тип — на стороне потребителя.
Соглашения об именовании ключей зафиксированы в плагинах: agents.isolated, agents.see_each_other, agents.collisions_enabled для multi-agent-режима; drive.left_pwm_norm, drive.right_pwm_norm для дифференциального привода; camera.pan_norm, camera.tilt_norm для серво-камеры. Точечная нотация используется как пространство имён и упрощает документирование контракта плагина.
4.4 Backend HTTP API и SignalR¶
4.4.1 Группировка маршрутов по доменам¶
Backend (Program.cs) в текущей версии публикует около 46 HTTP-маршрутов и один SignalR-хаб. По функциональному назначению маршруты группируются в десять доменов: status/health, connection, models, autopilot, sensors, LED, camera, command, logs, demo, scenarios. Группировка отражает прямые соответствия с разделами Web UI и одновременно служит точкой входа для CLI и автоматизации.
Таблица 4.4 — Сводка backend HTTP-маршрутов по доменам
| Домен | Маршруты | Назначение |
|---|---|---|
| Status/health | GET /api/status, GET /api/health |
Сводка о подключениях и текущих сессиях |
| Connection | POST /api/connection/connect, POST /api/connection/disconnect, GET /api/connection/target |
Управление сессией клиента к runtime |
| Unity discovery | GET /api/unity/runtime-catalog, POST /api/unity/runtime-selection, POST /api/unity/client-selection, GET /api/unity/discover |
Каталог Unity-runtime и переключение track/vehicle |
| Models | POST /api/models/upload, GET /api/models, GET /api/model-catalog, GET /api/models/active, POST /api/models/activate, GET /api/model-bindings/current, POST /api/model-bindings |
Реестр ONNX-моделей и привязка их к клиенту |
| Autopilot | POST /api/autopilot/start, POST /api/autopilot/stop, GET /api/autopilot/preview, GET /api/autopilot/status |
Запуск inference loop на активной модели |
| Sensors | GET /api/sensors/status, GET /api/sensors/latest, POST /api/sensors/config, POST /api/sensors/ultrasonic/position, POST /api/sensors/ultrasonic/auto-scan |
Sensor bridge к Pi и параметры авто-скана |
| LED | POST /api/led/pattern, POST /api/led/custom, POST /api/led/clear |
Управление LED-матрицей робота |
| Camera | GET /api/camera/status, GET /api/camera/snapshot, GET /api/camera/mjpeg |
Доступ к видеопотоку с Pi или Unity |
| Command | POST /api/command |
Низкоуровневая команда оператора |
| Logs/demo | POST /api/logs/start, POST /api/logs/stop, GET /api/logs/files, POST /api/logs/open-folder, POST /api/demo/start, POST /api/demo/stop, POST /api/demo/replay/start, POST /api/demo/replay/stop, GET /api/demo/replay/status, GET /api/demo/replay/sessions |
Журналирование и запись/воспроизведение демо |
| Scenarios | GET /api/scenarios, POST /api/scenarios/load |
Список и применение сценарных YAML-файлов |
| Protocol | GET /api/protocol |
Описание транспорта для активного режима |
| SignalR | MapHub /hub/telemetry |
Push-телеметрия в Web UI |
Полный перечень определён непосредственно в Program.cs и привязан к сервисам RuntimeSessionManager, ModelRegistryService, AutopilotService, SessionLogger, SessionVideoRecorder, DemoReplayService. Каждый маршрут реализован как minimal API endpoint без отдельного controller-класса, что отражает компактный размер backend (один файл Program.cs объёмом порядка 1060 строк) и сознательный отказ от шаблона MVC в пользу плоской декомпозиции.
4.4.2 Маршруты сессий (connect, disconnect, status)¶
Маршрут POST /api/connection/connect (Program.cs:82-93) принимает структуру ConnectRequest (Models/Contracts.cs:11-15) с полями ClientId, RuntimeMode, Host, Port и устанавливает сессию указанного клиента к одной из двух подложек — unity-sim или real-robot. Логика подключения делегирована RuntimeSessionManager.ConnectAsync; ответ — StatusDto со сводкой о состоянии подключения, включая DesiredConnection, TcpConnected, LatencyMs и LastError.
Симметричный маршрут POST /api/connection/disconnect (Program.cs:95-106) разрывает сессию по тем же ключам. Маршрут GET /api/status агрегирует состояние сессии и позволяет Web UI и CLI читать его без отправки запроса в runtime, а GET /api/health дополняет его сведениями о состоянии sensor bridge и cameras.
4.4.3 Маршруты автопилота и моделей¶
Реестр моделей и autopilot тесно связаны в платформе и образуют пару доменов с шестью endpoint-ами на каждом. POST /api/models/upload (Program.cs:108-140) принимает multipart/form-data с полями file (ONNX-артефакт), name, version, source, metadata, metrics и возвращает ModelInfoDto (Models/Contracts.cs:151-161) — структуру с ModelId, CreatedAtUtc, IsActive, путями к артефакту и метаданным и полем Compatibility (CompatibilityHintsDto), описывающим совместимые runtime-режимы и идентификаторы транспортных средств. Загрузка через multipart выбрана осознанно: ONNX-файлы могут достигать сотен мегабайт, и stream-загрузка экономит память по сравнению с base64-в-JSON.
Маршрут GET /api/models возвращает плоский список моделей; GET /api/model-catalog группирует их по полю Name в записи ModelCatalogEntryDto со списком Versions для удобства отображения в Web UI. POST /api/models/activate (Program.cs:162-173) принимает ActivateModelRequest с одним полем ModelId и помечает указанную модель как активную в реестре. GET /api/models/active возвращает ModelInfoDto активной модели или 404 Not Found, если активация не была выполнена.
Маршруты model bindings (/api/model-bindings/current и /api/model-bindings) добавляют к простой активации привязку модели к конкретному (ClientId, RuntimeMode, AgentId)-ключу — это позволяет в multi-client-сценарии иметь одновременно несколько активных привязок различных клиентов к разным моделям.
Запуск автопилота — POST /api/autopilot/start (Program.cs:206-217) принимает StartAutopilotRequest (Models/Contracts.cs:209-215) с полями ClientId, RuntimeMode, AgentId, ModelId, LoopIntervalMs, MaxDurationSeconds. Backend создаёт inference loop, читающий кадры из активного runtime, запускающий ONNX-модель через Microsoft.ML.OnnxRuntime и отправляющий выводимую команду обратно в runtime. Маршрут POST /api/autopilot/stop останавливает loop; GET /api/autopilot/status возвращает AutopilotStatusDto (Models/Contracts.cs:221-242) с сводкой о шагах, командах и времени последней активности; GET /api/autopilot/preview сэмплирует одну итерацию inference без отправки команды и возвращает PreviewSampleDto с logits и probabilities — это используется Web UI в режиме отладки модели.
4.4.4 Маршруты сенсоров и LED¶
Sensor- и LED-маршруты — наиболее KS0223-специфичная часть API, сопрягающаяся с Pi-овским addon-ом, опубликованным как отдельная HTTP-надстройка над рабочим протоколом контроллера. GET /api/sensors/status возвращает SensorBridgeStatusDto со сводкой о доступности bridge-а и числе последовательных ошибок; GET /api/sensors/latest — последнее принятое сообщение в виде SensorTelemetryDto. POST /api/sensors/config (Program.cs:359-386) принимает SensorConfigRequest с опциональными полями AutoScanEnabled, SampleIntervalMs, ScanIntervalSec, ScanSettleMs, DriveSpeedPercent, CameraSpeedPercent и применяет их к bridge-у через addon-овский endpoint.
Маршруты POST /api/sensors/ultrasonic/position и POST /api/sensors/ultrasonic/auto-scan управляют сервоприводом ультразвукового датчика на физическом роботе. На стороне unity-sim те же запросы приводят к смене направления виртуального ультразвука в сцене — это согласовано в обеих подложках через IKs0223RuntimeProvider.
LED-маршруты (/api/led/pattern, /api/led/custom, /api/led/clear) принимают LedPatternRequest или LedCustomFrameRequest со строкой паттерна или hex-кадром и применяют их к LED-матрице робота — на физической стороне напрямую через TCP-команду, на симуляционной — через корневой компонент vehicle-плагина, реализующий соответствующий канал.
4.4.5 Маршруты сценариев и плагинов¶
GET /api/scenarios (Program.cs:648-680) перечисляет все YAML-файлы из директории <repo>/configs/scenarios/ и возвращает их с размером, временем последней модификации и отображаемым именем. Поиск директории выполняется через ResolveScenariosDir с тремя кандидатами: переменная окружения SCENARIOS_DIR, путь /app/configs/scenarios для Docker-сборки и относительный путь от рабочего каталога backend для локальной разработки.
POST /api/scenarios/load (Program.cs:682-759) принимает LoadScenarioRequest с полем FilePath и применяет сценарий через subprocess-вызов CLI rusim scenario reset. Этот выбор архитектурно неочевиден на первый взгляд: backend мог бы парсить YAML и формировать SimulationConfig сам. Делегирование в rusim решает две задачи. Во-первых, парсер сценариев в python/sim_client/scenario.py уже реализует валидацию, разрешение include-ов и применение defaults; дублировать это в .NET — лишняя работа. Во-вторых, единая реализация гарантирует, что Web UI и CLI применяют сценарии одинаково. Единственное требование — наличие rusim в PATH или в переменной RUSIM_PATH.
В текущей версии backend не содержит отдельных маршрутов управления плагинами: установка плагинов происходит через CLI rusim plugin install, а runtime читает реестр ~/.rusim/plugin-registry.json при старте. Backend узнаёт о составе плагинов через GET /api/unity/runtime-catalog (Program.cs:254-269), который проксирует запрос к Unity GET /contract и возвращает Web UI каталог в виде UnityRuntimeCatalogDto.
4.4.6 SignalR-хаб для push-телеметрии¶
Telemetry-хаб (Hubs/TelemetryHub.cs) — единственный SignalR-хаб платформы, доступный по маршруту /hub/telemetry (Program.cs:816). Он реализует push-доставку телеметрии в браузер: backend, получая обновления статуса подключения, кадров камеры, sensor telemetry или прогресса демо-реплея, рассылает их подписанным клиентам без необходимости polling-а.
Метод BindClient(string clientId) группирует SignalR-соединение с clientId через стандартные SignalR Groups; имя группы вычисляется RuntimeSessionManager.GetClientGroup. Это позволяет одному backend-процессу обслуживать несколько браузеров, открытых разными операторами, и доставлять каждому только релевантные ему обновления. При обрыве соединения метод OnDisconnectedAsync (TelemetryHub.cs:44-53) удаляет привязку и снимает регистрацию клиента в RuntimeSessionManager.
Выбор SignalR над raw WebSocket мотивирован двумя соображениями. SignalR автоматически выбирает транспорт (WebSocket, Server-Sent Events, long polling) в зависимости от возможностей клиента и сети, что снимает с фронтенда задачу диагностики транспорта. SignalR имеет первоклассную интеграцию с ASP.NET Core и DI-контейнером, что позволяет инжектировать RuntimeSessionManager в хаб напрямую.
4.5 Plugin SDK API (краткий обзор)¶
Полное руководство по разработке плагинов составляет содержание раздела 5 настоящей работы. В данном подразделе зафиксирован архитектурный обзор API-поверхности SDK с акцентом на её роль в интеграционной модели платформы; детали реализации, lifecycle Editor-валидации и формат архива .rusim-plugin.zip рассмотрены в указанном разделе.
4.5.1 PluginDescriptorBase и наследники¶
Корнем иерархии описаний плагинов служит абстрактный класс PluginDescriptorBase (packages/com.uav-simulator.plugin-sdk/Runtime/PluginDescriptorBase.cs):
public abstract class PluginDescriptorBase : ScriptableObject
{
public string id;
public string displayName;
public ContractVersion version;
[TextArea] public string description;
}
Конкретные типы — VehiclePluginDescriptor и TrackPluginDescriptor — sealed-наследники, добавляющие специфичные для типа поля (prefab, deviceContract для vehicle; prefab, parametersSchemaJson для track). Сама база плагина определяется как ScriptableObject, что обеспечивает редактируемость через Unity Editor и сериализацию в asset-файл; внешний код взаимодействует с дескрипторами через стандартные Unity-механизмы загрузки и не требует знания о конкретной реализации плагина.
Таблица 4.5 — Поверхность Plugin SDK API
| Класс | Назначение | Точки расширения |
|---|---|---|
PluginDescriptorBase |
Общие поля идентификации и версии | Поля id, displayName, version, description |
VehiclePluginDescriptor |
Описание робота | Поля prefab, deviceContract |
TrackPluginDescriptor |
Описание трассы | Поля prefab, parametersSchemaJson |
VehicleBase |
Логика робота | ApplyControl, ReadState, TryReadCameraFrame, ApplyVehicleConfig, SetPeerVisibility, ResetVehicle |
TrackBase |
Логика трассы | ResetTrack(int seed) |
DeviceContractDescriptorAsset |
Контракт устройства как asset | DeviceContractDescriptor поле |
ContractVersion |
Семантическое версионирование | major, minor, patch, IComparable |
4.5.2 VehicleBase и TrackBase: lifecycle hooks¶
Базовый класс VehicleBase (VehicleBase.cs) задаёт шесть точек расширения, которые runtime вызывает в определённые моменты lifecycle симуляции. ApplyControl(ControlCommand command) — горячий путь, вызывается каждый шаг, должен быть безаллокационным; ReadState() возвращает VehicleState и вызывается также каждый шаг; TryReadCameraFrame(out CameraFrame frame) — опциональный метод для роботов с камерой, по умолчанию возвращает false; ApplyVehicleConfig(ConfigKeyValue[] vehicleParams) вызывается один раз при reset для применения параметров domain randomization; SetPeerVisibility(bool visible) управляет видимостью робота для других агентов в multi-agent-сцене; ResetVehicle(int seed) сбрасывает робот в начальное состояние с заданным seed.
Базовый класс TrackBase (TrackBase.cs) минимален и содержит единственную точку расширения — ResetTrack(int seed). Это отражает асимметрию между транспортными средствами и трассами: робот участвует в каждом шаге симуляции и активно реагирует на команды, тогда как трасса в большинстве сценариев пассивна и нуждается лишь в инициализации с заданным seed.
4.5.3 DeviceContractDescriptor¶
DeviceContractDescriptor (структура из UavSimulator.Contracts, упомянутая в 4.3.4) дублируется в SDK как обёртка DeviceContractDescriptorAsset. Asset-вариант — ScriptableObject, на который ссылается VehiclePluginDescriptor.deviceContract, и который Editor-валидатор PluginValidator (packages/com.uav-simulator.plugin-sdk/Editor/PluginValidator.cs) проверяет на соответствие реальной конфигурации префаба перед экспортом плагина в архив.
Дублирование контрактных типов в SDK — мера осознанная. SDK-проект не имеет ссылки на runtime-проект Unity напрямую; идентичные [Serializable]-классы в двух местах гарантируют совпадение бинарного представления при сериализации и одновременно позволяют разработчику плагина не таскать за собой полную иерархию runtime. Согласованность поддерживается тем, что обе копии классов формально объявлены идентично и любое расхождение немедленно проявляется при экспорте плагина — через ошибку валидации.
4.6 Стабильность и совместимость API¶
4.6.1 Семантическое версионирование контрактов¶
Версия контракта runtime отслеживается через поле contractVersion в SimulatorContractDescriptor. Текущее значение фиксируется в BuiltinPluginFactory.CreateSnapshot и публикуется через GET /contract. Принцип семантического версионирования соблюдается явно. Major-инкремент отражает несовместимое изменение поверхности — удаление поля DTO, изменение типа поля, удаление маршрута или несовместимая смена семантики ответа. Minor-инкремент отражает совместимое расширение — добавление опционального поля, добавление нового маршрута, добавление нового значения в enum-подобное строковое поле. Patch-инкремент отражает исправление поведения без изменения поверхности.
Версия плагина отслеживается через поле version типа ContractVersion в PluginDescriptorBase и сериализуется как структура {major, minor, patch}. На уровне идентификатора плагина major-версия дублируется в виде суффикса .vN — это эквивалентно стратегии «major in path», применяемой в публичных HTTP API: новая major-версия публикуется под новым идентификатором, и старые клиенты не ломаются.
4.6.2 Стратегия breaking changes¶
Платформа в текущей фазе развития (pre-1.0) допускает breaking changes в рамках сознательной стратегии: новая major-версия contract-а публикуется в release notes, и предыдущие плагины помечаются как несовместимые с новым runtime до их обновления. Такая жёсткая модель применима постольку, поскольку количество внешних плагинов невелико и контролируется автором платформы; для post-1.0 модели рассматривается переход к параллельному обслуживанию двух последних major-версий с явным истечением поддержки.
Несовместимое изменение всегда сопровождается тремя артефактами. Запись в CHANGELOG.md с явной отметкой BREAKING. Обновление contractVersion в SimulatorContractDescriptor (на runtime-уровне) или major-инкремент в PluginDescriptorBase.version и идентификаторе (на уровне плагина). Раздел миграции в release notes с описанием шагов адаптации существующего кода. Совместимые расширения такого ритуала не требуют — достаточно записи в CHANGELOG.md и minor-инкремента.
4.6.3 Compatibility-проверки на этапе install плагина¶
При установке плагина CLI rusim plugin install выполняет три проверки совместимости. Первая — структурная: архив должен содержать manifest с полями id, version, compatibleRuntime и набор asset-файлов в ожидаемом layout-е. Вторая — версионная: значение compatibleRuntime сравнивается с major-версией текущего runtime, и при несовпадении установка отклоняется с диагностическим сообщением. Третья — конфликтная: при наличии в локальном реестре уже установленного плагина с тем же id пользователю предлагается перезаписать, отказаться или указать новый идентификатор.
Эти проверки выполняются полностью на стороне CLI до записи в ~/.rusim/plugin-registry.json. Runtime сам проверки совместимости не дублирует — это минимизирует нагрузку при загрузке сцены и предполагает, что плагин в реестре заведомо валиден относительно текущей версии. Соответственно, при обновлении runtime между двумя major-версиями пользователю необходимо переустановить несовместимые плагины через rusim plugin install повторно, что и фиксируется в release notes.
4.7 Примеры взаимодействия¶
4.7.1 Полный цикл reset → step → state на стороне Python¶
Минимальный тренировочный цикл через SimClient (python/sim_client/http_client.py) выглядит компактно:
from sim_client.http_client import SimClient
client = SimClient(base_url="http://127.0.0.1:8000", timeout_s=10.0)
assert client.health()["status"] == "ok"
reset_payload = {
"seed": 42,
"timeScale": 1.0,
"selectedTrackId": "track.cardboard_corridor.v1",
"selectedVehicleId": "vehicle.ks0223.v1",
"trackParams": [{"key": "obstacle_density", "value": "0.3"}],
"vehicleParams": [],
"flags": [],
"agents": [
{"agentId": "ego", "vehicleId": "vehicle.ks0223.v1", "isPrimary": True}
],
}
obs = client.reset(reset_payload)
for _ in range(1000):
command = {"throttle": 0.5, "steer": 0.0, "brake": 0.0}
obs = client.step(command)
if obs["done"]:
break
SimClient под капотом использует requests.Session() с настроенным HTTPAdapter (pool_connections=4, pool_maxsize=16) — это критично для тренировочных запусков на Windows, где per-request соединения быстро исчерпывают пул TCP ephemeral-портов и приводят к ошибкам WinError 10055 в subprocess-воркерах. Пример использования сессии — комментарий в http_client.py:14-17. Метод _raise_for_status (http_client.py:146-156) разворачивает поле error из ответа Unity в осмысленное requests.HTTPError, что делает диагностику пять минут потраченных на сценарий с опечаткой в selectedTrackId мгновенно понятной.
4.7.2 Загрузка ONNX-модели через backend и активация¶
Полный цикл загрузки и активации ONNX-модели через backend и SimClient:
from pathlib import Path
from sim_client.http_client import SimClient
client = SimClient(base_url="http://127.0.0.1:5210")
uploaded = client.upload_model(
artifact_path=Path("artifacts/policy.onnx"),
name="cardboard-corridor",
version="rev37",
source="train_cardboard_corridor_v9.py",
metadata_json='{"frameStack": 4, "imageSize": 96}',
metrics_json='{"successRate": 0.83}',
)
model_id = uploaded["modelId"]
activated = client.activate_model(model_id)
assert activated["isActive"] is True
binding = client.set_model_binding(
client_id="operator-1",
runtime_mode="unity-sim",
model_id=model_id,
agent_id="ego",
)
Метод upload_model (http_client.py:94-126) формирует multipart/form-data запрос, читая ONNX-файл потоком — это позволяет загружать артефакты сотни мегабайт без удвоения по памяти. Поле metadata принимает произвольный JSON-объект и сохраняется в model registry как часть записи ModelInfoDto.MetadataPath; типичное использование — фиксация гиперпараметров обучения и характеристик архитектуры (frame stack, размер входа, encoder type), которые потом нужны при запуске autopilot для соответствующей предобработки кадра.
После активации привязка модели к конкретному оператору и режиму через POST /api/model-bindings позволяет системе работать с несколькими активными клиентами одновременно — каждый со своей моделью на своём агенте без конфликтов.
4.7.3 Установка пользовательского плагина через CLI¶
Инсталляция собранного пользовательского плагина выглядит так:
$ rusim plugin install ./vehicle.my-robot.v1.rusim-plugin.zip
[rusim] reading manifest…
[rusim] id = vehicle.my-robot.v1
[rusim] version = 1.0.0
[rusim] contract = 1
[rusim] verifying compatibility against runtime contract 1 — ok
[rusim] writing to ~/.rusim/plugins/vehicle.my-robot.v1/
[rusim] updating ~/.rusim/plugin-registry.json
[rusim] installed.
$ rusim list vehicles
vehicle.ks0223.v1 KS0223 (built-in)
vehicle.prometeo.sport.v1 Prometeo Sport (built-in)
vehicle.my-robot.v1 My Robot
$ rusim inspect vehicle.my-robot.v1
id : vehicle.my-robot.v1
displayName : My Robot
version : 1.0.0
sensors : camera (RGB24, 96x96, 30Hz), ultrasonic (float32, 1, 10Hz)
actuators : drive (-1..+1), steer (-1..+1)
CLI выполняет описанные в 4.6.3 проверки совместимости и при успехе обновляет реестр в ~/.rusim/plugin-registry.json. При следующем запуске Unity PluginRegistry.Load читает реестр через папку Resources/UavSimulator/Plugins, сливает с встроенным PluginRegistryAsset и добавляет новый плагин в каталог. Команды rusim list vehicles и rusim inspect <id> используют GET /contract runtime-API и отображают каталог в человеко-читаемой форме.
Описанные три примера покрывают типовые точки взаимодействия с платформой — тренировочный цикл, операторская работа с моделями и расширение каталога — и одновременно демонстрируют, как три уровня API (Unity HTTP, backend, plugin SDK) работают совместно, не пересекаясь по ответственности.