Author SHA1 Message Date
eSlider 762c236310 fix(ci): fetch PR ref on checkout instead of shallow main clone
CI & Release / Verify simulator (push) Skipped
CI & Release / Trivy scan (push) Skipped
CI & Release / Semantic Release (push) Skipped
CI & Release / Verify simulator (pull_request) Successful in 17s
CI & Release / Trivy scan (pull_request) Successful in 20s
CI & Release / Semantic Release (pull_request) Skipped
PR branches forked from main cannot checkout gitea.sha after a
depth-limited default-branch clone; fetch refs/pull/N/head instead.
2026-07-12 12:56:06 +01:00
eSlider df7f38a6a5 docs: add live demo links to design journey
CI & Release / Verify simulator (push) Skipped
CI & Release / Trivy scan (push) Skipped
CI & Release / Semantic Release (push) Skipped
CI & Release / Verify simulator (pull_request) Failing after 3s
CI & Release / Trivy scan (pull_request) Skipped
CI & Release / Semantic Release (pull_request) Skipped
Link swarm-data-explorer and swarm-analalytics in sections 7 and 9.
Same access note as swarm.produktor.io; no credentials in docs.
2026-07-10 12:18:21 +01:00
11 changed files with 65 additions and 94 deletions
+8 -4
View File
@@ -24,8 +24,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
git clone --depth=50 https://${{ gitea.actor }}:${{ gitea.token }}@git.produktor.io/${{ gitea.repository }}.git . git init .
git checkout ${{ gitea.sha }} git remote add origin https://${{ gitea.actor }}:${{ gitea.token }}@git.produktor.io/${{ gitea.repository }}.git
git fetch --depth=50 origin "${{ gitea.ref }}"
git checkout FETCH_HEAD
- name: Install dependencies - name: Install dependencies
run: pip install --quiet -r simulator/requirements.txt run: pip install --quiet -r simulator/requirements.txt
@@ -61,8 +63,10 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
git clone --depth=1 https://${{ gitea.actor }}:${{ gitea.token }}@git.produktor.io/${{ gitea.repository }}.git . git init .
git checkout ${{ gitea.sha }} git remote add origin https://${{ gitea.actor }}:${{ gitea.token }}@git.produktor.io/${{ gitea.repository }}.git
git fetch --depth=1 origin "${{ gitea.ref }}"
git checkout FETCH_HEAD
- name: Install Trivy - name: Install Trivy
run: | run: |
-5
View File
@@ -13,8 +13,3 @@ dist/
.terraform/ .terraform/
*.tfstate *.tfstate
*.tfstate.* *.tfstate.*
# Local tooling / WIP (keep on disk, never commit)
.cursor/
.grok/
prototype-3d/
-19
View File
@@ -17,25 +17,6 @@ This repository describes how to build, deliver, test, and operate that platform
7. **SQL is the contract.** Peer data access is read-only DuckDB SQL over SSH forced commands — the query language already lives on both ends, so no service, port, or protocol is invented for it. 7. **SQL is the contract.** Peer data access is read-only DuckDB SQL over SSH forced commands — the query language already lives on both ends, so no service, port, or protocol is invented for it.
8. **No debug API.** The bench and the flight use the same channel: an engineer debugging on the ground runs the identical query through the identical wrapper, permissions, and output format a peer drone would use. What you test is what flies. 8. **No debug API.** The bench and the flight use the same channel: an engineer debugging on the ground runs the identical query through the identical wrapper, permissions, and output format a peer drone would use. What you test is what flies.
None of the concrete tool picks above are mandates. This repo is a **from-scratch platform sketch**: enough structure to hire and build against, with every decision recorded so the team can replace a piece when a better fit appears.
## Implemented now vs proposed next
Honest map so a reader knows what runs today versus what is design intent.
| Area | Implemented in this repo (runnable PoC) | Proposed for a production air-gapped fleet |
| --- | --- | --- |
| On-board layout | Hive-partitioned Parquet writer, seal step, DuckDB views | Same contract; Compose services under systemd |
| Pose path | Fixed **45-byte** UDP frame + 2D bandwidth visualisation | Zenoh pub/sub (UDP kept as degraded minimal profile) |
| Peer query | Read-only SQL **gate** over HTTP explorer (keyword allow-list) | Same gate idea via **SSH forced command** + OS/engine hardening |
| Bulk sync | Visualised opportunistic transfer volume | rsync/rclone over persistent SSH between peers |
| Mesh trust | Ansible templates for WireGuard + ed25519 forced commands | Provisioned per-device keys; nothing joins at runtime |
| Ground segment | k3d/Terraform sim: lake, Grafana, offload CronJob, optional MinIO | k3s warehouse, GitOps overlays, post-flight mirror |
| CI / delivery | GitHub Actions: pytest, smoke flight, Trivy, semver release + fleet manifest artifact | Self-hosted GitLab + registry inside the air gap (same stages) |
| Docs | Problem, architecture, ADRs, design journey, open questions | Living ADRs owned by the team |
Start from the [design journey](docs/12-design-journey.md) for the story; use the table above when reviewing scope.
## Documentation ## Documentation
| Document | Contents | | Document | Contents |
+8 -11
View File
@@ -20,9 +20,8 @@ graph TB
subgraph serving [Layer 3 — Serving and sync] subgraph serving [Layer 3 — Serving and sync]
HOOK["event hook<br/>fires on new derived data"] HOOK["event hook<br/>fires on new derived data"]
PUB["state publisher<br/>pub/sub broadcast"] PUB["state publisher<br/>pub/sub broadcast"]
BULK["bulk sync<br/>rsync over SSH"] MINIO["MinIO<br/>derived datasets bucket"]
MINIO["MinIO<br/>optional derived bucket"] QAPI["query API<br/>peer data requests"]
QAPI["query API<br/>SQL-over-SSH"]
end end
end end
@@ -34,14 +33,12 @@ graph TB
WRITER -->|derived rows| HOOK WRITER -->|derived rows| HOOK
HOOK --> PUB HOOK --> PUB
HOOK --> MINIO HOOK --> MINIO
HOOK --> BULK
QAPI --> DUCK QAPI --> DUCK
PUB -.->|mesh pose| PEERS["peer drones"] PUB -.->|mesh| PEERS["peer drones"]
BULK -.->|sealed partitions| PEERS MINIO -.->|replication| PEERS
QAPI -.->|on demand| PEERS QAPI -.->|on demand| PEERS
``` ```
MinIO stays available where an S3 API helps (on-board derived datasets, ground warehouse). **In-flight peer bulk sync is SSH/rsync**, not object-store replication — see [04 — Swarm sync](04-swarm-sync.md).
### Layer 1 — Ingestion ### Layer 1 — Ingestion
- `sensor-ingest` subscribes to sensor sources (ROS 2 topics where available, raw drivers otherwise) and normalizes them into typed streams: IMU, barometer, temperature, LiDAR, RSSI, power, and so on. - `sensor-ingest` subscribes to sensor sources (ROS 2 topics where available, raw drivers otherwise) and normalizes them into typed streams: IMU, barometer, temperature, LiDAR, RSSI, power, and so on.
@@ -56,10 +53,10 @@ MinIO stays available where an S3 API helps (on-board derived datasets, ground w
### Layer 3 — Serving and sync ### Layer 3 — Serving and sync
- The **event hook** is the on-board "lambda": when the writer lands new *derived* rows (state, detections), it triggers registered actions — broadcast, optional MinIO put, bulk-sync hint, or a local mission-logic callback. Nothing polls. - The **event hook** is the on-board "lambda": when the writer lands new *derived* rows (state, detections), it triggers registered actions — broadcast, MinIO upload, or a local mission-logic callback. Nothing polls.
- The **state publisher** broadcasts compact position/attitude/detection payloads over the mesh pub/sub (UDP in the PoC; Zenoh proposed — [04](04-swarm-sync.md)). - The **state publisher** broadcasts compact position/attitude/detection payloads over the mesh pub/sub (transport analysis in [04 — Swarm sync](04-swarm-sync.md)).
- **Bulk sync** pulls sealed derived partitions from peers over persistent SSH (rsync delta transfer). MinIO is optional where an S3 API is wanted; it is **not** the in-flight peer replication path. - **Bulk sync** pulls sealed derived partitions from peers over persistent SSH (rsync delta transfer); MinIO remains optional where an S3 API is wanted.
- **Peer queries** are read-only DuckDB SQL — HTTP explorer gate in the PoC; **SSH forced commands** proposed for flight (SELECT-only gate, read-only OS user, columnar responses [04](04-swarm-sync.md)). - **Peer queries** are read-only DuckDB SQL over SSH forced commands — SELECT-only gate, read-only OS user, columnar responses (the design decision and its reasoning are in [04](04-swarm-sync.md)).
## Communication planes ## Communication planes
+13 -23
View File
@@ -16,27 +16,19 @@ Bandwidth is the scarcest resource in the system. Every message class gets an ex
## Broadcast payload: small on the wire, precise at rest ## Broadcast payload: small on the wire, precise at rest
The `state` broadcast is a fixed compact frame. The runnable PoC encodes it as The `state` broadcast is a fixed compact frame:
**45 bytes** little-endian (`simulator/virtual_drone/broadcast.py`); that size is
what the prototype uses for bandwidth estimates.
| Field | Wire type (PoC) | Notes | | Field | Type | Notes |
| --- | --- | --- | | --- | --- | --- |
| `magic` + `version` | 2s + uint8 | `b"SH"`, version `1` | | `drone_id` | uint16 | Fleet-scoped registry |
| `drone_id` | 8s ascii | Zero-padded; a fleet `uint16` registry id is a natural production swap |
| `ts_ns` | int64 | Epoch nanoseconds, same clock domain as storage | | `ts_ns` | int64 | Epoch nanoseconds, same clock domain as storage |
| `pos_x/y/z` | 3 × int32 | **Millimeters** in the **mission frame** — quantized on the wire only; storage keeps full float precision | | `pos_x/y/z` | int32 | **Millimeters** in the mission frame — quantized only here, storage keeps full float precision |
| `att_roll/pitch/yaw` | 3 × int16 | Centi-degrees — attitude stays on the wire so peers need no local shape model | | `att_roll/pitch/yaw` | int16 | Centi-degrees |
| `vel_x/y/z` | 3 × int16 | cm/s | | `vel_x/y/z` | int16 | cm/s |
| `frame_ref` | uint8 | Frame of reference id (GPS-denied: local/visual-odometry frames must be explicit) | | `frame_ref` | uint8 | Frame of reference id (GPS-denied: local/visual-odometry frames must be explicit) |
| `flags` | uint8 | Battery-low, returning, degraded-sensors, … | | `flags` | uint8 | Battery-low, returning, degraded-sensors, … |
45 bytes × 5 Hz × 50 drones ≈ 11 KB/s of pose traffic before transport overhead — still trivial on a congested mesh. ~40 bytes per frame → a 50-drone swarm at 5 Hz is ~10 KB/s of pose traffic before transport overhead. Trivial even on a congested mesh.
An earlier sketch used relative coordinates and a bounding sphere (no attitude).
It was dropped: mission-frame pose + attitude is simpler to fuse post-flight and
costs almost nothing at this frame size. Relative localization remains a
consumer concern when `frame_ref` differs across peers ([09](09-open-questions.md)).
`detections` events are slightly larger (class, confidence, bounding volume, ego-pose) but event-shaped and rare by comparison. `detections` events are slightly larger (class, confidence, bounding volume, ego-pose) but event-shaped and rare by comparison.
@@ -59,27 +51,25 @@ graph LR
SB -->|"store-and-forward relay"| SC SB -->|"store-and-forward relay"| SC
``` ```
### Recommended (proposal): Zenoh ### Recommended: Zenoh
- Designed exactly for constrained, dynamic networks: built-in peer discovery, brokerless peer-to-peer mode, store-and-forward, and a query layer on top of pub/sub. - Designed exactly for constrained, dynamic networks: built-in peer discovery, brokerless peer-to-peer mode, store-and-forward, and a query layer on top of pub/sub.
- First-class robotics citizenship: an official ROS 2 RMW implementation exists, so the ingestion side and the sync side can share one middleware. - First-class robotics citizenship: an official ROS 2 RMW implementation exists, so the ingestion side and the sync side can share one middleware.
- Tiny footprint, ARM64-native. - Tiny footprint, ARM64-native.
**PoC today:** the simulator and the 2D prototype exercise the **raw UDP** pose path only — the minimal degraded profile below. Zenoh is the proposed production pub/sub, not yet wired into the runnable stack.
### Alternatives considered ### Alternatives considered
| Option | Verdict | | Option | Verdict |
| --- | --- | | --- | --- |
| **DDS multicast** (ROS 2 default) | Works, battle-tested; but discovery storms and tuning pain on lossy wireless meshes are well documented. Keep as fallback since ROS 2 speaks it natively | | **DDS multicast** (ROS 2 default) | Works, battle-tested; but discovery storms and tuning pain on lossy wireless meshes are well documented. Keep as fallback since ROS 2 speaks it natively |
| **MQTT** | Needs a broker — a per-drone broker bridge is possible but adds moving parts for no gain over Zenoh | | **MQTT** | Needs a broker — a per-drone broker bridge is possible but adds moving parts for no gain over Zenoh |
| **Raw UDP multicast** | **Implemented in the PoC** for pose broadcast (fixed frame, no discovery); no query layer, no reliability — also the documented degraded mode | | **Raw UDP multicast** | Perfect as a last-resort minimal profile for the pose broadcast alone (fixed frame, no discovery); no query layer, no reliability — documented as the degraded mode |
| **MinIO bucket replication** | Wrong tool for the 5 Hz pose path; optional on board for derived datasets and primary on the ground warehouse — not the in-flight bulk path | | **MinIO bucket replication** | Wrong tool for the 5 Hz pose path, right tool for bulk derived datasets — see below |
## Two sync mechanisms, deliberately separate ## Two sync mechanisms, deliberately separate
1. **Fast path — pub/sub (Zenoh proposed; UDP in the PoC):** pose frames and detection events. Fire-and-forget with bounded staleness; consumers keep a peer-state cache. 1. **Fast path — pub/sub (Zenoh):** pose frames and detection events. Fire-and-forget with bounded staleness; consumers keep a peer-state cache.
2. **Bulk path — rsync over persistent SSH (proposed):** sealed `detections`/`state` Parquet partitions are pulled opportunistically between drones when links allow. This is how a drone that was out of range catches up on mission history without anyone re-sending events. The PoC visualises bulk volume; it does not yet run real rsync between virtual drones. 2. **Bulk path — rsync over persistent SSH:** sealed `detections`/`state` Parquet partitions are pulled opportunistically between drones when links allow. This is how a drone that was out of range catches up on mission history without anyone re-sending events.
Why SSH-based bulk sync over object-store replication: Why SSH-based bulk sync over object-store replication:
@@ -115,7 +105,7 @@ SQL access must not become a write channel. A single "read-only connection" flag
| Layer | Mechanism | What it stops | | Layer | Mechanism | What it stops |
| --- | --- | --- | | --- | --- | --- |
| 1. Key = operation | Forced command: the query key can only invoke the query wrapper, nothing else | Arbitrary exec, lateral movement | | 1. Key = operation | Forced command: the query key can only invoke the query wrapper, nothing else | Arbitrary exec, lateral movement |
| 2. Statement gate | Wrapper accepts a single statement, rejects anything but `SELECT`/`WITH`/… (no `COPY`, `ATTACH`, `INSTALL`, `SET`, multi-statements); production should prefer a real parser + bound parameters, not keywords alone | SQL-as-a-write-channel, config tampering | | 2. Statement gate | Wrapper accepts a single statement, parses it, rejects anything but `SELECT` (no `COPY`, `ATTACH`, `INSTALL`, `SET`, multi-statements); parameters bound, not interpolated | SQL-as-a-write-channel, config tampering |
| 3. OS permissions | Wrapper runs as a dedicated user with **read-only filesystem access** to the data root and write access to nothing | Any write that slips past layer 2 | | 3. OS permissions | Wrapper runs as a dedicated user with **read-only filesystem access** to the data root and write access to nothing | Any write that slips past layer 2 |
| 4. Engine hardening | `:memory:` database, external access disabled except the data-root glob, extension loading off | Reaching outside the store | | 4. Engine hardening | `:memory:` database, external access disabled except the data-root glob, extension loading off | Reaching outside the store |
| 5. Resource caps | Timeout, memory cap, niced CPU (flight software always wins), response size budget | Denial of service via expensive queries | | 5. Resource caps | Timeout, memory cap, niced CPU (flight software always wins), response size budget | Denial of service via expensive queries |
+25 -15
View File
@@ -1,12 +1,9 @@
# 12 — Design journey # 12 — Design journey
How this design came together, told in the order the thinking actually happened. How this design came together, told in the order the thinking actually happened.
It is a walk-through, not a report — and **not a mandate**. The goal is a It is a walk-through, not a report. The formal decisions, with options and
from-scratch platform sketch that shows how the pieces fit; every concrete trade-offs, live as [Architecture Decision Records](adr/README.md). This is the
choice is an option with trade-offs recorded as story behind them, and each chapter links straight into the code it produced.
[Architecture Decision Records](adr/README.md). Replace any piece if a better
fit shows up. This is the story behind the decisions, and each chapter links
straight into the code it produced.
--- ---
@@ -73,17 +70,15 @@ stored and in which structure, not to step into that work.
## 5. What actually needs to sync ## 5. What actually needs to sync
The most time-critical item is where each peer is, so every unit has time to The most time-critical item is where each peer is, so every unit has time to
react. The wire frame carries **mission-frame position** (millimetres on the react. The choice was to broadcast [relative pose](04-swarm-sync.md) (x, y, z and
wire, full float at rest), **attitude**, **velocity**, a `frame_ref`, and flags — time) instead of absolute coordinates. Relative is cheaper and enough for
45 bytes at 5 Hz. That is cheap enough that shrinking further is not worth the coordination. To avoid sending orientation, a unit is modelled as a **sphere**
fusion pain. An earlier sketch used relative coordinates and a bounding sphere that bounds its extent. That trades a little compute for much less data, and no
with no orientation; it was dropped. When peers disagree on frames, per-shape encoding. If the units are identical, their 3D model can be provisioned
`frame_ref` makes the mismatch explicit for consumers ahead instead of transmitted.
([09 — Open questions](09-open-questions.md)).
> **Read the code** > **Read the code**
> - [`simulator/virtual_drone/broadcast.py`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/simulator/virtual_drone/broadcast.py#L1-L45) — the 45-byte pose frame, position quantized on the wire only > - [`simulator/virtual_drone/broadcast.py`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/simulator/virtual_drone/broadcast.py#L1-L40) — the 46-byte pose frame, position quantized on the wire only
> - [`prototype/src/sim.ts`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/prototype/src/sim.ts#L60-L63) — `POSE_BYTES = 45` drives the volume estimate
> - [`prototype/src/sim.ts`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/prototype/src/sim.ts#L260-L277) — pose broadcasts at 5 Hz and opportunistic bulk sync, made visible > - [`prototype/src/sim.ts`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/prototype/src/sim.ts#L260-L277) — pose broadcasts at 5 Hz and opportunistic bulk sync, made visible
## 6. Security as nested layers, not one wall ## 6. Security as nested layers, not one wall
@@ -134,6 +129,13 @@ coming back to foundations DevOps and sysadmins already trust.
> - [`simulator/explorer/server.py`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/simulator/explorer/server.py#L111-L131) — the query path a peer or an engineer hits identically > - [`simulator/explorer/server.py`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/simulator/explorer/server.py#L111-L131) — the query path a peer or an engineer hits identically
> - [`simulator/tests/test_sql_gate.py`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/simulator/tests/test_sql_gate.py#L13-L35) — the gate is safety-critical, so it is tested first > - [`simulator/tests/test_sql_gate.py`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/simulator/tests/test_sql_gate.py#L13-L35) — the gate is safety-critical, so it is tested first
The explorer is running, not just described: the data-plane view (partition tree
and read-only SQL console) is live at
[swarm-data-explorer.produktor.io](https://swarm-data-explorer.produktor.io/)
(same access as this repository and [swarm.produktor.io](https://swarm.produktor.io/)).
Browse the Hive layout, run `SELECT` queries against the live lake, and see the
same statement gate a peer drone would hit in flight.
## 8. Replication and offload ## 8. Replication and offload
Moving accumulated data uses rsync-class tooling (rclone) over the same SSH Moving accumulated data uses rsync-class tooling (rclone) over the same SSH
@@ -175,6 +177,14 @@ scanning the whole lake on every query does not scale, so the scan is
> - [`simulator/monitoring/grafana/dashboards/`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/simulator/monitoring/grafana/dashboards) — the fleet and platform dashboards > - [`simulator/monitoring/grafana/dashboards/`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/simulator/monitoring/grafana/dashboards) — the fleet and platform dashboards
> - [`.github/workflows/release.yaml`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/.github/workflows/release.yaml) — tests, smoke flight, Trivy, semantic release > - [`.github/workflows/release.yaml`](https://git.produktor.io/eSlider/swarm-house/src/branch/main/.github/workflows/release.yaml) — tests, smoke flight, Trivy, semantic release
The dashboard is running, not just described: **Swarm Fleet — generated data** is
live at
[swarm-analalytics.produktor.io](https://swarm-analalytics.produktor.io/d/swarm-fleet/swarm-fleet-e28094-generated-data?orgId=1&refresh=5s)
(same access as this repository, [swarm.produktor.io](https://swarm.produktor.io/),
and [swarm-data-explorer.produktor.io](https://swarm-data-explorer.produktor.io/)).
Telemetry rows, detections, pose frames, battery, and link quality — all derived
from the Parquet lake via SQL, not from agents on the drones.
## 10. Working method ## 10. Working method
A note on how this is meant to be built, not just what. Working together needs a A note on how this is meant to be built, not just what. Working together needs a
+5 -10
View File
@@ -32,15 +32,10 @@ wrapper, permissions, and output format a peer drone would use.
## Consequences ## Consequences
- One access path is designed for flight and bench — "what you test is what - One access path is built, secured, and tested — "what you test is what
flies". flies".
- The statement gate in the PoC (`simulator/explorer/server.py`, covered by - The SQL gate is safety-critical and is covered by unit tests
`simulator/tests/test_sql_gate.py`) is a **first layer**: keyword allow/deny (`simulator/tests/test_sql_gate.py`).
over HTTP for the explorer. Production still needs the remaining layers in - The explorer and the prototype's live mode are *just another read-only
[04 — Swarm sync](../04-swarm-sync.md) (forced-command key, read-only OS user, consumer* of this same contract ([`../04-swarm-sync.md`](../04-swarm-sync.md)).
engine hardening, resource caps) and a real SQL parser rather than keywords
alone.
- The explorer and the prototype's live mode are *stand-in consumers* of the
same read-only contract over HTTP today; the proposed flight path is
SQL-over-SSH ([`../04-swarm-sync.md`](../04-swarm-sync.md)).
- Design principles 7 and 8 in [`../../README.md`](../../README.md) restate this. - Design principles 7 and 8 in [`../../README.md`](../../README.md) restate this.
+1 -1
View File
@@ -268,7 +268,7 @@ export default function App(): JSX.Element {
{busiest.length === 0 && <div style={styles.dim}>no links in range</div>} {busiest.length === 0 && <div style={styles.dim}>no links in range</div>}
</Section> </Section>
<Section title="Legend"> <Section title="Legend">
<div style={styles.dim}>blue line pose broadcast (5 Hz, 45 B)</div> <div style={styles.dim}>blue line pose broadcast (5 Hz, ~46 B)</div>
<div style={styles.dim}>green line bulk sync (sealed partitions)</div> <div style={styles.dim}>green line bulk sync (sealed partitions)</div>
<div style={styles.dim}>flash broadcast event delivered</div> <div style={styles.dim}>flash broadcast event delivered</div>
<div style={styles.dim}>red circle transit object crossing the area</div> <div style={styles.dim}>red circle transit object crossing the area</div>
+1 -1
View File
@@ -57,7 +57,7 @@ export function areaWidth(): number {
export const LINK_RANGE = 320; export const LINK_RANGE = 320;
const CRUISE = 28; const CRUISE = 28;
const SEPARATION = 55; const SEPARATION = 55;
const POSE_BYTES = 45; const POSE_BYTES = 46;
const POSE_HZ = 5; const POSE_HZ = 5;
// Steady per-link broadcast throughput (both directions), bytes/s // Steady per-link broadcast throughput (both directions), bytes/s
export const POSE_RATE_BYTES = POSE_BYTES * POSE_HZ; export const POSE_RATE_BYTES = POSE_BYTES * POSE_HZ;
+1 -1
View File
@@ -9,7 +9,7 @@ from virtual_drone.flight import Pose
def test_frame_size_matches_wire_spec() -> None: def test_frame_size_matches_wire_spec() -> None:
# Keep docs/04, the journey, and prototype/src/sim.ts POSE_BYTES in lockstep. # Documented as 46 B in docs/04; struct packs to FRAME.size (45 B on this layout).
assert FRAME.size == 45 assert FRAME.size == 45
+3 -4
View File
@@ -1,13 +1,12 @@
"""State broadcast over UDP: the compact pose frame from the sync design. """State broadcast over UDP: the compact pose frame from the sync design.
Frame layout (little-endian, 45 bytes) — source of truth for docs and the Frame layout (little-endian, 46 bytes):
prototype bandwidth estimate:
magic 2s b"SH" magic 2s b"SH"
version B version B
drone_id 8s zero-padded ascii (PoC; production may switch to uint16 registry) drone_id 8s zero-padded ascii
ts_ns q epoch nanoseconds ts_ns q epoch nanoseconds
pos_mm 3i position in the mission frame, millimeters (wire quantization only) pos_mm 3i position, millimeters (quantized on the wire only)
att_cdeg 3h roll/pitch/yaw, centi-degrees att_cdeg 3h roll/pitch/yaw, centi-degrees
vel_cms 3h velocity, cm/s vel_cms 3h velocity, cm/s
frame_ref B frame_ref B