Metadata-Version: 2.4
Name: ssl_link
Version: 0.1.0
Summary: SSL ground data plane: telemetry sources, protocol bridges, and telecommand for simulated and real vehicles
Author-email: SwarmSystemsLab <swarmsystemslab@gmail.com>
License: MIT
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: msgpack>=1.0
Requires-Dist: numpy>=1.24
Provides-Extra: all
Requires-Dist: h5py>=3.14.0; extra == 'all'
Requires-Dist: ivy-python>=3.3; extra == 'all'
Provides-Extra: dev
Requires-Dist: build>=1.2.2; extra == 'dev'
Requires-Dist: cibuildwheel>=2.23.3; extra == 'dev'
Requires-Dist: pre-commit>=3.0; extra == 'dev'
Requires-Dist: pytest-benchmark>=4.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.9.0; extra == 'dev'
Requires-Dist: tox-uv>=1.9; extra == 'dev'
Requires-Dist: tox>=4.18; extra == 'dev'
Requires-Dist: twine>=5.1.1; extra == 'dev'
Requires-Dist: ty>=0.0.14; extra == 'dev'
Provides-Extra: hdf5
Requires-Dist: h5py>=3.14.0; extra == 'hdf5'
Provides-Extra: lint
Requires-Dist: ruff>=0.9.0; extra == 'lint'
Provides-Extra: pprz
Requires-Dist: ivy-python>=3.3; extra == 'pprz'
Provides-Extra: pre-commit
Requires-Dist: pre-commit>=3.0; extra == 'pre-commit'
Provides-Extra: release
Requires-Dist: build>=1.2.2; extra == 'release'
Requires-Dist: cibuildwheel>=2.23.3; extra == 'release'
Requires-Dist: twine>=5.1.1; extra == 'release'
Provides-Extra: tests
Requires-Dist: pytest-benchmark>=4.0; extra == 'tests'
Requires-Dist: pytest-cov>=4.0; extra == 'tests'
Requires-Dist: pytest-xdist>=3.5; extra == 'tests'
Requires-Dist: pytest>=7.0; extra == 'tests'
Provides-Extra: type-checking
Requires-Dist: ty>=0.0.14; extra == 'type-checking'
Description-Content-Type: text/markdown

# ssl_link

**The SSL ground data plane** — one interface for getting robot data (from a log, a running
simulation, or a real vehicle over telemetry) and one interface for sending commands back.
Simulated and real vehicles look identical to everything downstream.

The *link* is the channel between your vehicles (simulated in
[`ssl_simulator`](../ssl_simulator), or flying over pprzlink/Ivy) and your ground applications
(the [`ssl_vista`](../ssl_simulator_vista) viewer, GCS tools, analysis notebooks) — telemetry
down, telecommand up. It owns the canonical **log format**, so every producer writes files that
every consumer reads.

---

Everything in `ssl_link` is built from **two contracts**. Learn these and the rest is adapters.

**1. A frame is `(time, {name: array})`.** A snapshot of the world: named, entity-indexed arrays
(`p` is `(N, 3)`, `R` is `(N, 3, 3)`, …) at one instant. This is exactly what
`ssl_simulator.Engine` hands to its `sink`, what goes on the wire, and what a telemetry parser
produces. It is the same vocabulary of *components* the simulator uses. So a plotter that reads
`"p"` doesn't care whether the number came from a solver an hour ago or a radio a millisecond ago.

**2. Data flows through a `DataSource`; commands flow through a `Commander`.**

```
                        ┌─────────────── ssl_link ───────────────┐
 producers              │  in:  DataSource   out: Commander      │   consumers
 ─────────              │  ┌──────────────┐  ┌────────────────┐  │   ─────────
 Engine(sink=…) ───────►│  │ StreamSource │  │ SimCommander   │◄─┼── vista / GCS panels
 udp datagrams ────────►│  │ UdpSource    │  │ IvyCommander   │  │   analysis notebooks
 pprzlink/Ivy ─────────►│  │ IvyFeed      │  │                │──┼──► real vehicles (Ivy)
 a .csv/.npz/.h5 log ──►│  │ LoggedSource │  └────────────────┘  │
                        │  └──────────────┘                      │
                        │  persistence: DataLogger / save_sim / load_sim (the log standard)
                        └────────────────────────────────────────┘
```

A `DataSource` is a plain `Mapping` (`source["p"]` gives `(T, N, 3)`) **plus** liveness
(`is_live`, `revision`) and frame access (`frame(idx)`, `time(idx)`, `history(...)`). A viewer
binds to a source and never knows which subclass it got. A `Commander` exposes the telecommand
surface (`setting`, `move_waypoint`, `jump_to_block`) — a GCS panel calls it and never knows
whether the target is simulated or flying.

---

## Installation

```bash
pip install ssl_link
```

The core (`DataSource`, the wire format, persistence for csv/npz) needs only **numpy + msgpack**.
Extras add capability:

| Extra | Adds | Needed for |
|---|---|---|
| `ssl_link[hdf5]` | `h5py` | reading/writing `.h5` log files |
| `ssl_link[pprz]` | `ivy-python` | the Ivy bridges & `IvyCommander` (real Paparazzi vehicles) |
| `ssl_link[all]` | both of the above | |

