Skip to content
Closed
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
30 changes: 30 additions & 0 deletions Dockerfile.standalone
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Builds the standalone (mock data) WASM module, generating the mock navigation data first. Unlike the sim build, this needs no MSFS SDK.
FROM rust:1.90

ARG WASI_SDK_VERSION=25

RUN apt-get update && \
apt-get install -y --no-install-recommends wget ca-certificates git && \
rm -rf /var/lib/apt/lists/*

# Install wasi-sdk, used to compile SQLite's C code for wasm32-wasip1
RUN wget -q https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz && \
tar -xzf wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz -C /opt && \
mv /opt/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux /opt/wasi-sdk && \
rm wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz

# The rusqlite fork takes the wasi sysroot from $MSFS_SDK/WASM/wasi-sysroot, so point that at the wasi-sdk sysroot
RUN mkdir -p /opt/msfs-sdk-shim/WASM && \
ln -s /opt/wasi-sdk/share/wasi-sysroot /opt/msfs-sdk-shim/WASM/wasi-sysroot

ENV MSFS_SDK=/opt/msfs-sdk-shim \
CC_wasm32_wasip1=/opt/wasi-sdk/bin/clang \
AR_wasm32_wasip1=/opt/wasi-sdk/bin/llvm-ar

RUN rustup target add wasm32-wasip1

# Bun runs the mock navigation data generator (scripts/generate-mock-navdata.ts) before each build
COPY --from=oven/bun:1 /usr/local/bin/bun /usr/local/bin/bun

# Needed when running in CI/CD to avoid dubious ownership errors
RUN git config --global --add safe.directory /workspace
124 changes: 124 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@
- `example/`
- `aircraft/` includes a base aircraft to test in the sim
- `gauge/` includes a very simple TypeScript instrument to communicate with the WASM module
- `standalone-demo/` runs the [standalone module](#running-outside-the-sim-standalone-mode) outside the sim, as a script and as a NestJS HTTP API with Swagger UI
- `scripts/` includes the build scripts, including the [standalone build](#running-outside-the-sim-standalone-mode) and its mock data generator
- `src/`
- `ts` includes source code for the JS interface for interfacing with the WASM module
- `transport/` includes the [transports](#using-the-js-interface-with-a-transport) used to reach the WASM module (CommBus in the sim, or the standalone module)
- `wasm` includes the Rust source code for the WASM module which handles the downloading of the database file, and interfacing with the database
- `platform/` includes the [platform adapters](#platforms) (MSFS or standalone), selected at compile time

## Including in Your Aircraft

Expand Down Expand Up @@ -102,6 +106,126 @@ The default location for navigation data is `work/NavigationData`.
1. Change directory to [`example/gauge`](example/gauge/) using `cd example/gauge`
2. Run `bun run build` to build into the `PackageSources` folder of the aircraft sample (or `bun run dev` to build into the `Packages` folder of the aircraft and listen to changes in the source).

## Running Outside the Sim (Standalone Mode)

The WASM module can also be built as a **standalone** module, which runs outside the simulator (in a browser, or in Bun/Node) and serves mock navigation data. This lets you develop and test instruments and tools against the real interface, without starting MSFS.

The standalone module runs the same Rust core as the sim build: every function (including `ExecuteSQLQuery`) goes through the same dispatch and the same SQL queries. Only the platform-specific parts are swapped out, and the navigation data is a mock database embedded in the module.

### Platforms

The Rust code is split into a shared core and a platform adapter, selected at compile time with a Cargo feature:

```
Rust core
(database, SQL queries, function dispatch, events)
│
trait Platform
/ \
MsfsPlatform StandalonePlatform
CommBus host import (navigraph.send_message)
work folder, embedded mock database
bundled data, (in-memory SQLite)
network download simulated download
```

| Feature | Build command | Used for |
| ---------------- | ------------------------------- | --------------------------------------- |
| `msfs` (default) | `bun run build:wasm` | The gauge in MSFS 2020 & 2024 |
| `standalone` | `bun run build:wasm:standalone` | Running outside the sim, with mock data |

The two features are mutually exclusive. The platform trait lives in [`src/wasm/src/platform`](src/wasm/src/platform/mod.rs), and the core only talks to the platform through it.

| Behaviour | `msfs` | `standalone` |
| -------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------ |
| Messages to JS | CommBus | `navigraph.send_message`, imported from the host |
| Navigation data | `work/NavigationData` (bundled or downloaded) | Mock database, embedded in the module |
| `DownloadNavigationData` | Downloads and installs the data from the URL | Sends `DownloadProgress` events, then reloads the mock data (no network) |
| `GetNavigationDataInstallStatus` | Installed cycle, and latest cycle from the Navigraph API | Cycle of the mock data |

### Building the Standalone Module

1. Make sure Docker is running.
2. Run `bun run build:wasm:standalone` at the root of the repository.
- The first run builds the `navigation-data-interface-standalone-build` image from [`Dockerfile.standalone`](Dockerfile.standalone). It does not need the MSFS SDK.
- The mock navigation data is generated inside the container, right before the build (see [Mock Navigation Data](#mock-navigation-data)).
3. The module is written to `dist/standalone/msfs_navigation_data_interface.wasm`.

> [!IMPORTANT]
> The standalone module must be built through `bun run build:wasm:standalone`. Running `cargo build --no-default-features --features standalone` directly fails, as the mock navigation data only exists in the build container.

### Mock Navigation Data

The mock navigation data is generated by [`scripts/generate-mock-navdata.ts`](scripts/generate-mock-navdata.ts), inside the standalone build container. It is never committed, and is written to `targets/standalone/mock-data`.

It is a subset of the [example bundled database](example/aircraft/PackageSources/Navigraph/BundledData/), so it has exactly the same schema as real navigation data:

- The selected airports, with all their procedures, runways, gates, communications, etc. By default these are `MMUN`, `MMMD` and `MSLP`.
- All other airports (with their runways) within about 2° of the selected airports, so range queries return realistic results.
- Enroute waypoints, navaids, holdings and communications in the same area, plus the airways, airspaces and FIRs passing through it.

To generate the mock data for other airports, pass them to the build (they must exist in the example database):

```sh
bun run build:wasm:standalone MMMX MMGL
```

### Using the JS Interface with a Transport

The JS interface talks to the WASM module through a transport, found in [`src/ts/transport`](src/ts/transport):

- `CommBusTransport`: talks to the gauge in the sim over the CommBus.
- `StandaloneTransport`: loads the standalone module, drives it (the equivalent of sim frames), and passes calls to it. It works in browsers and in Bun/Node.

By default, the interface uses the CommBus when it is available. To run outside the sim, pass the standalone module:

```ts
import { NavigraphNavigationDataInterface, TransportMode } from "@navigraph/msfs-navigation-data-interface";

// In the sim (default): uses the CommBus
const simInterface = new NavigraphNavigationDataInterface();

// Outside the sim: runs the standalone module, serving mock data
const navigationDataInterface = new NavigraphNavigationDataInterface({
mode: TransportMode.Standalone,
standalone: {
// A URL to fetch, the module bytes, a compiled WebAssembly.Module, or a fetch Response
wasm: "/msfs_navigation_data_interface.wasm",
},
});

navigationDataInterface.onReady(async () => {
const airport = await navigationDataInterface.get_airport("MMUN");
});
```

For runnable examples, see [`example/standalone-demo`](example/standalone-demo): a script (`bun run demo:standalone`), and a NestJS HTTP API with Swagger UI serving the mock data (`bun run demo:standalone:serve`).

The available modes are:

- `TransportMode.Auto` (default): uses the CommBus when `RegisterCommBusListener` is available. Otherwise, uses the standalone module if `standalone` options are passed, and throws if they are not.
- `TransportMode.CommBus`: always uses the CommBus.
- `TransportMode.Standalone`: always uses the standalone module (requires `standalone` options).

You can also pass your own transport, by implementing the `NavigationDataTransport` interface (`call` and `onEvent`):

```ts
const navigationDataInterface = new NavigraphNavigationDataInterface(myTransport);
```

### Hosting the Standalone Module Yourself

If you are not using the JS interface, the standalone module can be driven directly. It uses the same payloads and channels as the [CommBus events](#interfacing-with-the-gauge-manually):

- Exports:
- `navigraph_alloc(len) -> ptr` allocates a buffer in the module memory.
- `navigraph_call_function(ptr, len)` queues a function call. The buffer holds a UTF-8 `NAVIGRAPH_CallFunction` payload, and ownership passes to the module.
- `navigraph_dealloc(ptr, len)` frees a buffer which was not passed to `navigraph_call_function`.
- `navigraph_update()` runs queued functions and sends the heartbeat. Call it regularly, like a sim frame (e.g. every 16ms).
- Imports:
- `navigraph.send_message(channel_ptr, channel_len, data_ptr, data_len)` receives `NAVIGRAPH_FunctionResult` and `NAVIGRAPH_Event` messages (UTF-8). Copy the data before returning, and avoid calling back into the module from it.
- `wasi_snapshot_preview1`, as the module targets `wasm32-wasip1`. Only stdout/stderr, clocks and randomness are needed; [`StandaloneTransport`](src/ts/transport/StandaloneTransport.ts) contains a minimal implementation.

## Interfacing with the gauge manually

The navigation data interface acts as its own WASM gauge in sim, so in order to communicate with it, you must use the [CommBus](https://docs.flightsimulator.com/html/Programming_Tools/WASM/Communication_API/Communication_API.htm).
Expand Down
Loading