Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,28 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v6

- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm

- name: Build frontend plugin
run: |
pnpm install --frozen-lockfile
pnpm check
pnpm build

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Install dependencies
run: uv sync --locked
- name: Install Python dependencies
run: uv sync --locked --all-packages

- name: Run unit and integration tests
run: uv run pytest
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# Automatically generated directories and files
output
cache

# tangram frontend plugin builds (example_plugins/tangram/*)
node_modules/
dist-frontend/
bluesky/resources/grib
bluesky/resources/netcdf

Expand Down
12 changes: 9 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ uv run pytest -m api tests/test_api.py # REST API tests — spawn a separate

uv run ruff check . # lint
uv run ruff format . # format (line-length 100)
uv run pyright # type check (standard mode)
uv run pyright # type check (standard mode; covers example_plugins EXCEPT example_plugins/tangram)

# tangram frontend plugin — its own self-contained uv + pnpm workspace (separate .venv);
# all commands run from example_plugins/tangram/ (see its justfile)
cd example_plugins/tangram && just check # ruff + pyright + pnpm (eslint + vue-tsc + tsc)
cd example_plugins/tangram && just fmt # ruff --fix/format + pnpm lint:fix
cd example_plugins/tangram && pnpm build # bundle each plugin into dist-frontend/

uv run minisky commands docs # regenerate docs/reference/commands.md after changing commands
uv run minisky docs serve # docs live preview
Expand All @@ -41,7 +47,7 @@ The FastAPI app lives in `minisky/server.py`; `minisky server` is the CLI entry

Full details in `docs/architecture.md` — read it before making structural changes. The essentials:

**Singletons.** `minisky.init()` constructs module-level singletons everything else references: `sim` (clock/state machine), `traf` (all aircraft state + flight-dynamics update), `runner` (async loop stepping at a controllable rate), `scr` (`ConsoleIO` output buffer), `navdb` (waypoints/airports/airways from parquet). They are `None` until `init()` runs. Call `load_plugins()` after `init()` to activate plugins from `settings.yml`.
**Singletons.** `minisky.init()` constructs module-level singletons everything else references: `sim` (clock/state machine), `traf` (all aircraft state + flight-dynamics update), `runner` (async loop stepping at a controllable rate), `scr` (`ConsoleIO` output buffer), `navdb` (waypoints/airports/airways from parquet). They are `None` until `init()` runs. Call `load_plugins()` after `init()` to activate plugins from `settings.toml`.

**Import order in `minisky/__init__.py` is load-bearing.** `traffic` is imported last and separately because the performance model runs module-level code touching `minisky.data` (set up by the settings import). Reordering causes a circular import.

Expand Down Expand Up @@ -75,4 +81,4 @@ Every text command — scenario file, REST `stack/` endpoint, or console — goe

- Package/dependency management is **uv**. Prefix Python invocations with `uv run`.
- After adding or changing a stack command, regenerate `docs/reference/commands.md` with the gen script.
- `settings.yml` holds runtime config (ASAS protected-zone margins, plugin path, `enabled_plugins`).
- `settings.toml` holds runtime config (ASAS protected-zone margins, plugin path, `enabled_plugins`).
13 changes: 13 additions & 0 deletions docs/api/tangram-minisky.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# `tangram_minisky`