> **Note.** `ssl_link` does **not** depend on `ssl_simulator`. the data plane is
> producer-agnostic. The relationship is the other way round: `ssl_simulator` depends on
> `ssl_link` for the log format, so simulated and real runs share one on-disk standard. Install
> `ssl_simulator` separately when you want the simulator as a data producer.

---

## Tutorial

### 1. A source is a live, seekable view of frames

`StreamSource` is the base live buffer: push frames in, read them back like a finished run.

```python
import numpy as np
from ssl_link import StreamSource

source = StreamSource(capacity=1000)             # bounded ring: keeps the last 1000 frames
for k in range(5):
    source.push(k * 0.1, {"p": np.full((3, 2), float(k))})   # (time, frame)

source["p"].shape        # (5, 3, 2)  — stacked like a logged run
source.n_frames          # 5
source.frame(-1)["p"]    # the latest snapshot: (3, 2)
source.is_live           # True
```

It's bounded on purpose, a telemetry link runs for hours, so an unbounded buffer isn't an
option. It rejects a changing set of component names mid-stream, so ragged data can't creep in.

### 2. Live-stream a running simulation (needs `ssl_simulator`)

`StreamSource.push` has *exactly* the signature of `ssl_simulator.Engine`'s streaming sink — so
connecting a running sim to a live source is one argument, no glue:

```python
from ssl_simulator import Engine
# ... build a `world` (see ssl_simulator) ...

stream = StreamSource(capacity=2000)
Engine(time_step=0.01, log_time_step=0.1, sink=stream.push).run(world, duration=60.0)
# `stream` fills as the engine runs; hand it to a viewer to watch live.
```

### 3. Cross a process boundary with UDP

`udp_sink` is a sink that publishes each frame as one msgpack datagram; `UdpSource` receives them
on a background thread and pushes them into itself. Producer and consumer can be different
terminals or machines.

```python
# producer process:
from ssl_link.transports import udp_sink
Engine(sink=udp_sink(host="127.0.0.1", port=4700)).run(world, 60.0)

# consumer process (a viewer, a recorder, …):
from ssl_link.sources import UdpSource
with UdpSource(port=4700, capacity=2000).start() as source:
    ...  # `source` is a DataSource that fills as datagrams arrive
```

A UDP-streamed run is byte-for-byte the same as the logged replay of the same run — the wire
adds nothing but distance.

### 4. Persist to the canonical log format

The formats (`.csv` with a `# SETTINGS:` header, `.npz`, `.h5`) are the shared standard. Any
producer's output opens in any consumer:

```python
from ssl_link import save_sim, load_sim

save_sim("run.csv", {"time": t, "p": p, "R": R}, settings={"gain": 0.5})
data, settings = load_sim("run.csv")          # data["p"] -> (T, N, 3)

from ssl_link.sources import LoggedSource
source = LoggedSource.from_file("run.csv")     # replay a log through the same DataSource interface
```

`DataLogger` is the *streaming* writer the simulator's engine uses (writes a row per tick); reach
for `save_sim` when you already have the whole run in memory.

### 5. WIP: Send commands — the same code for sim and reality

A `Commander` is the telecommand surface. Point a GCS panel at one and swap the implementation
underneath:

```python
from ssl_link.command import SimCommander, IngestCommands

commander = SimCommander()                     # queues commands into a running world
world.add_system(IngestCommands(commander, {   # the world declares what each command does
    "setting": lambda w, ac_id, cmd: w["gain"].__setitem__(ac_id, cmd["value"]),
}))
# ... run the engine in a thread ...
commander.setting(ac_id=0, index=0, value=4.0) # mid-run: drone 0's gain becomes 4.0
```

Swap `SimCommander` for `IvyCommander` (needs `ssl_link[pprz]`) and the identical
`commander.setting(...)` call radios a real Paparazzi aircraft instead. That symmetry — one
command API, sim or reality — is the point of the layer.

### 6. WIP: Bridge a real Paparazzi swarm (needs `ssl_link[pprz]`)

`IvyTelemetrySink` publishes a simulation onto the Ivy bus as per-aircraft pprzlink telemetry, so
existing ground tools see a simulated swarm exactly as they see real drones. `IvyFeed` does the
reverse — assembles incoming telemetry back into component frames for a viewer. Both are driven by
one `mapping` table (`bridges.DEFAULT_TELEMETRY_MAP`) so the two directions can't drift apart.

> **Scope.** `IvyTelemetrySink` emulates the *telemetry surface*, not the firmware. Full PprzGCS
> visibility additionally requires `server.ml` with the aircraft in `conf.xml` (the ALIVE/CONFIG
> handshake). Lab tools that subscribe telemetry directly by `ac_id` need none of that.

---

## Package map

```
ssl_link/
  sources/       DataSource (the contract) · LoggedSource · StreamSource · UdpSource
  transports/    encode_frame/decode_frame (msgpack wire) · udp_sink (an Engine sink)
  persistence/   DataLogger · save_sim · load_sim   — the canonical log standard
  bridges/       IvyTelemetrySink · IvyFeed · the message↔component mapping table   [pprz]
  command/       Commander (protocol) · SimCommander/IngestCommands · IvyCommander
```

Top-level shortcuts: `from ssl_link import DataSource, LoggedSource, StreamSource, DataLogger,
load_sim, save_sim`.

---


## License

MIT
