-
Notifications
You must be signed in to change notification settings - Fork 1
[go][js] update /api/capabilities #430
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f57fe4a
c950440
6387374
a19ab8d
8a5049d
80ae085
edbeec8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| # Multi-Sensor Capabilities API Plan | ||
|
|
||
| - **Status:** Complete | ||
| - **Layers:** API, Frontend, cmd/radar | ||
| - **Canonical:** `internal/api/server.go`, `cmd/radar/capabilities.go` | ||
|
|
||
| Redesign `/api/capabilities` to support multiple named sensors per class, | ||
| future-proofing for deployments with more than one radar or LiDAR unit. | ||
|
|
||
| ## 1. Current Format | ||
|
|
||
| ```json | ||
| { | ||
| "radar": true, | ||
| "lidar": { "enabled": false, "state": "disabled" }, | ||
| "lidar_sweep": false | ||
| } | ||
| ``` | ||
|
|
||
| Flat structure — one boolean for radar, one object for LiDAR. No room for a | ||
| second sensor of either class without a breaking change. | ||
|
|
||
| ## 2. Target Format | ||
|
|
||
| Two top-level keys — `radar` and `lidar` — each a **named object** | ||
| (keys are stable, human-assigned sensor names). No `_sensors` suffix so path | ||
| access stays light: `$.lidar.hesai.enabled`. | ||
|
|
||
| ### Single-sensor deployment (today) | ||
|
|
||
| ```json | ||
| { | ||
| "radar": { | ||
| "default": { | ||
| "enabled": true, | ||
| "status": "receiving", | ||
| "last_reading_at": "2026-03-24T06:45:12Z" | ||
| } | ||
| }, | ||
| "lidar": {} | ||
| } | ||
| ``` | ||
|
|
||
| ### Multi-sensor deployment (future) | ||
|
|
||
| ```json | ||
| { | ||
| "radar": { | ||
| "ops243_front": { | ||
| "enabled": true, | ||
| "status": "receiving", | ||
| "last_reading_at": "2026-03-24T06:45:12Z" | ||
| }, | ||
| "ops243_rear": { | ||
| "enabled": true, | ||
| "status": "stale", | ||
| "last_reading_at": "2026-03-23T02:11:44Z" | ||
| } | ||
| }, | ||
| "lidar": { | ||
| "hesai": { | ||
| "enabled": true, | ||
| "status": "receiving", | ||
| "last_reading_at": "2026-03-24T07:38:59Z", | ||
| "sweep": true | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Why named objects over lists | ||
|
|
||
| | Concern | Named objects | Lists | | ||
| |---------|--------------|-------| | ||
| | Lookup by identity | `caps.radar["ops243_front"]` — O(1), stable key | Must scan by name field | | ||
| | Diffing across polls | Keys are stable — trivial Svelte keying | Index shifts on removal | | ||
| | Go type | `map[string]SensorStatus` — idiomatic | `[]SensorStatus` + Name field | | ||
| | Uniqueness | Structural — keys unique by definition | Must validate no duplicates | | ||
| | Ordering | Maps unordered (UI sorts by name) | Ordered but meaningless | | ||
|
|
||
| Named objects win on every axis relevant here. | ||
|
|
||
| ### Field definitions | ||
|
|
||
| | Field | Type | Description | | ||
| |-------|------|-------------| | ||
| | `enabled` | `bool` | Sensor channel was activated at startup | | ||
| | `status` | `string` | Runtime state: `disabled`, `starting`, `receiving`, `stale`, `error` | | ||
| | `last_reading_at` | `string \| null` | ISO 8601 timestamp of last data received; `null` = never | | ||
| | `sweep` | `bool` | (lidar only) Sweep/auto-tuner operational | | ||
|
|
||
| ### State machine | ||
|
|
||
| ``` | ||
| disabled → starting → receiving ⇄ stale | ||
| ↘ error | ||
| ``` | ||
|
Comment on lines
+87
to
+97
|
||
|
|
||
| ### Empty map semantics | ||
|
|
||
| `{}` = no sensors of this class are currently configured or active for this | ||
| sensor class. Providers may omit disabled sensors from the map, so a missing | ||
| entry is treated the same as a disabled or unconfigured sensor. | ||
|
|
||
| ## 3. Go Types | ||
|
|
||
| ```go | ||
| // SensorStatus is the per-sensor health snapshot. | ||
| type SensorStatus struct { | ||
| Enabled bool `json:"enabled"` | ||
| Status string `json:"status"` | ||
| LastReadingAt *string `json:"last_reading_at"` | ||
| } | ||
|
|
||
| // LidarSensorStatus extends SensorStatus with lidar-specific fields. | ||
| type LidarSensorStatus struct { | ||
| SensorStatus | ||
| Sweep bool `json:"sweep"` | ||
| } | ||
|
|
||
| // Capabilities is the JSON shape returned by /api/capabilities. | ||
| type Capabilities struct { | ||
| Radar map[string]SensorStatus `json:"radar"` | ||
| Lidar map[string]LidarSensorStatus `json:"lidar"` | ||
| } | ||
| ``` | ||
|
|
||
| `map[string]T` marshals to `{}` when empty — clean, no `null` vs `[]` ambiguity. | ||
|
|
||
| ## 4. Frontend Types | ||
|
|
||
| ```typescript | ||
| interface SensorStatus { | ||
| enabled: boolean; | ||
| status: 'disabled' | 'starting' | 'receiving' | 'stale' | 'error'; | ||
| last_reading_at: string | null; | ||
| } | ||
|
|
||
| interface LidarSensorStatus extends SensorStatus { | ||
| sweep: boolean; | ||
| } | ||
|
|
||
| interface Capabilities { | ||
| radar: Record<string, SensorStatus>; | ||
| lidar: Record<string, LidarSensorStatus>; | ||
| } | ||
| ``` | ||
|
|
||
| ### Convenience derivations | ||
|
|
||
| ```typescript | ||
| const anyLidarEnabled = derived(capabilities, ($c) => | ||
| Object.values($c.lidar).some(s => s.enabled) | ||
| ); | ||
| ``` | ||
|
|
||
| ## 5. Migration Path | ||
|
|
||
| | Old field | New location | | ||
| |-----------|-------------| | ||
| | `radar: true` | `radar.default.enabled = true` | | ||
| | `lidar.enabled` | `lidar.default.enabled` (or empty map) | | ||
| | `lidar.state` | `lidar.default.status` | | ||
| | `lidar_sweep` | `lidar.default.sweep` | | ||
|
|
||
| Frontend ships embedded in the binary — both sides change atomically. No | ||
| backwards-compatibility shim needed. | ||
|
|
||
| ## 6. Sensor naming | ||
|
|
||
| Keys are stable, human-assigned identifiers. Today the single radar/lidar uses | ||
| `"default"`. Multi-sensor deployments use descriptive names: | ||
|
|
||
| - `"ops243_front"`, `"ops243_rear"` — by model and position | ||
| - `"hesai"`, `"hesai_kerb"` — by model and role | ||
|
|
||
| Names come from CLI flags or config at startup. The server rejects duplicate | ||
| names. | ||
|
|
||
| ## 7. Future extensibility | ||
|
|
||
| A third sensor class (thermal, ultrasonic) is just another top-level key: | ||
|
|
||
| ```json | ||
| { | ||
| "radar": { ... }, | ||
| "lidar": { ... }, | ||
| "thermal": { ... } | ||
| } | ||
| ``` | ||
|
|
||
| Each class gets its own extended status type if it has class-specific fields. | ||
|
|
||
| ## 8. Implementation Checklist | ||
|
|
||
| ### Backend (Go) | ||
|
|
||
| - [x] Replace `Capabilities`, `LidarCapability` structs in `internal/api/server.go` | ||
| with new `SensorStatus`, `LidarSensorStatus`, `Capabilities` types | ||
| - [x] Update `showCapabilities` default in `internal/api/server_admin.go` | ||
| - [x] Rewrite `capabilitiesProvider` in `cmd/radar/capabilities.go` to populate | ||
| `map[string]SensorStatus` / `map[string]LidarSensorStatus` | ||
| - [x] Update `internal/api/capabilities_test.go` | ||
| - [x] Update `cmd/radar/capabilities_test.go` | ||
|
|
||
| ### Frontend (Svelte/TypeScript) | ||
|
|
||
| - [x] Update `Capabilities`, `LidarCapability` types in `web/src/lib/api.ts` | ||
| - [x] Update default capabilities and derived stores in | ||
| `web/src/lib/stores/capabilities.ts` | ||
| - [x] Update layout gate in `web/src/routes/+layout.svelte` to use | ||
| `Object.values($capabilities.lidar).some(s => s.enabled)` | ||
| - [x] Update `web/src/lib/stores/capabilities.test.ts` | ||
|
|
||
| ### Validation | ||
|
|
||
| - [x] `make lint-go && make test-go` | ||
| - [x] `make lint-web && make test-web` | ||
| - [x] `make build-web` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plan/examples and type definitions include a
last_reading_atfield, but the implemented Go/TypeScriptSensorStatustypes in this PR do not include it. Either implementlast_reading_atend-to-end or remove it from the plan/spec so the documentation matches the shipped API.