diff --git a/README.md b/README.md index fc05ee9..0b1785e 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,7 @@ present. - [Live console and embedded Rerun runbook](docs/06_K1_LIVE_VIEWER.md) - [Mission Core monorepo and plugin boundary](docs/07_MISSION_CORE_MONOREPO.md) - [Monorepo architecture decision](docs/adr/0002-mission-core-monorepo.md) +- [Device plugin UI and runtime boundary](docs/adr/0003-device-plugin-ui-and-runtime-boundary.md) - [Redacted live lab report](docs/lab/001_K1_LIVE_MQTT_20260715.redacted.md) - [Session manifest schema](schemas/session-manifest.schema.json) - [Reference input provenance](docs/reference/README.md) diff --git a/apps/control-station/README.md b/apps/control-station/README.md index 5796f08..52acef5 100644 --- a/apps/control-station/README.md +++ b/apps/control-station/README.md @@ -16,6 +16,7 @@ device adapter, но структура интерфейса от него не | Контур | Состояние | Что это означает | | --- | --- | --- | | Mission Core fixed shell | Реализован | Header, навигация по разделам, рабочая поверхность, окна и инспекторы работают в одном приложении. | +| Device plugin registry | Реализован, v1alpha1 | До выбора модели provider остаётся inert и не делает I/O; каждый manifest объявляет ровно одну модель, custom `device.connection` UI key и backend factory. Frontend component подключается одним reviewed import в composition root. | | Локальный control plane | Реализован | React получает состояние и выполняет операции через FastAPI REST и WebSocket на loopback. | | K1 BLE → Wi-Fi | Реализован | Реальный BLE-поиск всех видимых устройств и одна подтверждённая provisioning-запись выбранному устройству. | | K1 live/replay MQTT | Реализован | Read-only приём, raw-first сохранение, декодирование облака точек и позы, реальные метрики. | @@ -42,7 +43,8 @@ K1 MQTT :1883, read-only └── встроенный @rerun-io/web-viewer └── пространственная сцена Mission Core -Mission Core Control Station ←→ REST /api/* + WebSocket /api/events +Mission Core Control Station ←→ REST /api/v1/device-plugins/* + + plugin-scoped WebSocket events FastAPI на 127.0.0.1:8000 CoreBluetooth + live/replay runtime ``` @@ -129,7 +131,7 @@ cd apps/control-station npm run dev ``` -Vite слушает `http://127.0.0.1:5173` и проксирует `/api` и `/api/events` на +Vite слушает `http://127.0.0.1:5173` и проксирует весь `/api` (включая WebSocket) на `http://127.0.0.1:8000`. Другой локальный backend можно указать переменной `VITE_API_TARGET`. Preview production-сборки запускается командой `npm run preview` на `http://127.0.0.1:4173`. @@ -138,18 +140,20 @@ Vite слушает `http://127.0.0.1:5173` и проксирует `/api` и `/ 1. Запустить `uv run k1link serve` и открыть Mission Core Control Station. 2. Выбрать **Парк → Локальное устройство**. -3. Включить K1, дождаться стабильного индикатора и подтвердить это в форме. -4. Нажать **Показать все BLE-устройства**. Интерфейс показывает полный результат +3. В каталоге моделей выбрать **XGRIDS LixelKity K1**. Только после этого + активируется runtime и монтируется custom UI XGRIDS-плагина. +4. Включить K1, дождаться стабильного индикатора и подтвердить это в форме. +5. Нажать **Показать все BLE-устройства**. Интерфейс показывает полный результат шестисекундного поиска; метка совместимости является подсказкой, выбор делает оператор. -5. Ввести SSID и пароль существующей сети и явно запустить подключение. Это одна +6. Ввести SSID и пароль существующей сети и явно запустить подключение. Это одна reviewed provisioning-запись без автоматических повторов. -6. Запустить live-приём по определённому адресу K1 либо replay локального +7. Запустить live-приём по определённому адресу K1 либо replay локального `.k1mqtt`/проверенного TSV. Физическое сканирование K1 запускается и останавливается подтверждённым двойным нажатием кнопки устройства. -7. Backend автоматически поднимет Rerun gRPC на TCP 9876 и опубликует адрес в +8. Backend автоматически поднимет Rerun gRPC на TCP 9876 и опубликует адрес в state. Ручной source вводить не требуется. -8. Открыть **Наблюдение → Пространственная сцена**. Реальные облако и траектория, +9. Открыть **Наблюдение → Пространственная сцена**. Реальные облако и траектория, частота, число точек, задержка и пропуски preview появятся после прихода сообщений K1. @@ -197,16 +201,26 @@ Custom timeline, переключатель 2D/3D/карты, семантиче | Метод | Route | Тело / назначение | | --- | --- | --- | | `GET` | `/api/health` | Проверка локального сервиса. | -| `GET` | `/api/state` | Авторитетный snapshot состояния. | -| `POST` | `/api/ble/scan` | `{ "duration_seconds": 6 }`. | -| `POST` | `/api/connect` | `{ "device_id", "ssid", "password" }`. | -| `POST` | `/api/session/live` | Опционально `{ "host", "duration_seconds" }`. | -| `POST` | `/api/session/replay` | `{ "path", "speed", "loop" }`. | -| `POST` | `/api/session/stop` | Остановка активного источника. | -| `POST` | `/api/viewer/settings` | Размер/цвет точек, накопление, видимость облака, траектории и сетки. | -| `WS` | `/api/events` | Периодические snapshots для live UI. | +| `GET` | `/api/v1/device-plugins` | Валидированные manifests установленных device plugins. | +| `GET` | `/api/v1/device-models` | Backend-каталог моделей из валидированных manifests; UI-каталог текущей сборки формируется отдельным static composition root. | +| `POST` | `/api/v1/device-plugins/{pluginId}/actions/{actionId}` | Namespaced действие через host allowlist; тело `{ "input": { ... } }`. | +| `WS` | `/api/v1/device-plugins/{pluginId}/events` | Plugin-scoped snapshots с `pluginId` и монотонным `sequence`. | -`/api/state` и изменяющие состояние ответы могут вернуть snapshot напрямую или +Frontend XGRIDS-плагина использует только v1alpha namespaced routes. Старые +`/api/state`, `/api/events`, `/api/ble/scan`, `/api/connect`, `/api/session/*` и +`/api/viewer/settings` сохранены как deprecated compatibility shims внутри +XGRIDS backend contribution. + +В v1alpha1 backend composition загружается из manifests, а frontend plugins +статически включаются в сборку через `src/composition/devicePlugins.ts`. +Автоматической runtime-сверки двух installed sets пока нет: их соответствие — +проверяемое требование сборки до появления подписанных plugin bundles и startup +compatibility handshake. Поля manifest `permissions`, `mutating` и +`secretFields` пока являются декларативными метаданными; generic host валидирует +их форму и action allowlist, но ещё не реализует на их основе RBAC, подтверждения +оператора или secret-vault substitution. + +Legacy `/api/state` и изменяющие состояние ответы могут вернуть snapshot напрямую или как `{ "state": { ... } }`. Текущие поля включают `phase`, `message`, `devices`, `selected_device_id`, `k1_ip`, `source_mode`, `metrics`, `rerun_grpc_url` и `viewer_settings`. Legacy-поля `foxglove_ws_url`/`foxglove_viewer_url` остаются @@ -218,14 +232,15 @@ Custom timeline, переключатель 2D/3D/карты, семантиче | --- | --- | | `src/App.tsx` | Fixed shell, выбор разделов и окна source/display/layers/layout. | | `src/productModel.ts` | Архитектурные разделы, рабочие поверхности и уровни готовности. | -| `src/workspaces/DeviceWorkspace.tsx` | Реальный K1 BLE/Wi-Fi/live/replay adapter UI. | +| `src/core/device-plugins/` | Vendor-neutral manifest parser, registry, lifecycle и plugin host. | +| `src/core/runtime/` | Нормализованное состояние активного устройства и spatial source. | +| `src/composition/devicePlugins.ts` | Единственный allowlist импортов конкретных device plugins. | +| `src/workspaces/DeviceWorkspace.tsx` | Generic выбор модели и `device.connection` slot. | +| `src/device-plugins/xgrids-k1/` | Реальный K1 BLE/Wi-Fi/live/replay UI, client и compatibility mapper. | | `src/workspaces/Workspaces.tsx` | Оперативный обзор, spatial viewport и остальные продуктовые поверхности. | | `src/components/RerunViewport.tsx` | Жизненный цикл встроенного Rerun Web Viewer и selection events. | -| `src/api.ts` | REST/WebSocket контракт с FastAPI. | -| `src/useK1Console.ts` | Состояние backend, polling, события и действия оператора. | | `src/sceneSettings.ts` | Типизированный UI-профиль пространственной сцены. | -| `src/presentation.ts` | Русские подписи фаз и форматирование реальных метрик. | -| `src/messages.ts` | Обезличивание и локализация технических сообщений в пользовательском интерфейсе. | +| `src/presentation.ts` | Vendor-neutral подписи lifecycle и форматирование нормализованных метрик. | | `src/styles.css`, `src/styles/*` | Компоновка shell и рабочих поверхностей. | ## Safety и чувствительные данные diff --git a/apps/control-station/src/App.tsx b/apps/control-station/src/App.tsx index 8830013..ee55016 100644 --- a/apps/control-station/src/App.tsx +++ b/apps/control-station/src/App.tsx @@ -26,7 +26,9 @@ import { } from "@nodedc/ui-react"; import { LandingStage } from "./components/LandingStage"; -import type { ViewerSettings } from "./api"; +import { useDevicePluginHost } from "./core/device-plugins/DevicePluginHost"; +import { useMissionRuntime } from "./core/runtime/MissionRuntimeContext"; +import type { ViewerSettings } from "./core/runtime/contracts"; import { rootById, roots, @@ -35,14 +37,12 @@ import { type RootId, } from "./productModel"; import { backendLabel, phaseLabel, phaseTone } from "./presentation"; -import { localizeRuntimeMessage } from "./messages"; import { defaultSceneSettings, type PointColorMode, type PointPalette, type SceneSettings, } from "./sceneSettings"; -import { useK1Console } from "./useK1Console"; import { DeviceWorkspace } from "./workspaces/DeviceWorkspace"; import { WorkspaceRenderer } from "./workspaces/Workspaces"; import "./styles/scene-windows.css"; @@ -106,7 +106,8 @@ function mergeViewerSettings( } export default function App() { - const console = useK1Console(); + const runtime = useMissionRuntime(); + const { selection } = useDevicePluginHost(); const workspace = useApplicationWorkspace({ navigationOpen: false, contentExpanded: true, @@ -129,17 +130,17 @@ export default function App() { const activeDefinition = workspaceById(workspace.activeView); const rootWorkspaces = workspacesForRoot(activeRoot); const activeSceneWindow = sceneWindowOrder[sceneWindowOrder.length - 1] ?? null; - const automaticSourceUrl = console.state?.rerun_grpc_url?.trim() ?? ""; + const automaticSourceUrl = runtime.state?.spatialSource?.url.trim() ?? ""; const effectiveSourceUrl = sourceUrl || automaticSourceUrl; useEffect(() => { - const remote = console.state?.viewer_settings; + const remote = runtime.state?.viewerSettings; if (!remote) return; setSceneSettings((current) => mergeViewerSettings(current, remote)); if (!displayWindowOpen) { setDisplayDraft((current) => mergeViewerSettings(current, remote)); } - }, [console.state?.viewer_settings, displayWindowOpen]); + }, [runtime.state?.viewerSettings, displayWindowOpen]); const activateSceneWindow = useCallback((windowId: SceneToolWindowId) => { setSceneWindowOrder((current) => [ @@ -208,7 +209,7 @@ export default function App() { }; const applyDisplaySettings = async () => { - const applied = await console.updateViewerSettings(toViewerSettings(displayDraft)); + const applied = await runtime.updateViewerSettings(toViewerSettings(displayDraft)); if (applied) setSceneSettings(displayDraft); }; @@ -217,7 +218,7 @@ export default function App() { const next = { ...sceneSettings, ...patch }; setSceneSettings(next); setDisplayDraft((current) => (displayWindowOpen ? { ...current, ...patch } : next)); - const applied = await console.updateViewerSettings(toViewerSettings(next)); + const applied = await runtime.updateViewerSettings(toViewerSettings(next)); if (!applied) { setSceneSettings(previous); if (!displayWindowOpen) setDisplayDraft(previous); @@ -231,7 +232,7 @@ export default function App() { actions.push({ label: "Обновить состояние локального контура", icon: "refresh", - onClick: () => void console.refresh(), + onClick: () => void runtime.refresh(), }); } @@ -249,7 +250,7 @@ export default function App() { onClick: openLayout, }); return actions; - }, [activeDefinition?.kind, console, sceneSettings, sourceUrl]); + }, [activeDefinition?.kind, runtime, sceneSettings, sourceUrl]); const header = ( - void console.refresh()} title="Обновить локальный контур"> -