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
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
ts-bindings:
name: TypeScript bindings up to date
runs-on: ubuntu-latest
defaults:
run:
working-directory: software/pc
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: software/pc
- uses: taiki-e/install-action@just
- run: just check-types
3 changes: 3 additions & 0 deletions software/pc/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[env]
# Output directory for ts-rs generated TypeScript bindings
TS_RS_EXPORT_DIR = { value = "frontend/src/lib", relative = true }
32 changes: 32 additions & 0 deletions software/pc/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions software/pc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,8 @@ heapless = "0.9.2"
# SHA-256 for OTA firmware verification
sha2 = "0.10"

# Generate TypeScript bindings for WebSocket types (see `just gen-types`)
ts-rs = "12"

[dev-dependencies]
tokio = { version = "1", features = ["full", "test-util"] }
3 changes: 3 additions & 0 deletions software/pc/frontend/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ static/**
pnpm-lock.yaml
package-lock.json
yarn.lock

# Generated by ts-rs (just gen-types)
src/lib/bindings.ts
2 changes: 1 addition & 1 deletion software/pc/frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ export default ts.config(
}
},
{
ignores: ['.svelte-kit/**', 'build/**', 'node_modules/**']
ignores: ['.svelte-kit/**', 'build/**', 'node_modules/**', 'src/lib/bindings.ts']
}
);
2 changes: 2 additions & 0 deletions software/pc/frontend/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: true
68 changes: 68 additions & 0 deletions software/pc/frontend/src/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* Theme tokens. Dark is the default; light follows the OS preference. */
:root {
color-scheme: dark;

--bg: #111;
--surface: #1a1a1a;
--surface-2: #222;
--surface-hover: #333;
--border: #444;
--border-strong: #555;
--border-focus: #777;

--text: #eee;
--text-muted: #aaa;
--text-dim: #888;

--plot-grid: #2a2a2a;
--plot-axis: #333;

--danger: #e53935;
--danger-banner: #d32f2f;
--danger-hover: #2a1010;
--warning: #e65100;
--success: #4caf50;
--jog: #ffa726;

--trace-0: #fff;
--trace-1: #4fc3f7;
--trace-2: #ff8a65;
--trace-3: #81c784;
--trace-4: #ce93d8;
--trace-5: #fff176;
}

@media (prefers-color-scheme: light) {
:root {
color-scheme: light;

--bg: #f4f4f5;
--surface: #fff;
--surface-2: #fafafa;
--surface-hover: #ebebeb;
--border: #d4d4d8;
--border-strong: #bdbdc2;
--border-focus: #8e8e93;

--text: #1c1c1e;
--text-muted: #52525b;
--text-dim: #8e8e93;

--plot-grid: #e4e4e7;
--plot-axis: #c8c8cc;

--danger: #d32f2f;
--danger-banner: #d32f2f;
--danger-hover: #fdecea;
--warning: #e65100;
--success: #2e7d32;
--jog: #ef6c00;

--trace-0: #212121;
--trace-1: #0288d1;
--trace-2: #e64a19;
--trace-3: #2e7d32;
--trace-4: #7b1fa2;
--trace-5: #f9a825;
}
}
92 changes: 92 additions & 0 deletions software/pc/frontend/src/lib/bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* A single measurement data point.
*/
export type DataPoint = {
/**
* Turntable angle in degrees.
*/
angle_deg: number,
/**
* Averaged RSSI in dBm (negative; closer to 0 = stronger).
*/
rssi_dbm: number,
/**
* Number of raw RSSI samples averaged into this reading.
*/
sample_count: number, };

/**
* Serial port information returned by the list_ports endpoint.
*/
export type PortInfo = {
/**
* OS device path (e.g. `/dev/ttyUSB0` or `COM3`).
*/
name: string,
/**
* Human-readable description from the USB descriptor.
*/
description: string, };

/**
* Sweep configuration sent from UI to backend.
*/
export type SweepConfig = { start_deg: number, stop_deg: number, step_deg: number, samples_per_angle: number, };

/**
* Current system status broadcast to all WebSocket clients on state changes.
*/
export type SystemStatus = {
/**
* Whether an antenna pattern sweep is in progress.
*/
sweeping: boolean,
/**
* Whether the TX ESP32 is paired via ESP-NOW.
*/
tx_connected: boolean,
/**
* Whether the RX ESP32 is paired via ESP-NOW.
*/
rx_connected: boolean,
/**
* Whether the turntable ESP32 is paired via ESP-NOW.
*/
turntable_connected: boolean,
/**
* Whether the PC is connected to the Bridge over serial/TCP.
*/
serial_connected: boolean,
/**
* Number of data points stored on the backend.
*/
data_points: number,
/**
* Active RF measurement backend name (e.g. "espnow", "dect-nr+").
*/
backend_name: string,
/**
* Carrier frequency in MHz, if applicable.
*/
backend_freq_mhz: number | null,
/**
* Motor-to-turntable gear ratio (motor_deg = turntable_deg × ratio).
*/
transmission_ratio: number, };