The tangram-side frontend plugin that renders live MiniSky traffic on a
[tangram](https://github.com/open-aviation/tangram) map. It is a separately
packaged member of MiniSky's root
[uv workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/),
deliberately frontend-only: all
simulator-side logic lives in MiniSky's `TANGRAM` plugin. See the
[tangram streaming guide](../guides/tangram.md) for the full setup.

## Plugin and configuration

::: tangram_minisky
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ of the code refers to:
import minisky

minisky.init() # create the singletons
minisky.load_plugins() # optional: load plugins enabled in settings.yml
minisky.load_plugins() # optional: load plugins enabled in settings.toml
```

## The simulation loop
Expand Down Expand Up @@ -104,7 +104,7 @@ subsystems that act on it each timestep:
LNAV/VNAV logic following a [`Route`][minisky.traffic.route.Route] of waypoints.
- **Conflict detection** (`traffic/asas/detection.py`) — pairwise state-based detection
within a lookahead time against a protected zone (default 5 NM / 1000 ft, configurable
in `settings.yml`). Candidate pairs are pre-selected with a KD-tree on projected
in `settings.toml`). Candidate pairs are pre-selected with a KD-tree on projected
positions plus a vertical reachability filter, so cost scales with local traffic
density rather than N².
- **Conflict resolution** (`traffic/asas/mvp.py`) — Modified Voltage Potential resolution
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ and stepping the simulation yourself.

## Configuration

Runtime settings live in `settings.yml` at the repository root, e.g. conflict-detection
Runtime settings live in `settings.toml` at the repository root, e.g. conflict-detection
lookahead time and protected-zone sizes, the plugin search directory, and which plugins to
load at startup:

Expand Down
12 changes: 6 additions & 6 deletions docs/guides/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The `example_plugins/` directory contains working examples.

## Anatomy of a plugin

A plugin is a Python file in the plugin directory (`plugin_path` in `settings.yml`,
A plugin is a Python file in the plugin directory (`plugin_path` in `settings.toml`,
default `example_plugins`) that defines an `init_plugin()` function:

```python
Expand All @@ -26,7 +26,7 @@ def init_plugin():
example = Example()

return {
"plugin_name": "EXAMPLE", # name used by PLUGIN LOAD / settings.yml
"plugin_name": "EXAMPLE", # name used by PLUGIN LOAD / settings.toml
"update_interval": 5, # seconds of sim time between update calls
"update": example.update, # called every update_interval
# "preupdate": ..., # called before traf.update()
Expand Down Expand Up @@ -106,11 +106,11 @@ the plugin is loaded), so a broken plugin can't crash the simulator at startup.

Load plugins in any of three ways:

- **At startup** — list them in `settings.yml`:
- **At startup** — list them in `settings.toml`:

```yaml
plugin_path: example_plugins
enabled_plugins: ['EXAMPLE']
```toml
plugin_path = "example_plugins"
enabled_plugins = ["EXAMPLE"]
```

(Requires the host program to call [`minisky.load_plugins()`][minisky.load_plugins]
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/running-scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ uv run minisky run --scenario scenarios/kl204.scn --speed 10
| `--speed` | `1` | Simulation speed multiplier relative to wall time |

The script initialises the simulator with the scenario, loads any plugins enabled in
`settings.yml`, and runs the [`Runner`][minisky.simulation.runner.Runner] loop until the
`settings.toml`, and runs the [`Runner`][minisky.simulation.runner.Runner] loop until the
scenario ends the simulation.

## Scenario files
Expand Down
240 changes: 240 additions & 0 deletions docs/guides/tangram.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# Streaming to a tangram map

MiniSky can act as an *external simulator* for
[tangram](https://github.com/open-aviation/tangram), the open aviation data
visualisation framework: live simulated traffic appears on tangram's map, and
the simulator can be controlled (run/hold/reset, speed, arbitrary stack
commands) from a tangram sidebar widget.

Nothing is added to the tangram source tree. Tangram discovers plugins
through Python entry points, so its side of the integration is a package you
`pip install` into whatever environment runs `tangram serve`, plus one line
of configuration. Both halves of the integration live in this repository:

```
minisky process (TANGRAM plugin) tangram process
publishes to:minisky:new-data ─▶ Redis ─▶ Channel service ─▶ browser
publishes to:minisky:console ─▶ (WebSocket) │
listens on from:minisky:command ◀─ Redis ◀────────────────────── ┘
```

- **`example_plugins/tangram.py`** (the `TANGRAM` MiniSky plugin) owns all the
logic: it converts each simulation snapshot to aviation units, publishes it
to Redis, relays console output, and executes stack commands pushed from the
browser. MiniSky talks to tangram *only* through Redis pub/sub — tangram's
transport convention (`to:<topic>:<event>` / `from:<topic>:<event>`) that
has stayed stable across its plugin API changes.
- **`example_plugins/tangram/tangram_minisky/`** is a separately packaged, thin
tangram frontend plugin: it registers a `minisky_aircraft` entity type, a
deck.gl layer, trail rendering via tangram's shared trajectory store, and
the control widget. No business logic lives there, so it is cheap to rewrite
when tangram's frontend plugin API changes.

## Step-by-step setup

The steps below assume Redis in a container, MiniSky and tangram on the host
— the common development setup. (Tangram's own container image only bundles
its in-tree plugins, so a dockerised `tangram serve` cannot load
`tangram_minisky` without image changes; run it on the host instead.)

### 1. Redis

Any Redis 5+ reachable by both processes. With docker/podman, publish the
port to the host:

```bash
docker run -d --rm -p 6379:6379 --name redis redis:8-alpine
```

If you already run Redis for a tangram deployment, reuse it — the two sides
meet on the channel name, not on any shared configuration.

### 2. MiniSky side (the producer)

```bash
just sync
```

<!-- TODO(abraham): when we work on PR#24, this is no longer needed for end users because the wheel should have already contained the pre-built frontend. it only applies for *developers*. -->

In `settings.toml`:

```toml
enabled_plugins = ["TANGRAM"]

# [tangram] is optional; uncomment to override the defaults shown here.
# [tangram]
# redis_url = "redis://127.0.0.1:6379"
# channel = "minisky"
# max_hz = 5
```

Start MiniSky (any front — the bridge works the same in all of them):

```bash
uv run minisky server
# or: uv run minisky run --scenario scenarios/kl204.scn
```

Startup should print
`Tangram bridge publishing to to:minisky:* at redis://127.0.0.1:6379`.

**Verify the transport now, before touching any frontend:**

```bash
# watch everything MiniSky publishes (prefix with `docker exec -it <container>`
# if redis-cli is not installed on the host)
redis-cli psubscribe "to:*"

# drive the simulator from outside
redis-cli publish "from:minisky:command" '{"command": "CRE KL204 B744 52 4 90 FL300 250"}'
redis-cli publish "from:minisky:command" '{"command": "HOLD"}'
redis-cli publish "from:minisky:command" '{"command": "OP"}'
```

Expect `to:minisky:new-data` snapshots (~`[tangram].max_hz`/s while running,
1/s heartbeat otherwise) reacting to the commands, plus `to:minisky:console`
lines. If this works, the simulator side is done; everything after this point
is tangram-side only.

### 3. Run tangram

There are two options:

#### uv tool

```bash
uv tool install tangram_core \
--with ./example_plugins/tangram/tangram_minisky \
--force
tangram serve --config /path/to/tangram.toml
```

Rerun the install after rebuilding the frontend so the tool receives the new
`dist-frontend` bundle.

#### execution project (recommended)

Expected layout:

```text
./
├── minisky/
├── tangram/
└── tangram_minisky_exe/
```

`tangram_minisky_exe/pyproject.toml` pins the published core and keeps only the
plugin editable:

```toml
[project]
name = "tangram-minisky-exe"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["tangram-core", "tangram-minisky"]

[tool.uv.sources]
tangram-minisky = { path = "../minisky/example_plugins/tangram/tangram_minisky", editable = true }
```

Run it with:

```bash
cd ../tangram_minisky_exe
uv sync
uv run tangram serve --config tangram.toml
```

Open <http://localhost:2346>. Note that this runs on the published tangram package.

## Local tangram checkout

To test an unpublished local checkout instead, make these temporary development-only changes.

In `tangram_minisky_exe/pyproject.toml`, unpin core and add its editable source:

```toml
[project]
dependencies = ["tangram-core", "tangram-minisky"]

[tool.uv.sources]
tangram-core = { path = "../tangram/packages/tangram_core", editable = true }
tangram-minisky = { path = "../minisky/example_plugins/tangram/tangram_minisky", editable = true }
```

In MiniSky's root `pnpm-workspace.yaml`, temporarily add:

```yaml
overrides:
"@open-aviation/tangram-core": "link:../tangram/packages/tangram_core"
```

Then refresh both environments:

```bash
pnpm install
pnpm build
cd ../tangram_minisky_exe
uv sync
```

Do not commit the temporary overrides or their lockfile changes. Comment them and
run `pnpm install` again.

## Troubleshooting

Work upstream-to-downstream:

1. **No `to:minisky:new-data` in `redis-cli psubscribe "to:*"`** — MiniSky
side. Check the plugin loaded at startup, and run the `TANGRAM` stack
command in the MiniSky console: it reports connection state, message count
and the last Redis error. The bridge heartbeats once per second even when
the simulation is idle, so *silence means it is not connected*.
2. **Snapshots flow, but tangram logs `fail to decode JWT` /
`InvalidSignature` on joins** — the browser holds tokens signed under a
different `jwt_secret` than the running channel service. Almost always a
stale tab auto-reconnecting after a restart with a changed secret:
hard-reload the page. Also check nothing else (an old `tangram serve`, a
tangram container) is squatting ports 2346/2347: `lsof -i :2346 -i :2347`.
3. **Channel joins succeed but the widget says "Simulator offline"** — no
snapshot or heartbeat arrived for 5 seconds. Almost always a Redis URL
mismatch: `tangram.toml`'s `redis_url` and `settings.toml`'s
`[tangram].redis_url` must point at the *same* Redis instance (mind
host-vs-container addressing: a dockerised tangram reaches a compose
Redis at `redis://redis:6379`, a host process at `redis://127.0.0.1:6379`).
A channel-name mismatch between the two sides has the same symptom.

## Wire contract

All payloads are JSON. Aircraft fields use aviation units (altitude in ft,
speeds in kt, vertical rate in fpm) under jet1090-style names, so tangram-side
consumers see familiar shapes; the conversion from MiniSky's internal SI state
happens in the MiniSky plugin, keeping `minisky.streaming` consumer-agnostic.

- `to:<channel>:new-data`:
`{"aircraft": [{id, callsign, typecode, latitude, longitude, altitude,
groundspeed, tas, ias, vertical_rate, track, inconf, timestamp}],
"count": n, "siminfo": {simt, simdt, simutc, speed, ntraf, state,
state_name, scenname, nconf_cur, nlos_cur}}`.
Published on every simulation step (wall-clock capped at `[tangram].max_hz`).
Whenever the simulation is not advancing — including a freshly started
simulator with no scenario — a heartbeat with refreshed `siminfo` (and the
last aircraft list) is republished every second, so the frontend always
sees the simulator and its state changes.
- `to:<channel>:console`: `{"lines": [...]}` — everything echoed to the
MiniSky console (the bridge tees the console, it does not consume it).
- `from:<channel>:command`: `{"command": "..."}` — one stack command,
executed on the next simulation step (works while paused, so `OP` can
un-pause). Bare strings are also accepted for redis-cli convenience.

## Known limitations

- tangram's playback timeline (`api.time`) is client-side only; scrubbing or
pausing it does not drive the simulator clock. Use the control widget (or
stack commands) instead.
- Console output relayed to tangram is a tee of everything echoed by the
simulator, not a per-command response stream.
- Commands published before the bridge's Redis subscription is live are lost
(pub/sub has no replay); the bridge logs its status via the `TANGRAM`
stack command.
Loading
Loading