/**
* TX configuration.
*/
export type TxConfig = { channel: number, tx_power_dbm: number, packet_rate_hz: number, };

/**
* Commands received from the frontend over WebSocket.
*/
export type WsCommand = { "type": "StartSweep" } & SweepConfig | { "type": "ConfigureTx" } & TxConfig | { "type": "Stop" } | { "type": "QueryStatus" } | { "type": "ListPorts" } | { "type": "Connect", port: string, } | { "type": "Disconnect" } | { "type": "ExportCsv" } | { "type": "ReturnHome" } | { "type": "Jog", delta_deg: number, } | { "type": "OtaUpload", target: string, firmware_path: string, } | { "type": "SetTransmissionRatio", ratio: number, };

/**
* Events pushed to the frontend over WebSocket.
*/
export type WsEvent = { "type": "DataPoint" } & DataPoint | { "type": "SweepComplete" } | { "type": "HomeComplete" } | { "type": "JogComplete", angle_deg: number, } | { "type": "Status" } & SystemStatus | { "type": "OtaProgress", chunks_sent: number, total_chunks: number, } | { "type": "OtaFinished" } | { "type": "Error", message: string, } | { "type": "Log", message: string, };
109 changes: 9 additions & 100 deletions software/pc/frontend/src/lib/ws.ts
Original file line number Diff line number Diff line change
@@ -1,105 +1,14 @@
/** WebSocket client for communicating with the Axum backend. */

/** A single RSSI measurement at a given turntable angle. */
export type DataPoint = {
/** Turntable angle in degrees. */
angle_deg: number;
/** Averaged RSSI reading in dBm (negative values; closer to 0 = stronger). */
rssi_dbm: number;
/** Number of raw RSSI samples averaged into this reading. */
sample_count: number;
};

/** Snapshot of the system's connection and sweep state, sent periodically by the backend. */
export type SystemStatus = {
/** Whether a sweep is currently in progress. */
sweeping: boolean;
/** Whether the TX (transmitter) ESP32 is paired via ESP-NOW. */
tx_connected: boolean;
/** Whether the RX (receiver) ESP32 is paired via ESP-NOW. */
rx_connected: boolean;
/** Whether the turntable ESP32 is paired via ESP-NOW. */
turntable_connected: boolean;
/** Whether the PC is connected to the Bridge over serial. */
serial_connected: boolean;
/** Total data points collected in the current session. */
data_points: number;
/** Active RF measurement backend name (e.g. "espnow", "dect-nr+"). */
backend_name: string;
/** Carrier frequency in MHz, if applicable. */
backend_freq_mhz: number | null;
/** Motor-to-turntable gear ratio (motor_deg = turntable_deg × ratio). */
transmission_ratio: number;
};

/** A serial port available on the host machine. */
export type PortInfo = {
/** OS device path (e.g. `/dev/ttyUSB0` or `COM3`). */
name: string;
/** Human-readable description from the USB descriptor. */
description: string;
};

/**
* Server-to-client WebSocket events.
*
* The backend pushes these as JSON over the `/ws` endpoint.
*/
export type WsEvent =
| {
type: 'DataPoint';
angle_deg: number;
rssi_dbm: number;
sample_count: number;
}
| { type: 'SweepComplete' }
| { type: 'HomeComplete' }
| { type: 'JogComplete'; angle_deg: number }
| {
type: 'Status';
sweeping: boolean;
tx_connected: boolean;
rx_connected: boolean;
turntable_connected: boolean;
serial_connected: boolean;
data_points: number;
backend_name: string;
backend_freq_mhz: number | null;
transmission_ratio: number;
}
| { type: 'OtaProgress'; chunks_sent: number; total_chunks: number }
| { type: 'OtaFinished' }
| { type: 'Error'; message: string }
| { type: 'Log'; message: string };

/**
* Client-to-server WebSocket commands.
*
* Sent as JSON to the `/ws` endpoint to control the measurement system.
*/
export type WsCommand =
| {
type: 'StartSweep';
start_deg: number;
stop_deg: number;
step_deg: number;
samples_per_angle: number;
}
| {
type: 'ConfigureTx';
channel: number;
tx_power_dbm: number;
packet_rate_hz: number;
}
| { type: 'Stop' }
| { type: 'QueryStatus' }
| { type: 'ListPorts' }
| { type: 'Connect'; port: string }
| { type: 'Disconnect' }
| { type: 'ExportCsv' }
| { type: 'ReturnHome' }
| { type: 'Jog'; delta_deg: number }
| { type: 'SetTransmissionRatio'; ratio: number };
// Types are generated from the Rust definitions in src/lib.rs (`just gen-types`).
import type { WsCommand, WsEvent } from './bindings';
export type {
DataPoint,
PortInfo,
SystemStatus,
WsCommand,
WsEvent
} from './bindings';

export function createWsConnection(
onEvent: (event: WsEvent) => void,
Expand Down
1 change: 1 addition & 0 deletions software/pc/frontend/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';

let { children } = $props();
Expand Down
Loading
Loading