feat: generate typed frontend API contracts
This commit is contained in:
@@ -29,3 +29,4 @@ hex = "0.4"
|
||||
pickledb = "0.5"
|
||||
dirs = "6"
|
||||
uuid = { workspace = true }
|
||||
ts-rs = "12.0.1"
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
export class ApiError extends Error {
|
||||
constructor(status, message) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
function isRecord(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
export function isRigSnapshot(value) {
|
||||
if (!isRecord(value) || !isRecord(value.info) || !isRecord(value.status)) {
|
||||
return false;
|
||||
}
|
||||
const { info, status } = value;
|
||||
return typeof value.initialized === "boolean" && typeof info.manufacturer === "string" && typeof info.model === "string" && isRecord(status.freq) && typeof status.freq.hz === "number" && (typeof status.mode === "string" || isRecord(status.mode)) && typeof status.tx_en === "boolean";
|
||||
}
|
||||
export function isRigListResponse(value) {
|
||||
return isRecord(value) && (value.active_remote === null || typeof value.active_remote === "string") && Array.isArray(value.rigs) && value.rigs.every(
|
||||
(rig) => isRecord(rig) && typeof rig.remote === "string" && typeof rig.manufacturer === "string" && typeof rig.model === "string" && Array.isArray(rig.supported_modes) && typeof rig.tx === "boolean" && typeof rig.filter_controls === "boolean" && typeof rig.initialized === "boolean"
|
||||
);
|
||||
}
|
||||
export function isDecoderRegistry(value) {
|
||||
return Array.isArray(value) && value.every(
|
||||
(decoder) => isRecord(decoder) && typeof decoder.id === "string" && typeof decoder.label === "string" && (decoder.activation === "mode_bound" || decoder.activation === "toggle") && Array.isArray(decoder.active_modes) && decoder.active_modes.every((mode) => typeof mode === "string") && typeof decoder.background_decode === "boolean" && typeof decoder.bookmark_selectable === "boolean"
|
||||
);
|
||||
}
|
||||
export class TrxApi {
|
||||
constructor(baseUrl = "") {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
async get(path, validate) {
|
||||
return this.request(path, { cache: "no-store" }, validate);
|
||||
}
|
||||
async post(path, body, validate) {
|
||||
return this.request(
|
||||
path,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
},
|
||||
validate
|
||||
);
|
||||
}
|
||||
async request(path, init, validate) {
|
||||
const response = await fetch(`${this.baseUrl}${path}`, init);
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new ApiError(response.status, detail || response.statusText);
|
||||
}
|
||||
const value = await response.json();
|
||||
if (!validate(value)) {
|
||||
throw new ApiError(response.status, `Malformed response from ${path}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
export function decodeServerEvent(event, validate) {
|
||||
let value;
|
||||
try {
|
||||
value = JSON.parse(event.data);
|
||||
} catch (error) {
|
||||
throw new TypeError("Server event is not valid JSON", { cause: error });
|
||||
}
|
||||
if (!validate(value)) {
|
||||
throw new TypeError("Server event has an unexpected shape");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use trx_core::radio::freq::{Band, Freq};
|
||||
use trx_core::rig::state::{SpectrumData, VchanRdsEntry};
|
||||
use trx_core::rig::{
|
||||
RigAccessMethod, RigCapabilities, RigInfo, RigRxStatus, RigStatus, RigTxStatus, RigVfo,
|
||||
RigVfoEntry,
|
||||
};
|
||||
use trx_core::{DecoderConfig, RdsData, RigFilterState, RigMode, RigSnapshot, WfmDenoiseLevel};
|
||||
use trx_frontend_http::server::api::rig::{RigListItem, RigListResponse};
|
||||
use trx_protocol::{DecoderActivation, DecoderDescriptor};
|
||||
use ts_rs::{Config, TS};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut output = String::from(
|
||||
"// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>\n\
|
||||
//\n\
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later\n\n\
|
||||
// Generated by `cargo run -p trx-frontend-http --example generate_typescript`.\n\
|
||||
// Do not edit manually.\n\n",
|
||||
);
|
||||
let config = Config::default();
|
||||
|
||||
macro_rules! export {
|
||||
($type:ty) => {
|
||||
writeln!(output, "export {}\n", <$type as TS>::decl(&config))?;
|
||||
};
|
||||
}
|
||||
|
||||
export!(Band);
|
||||
export!(Freq);
|
||||
export!(RigMode);
|
||||
export!(RigAccessMethod);
|
||||
export!(RigCapabilities);
|
||||
export!(RigInfo);
|
||||
export!(RigVfoEntry);
|
||||
export!(RigVfo);
|
||||
export!(RigTxStatus);
|
||||
export!(RigRxStatus);
|
||||
export!(RigStatus);
|
||||
export!(DecoderConfig);
|
||||
export!(WfmDenoiseLevel);
|
||||
export!(RigFilterState);
|
||||
export!(RdsData);
|
||||
export!(SpectrumData);
|
||||
export!(VchanRdsEntry);
|
||||
export!(RigSnapshot);
|
||||
export!(RigListItem);
|
||||
export!(RigListResponse);
|
||||
export!(DecoderActivation);
|
||||
export!(DecoderDescriptor);
|
||||
|
||||
let output_path =
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("frontend/src/api/generated.ts");
|
||||
fs::create_dir_all(output_path.parent().expect("generated file has a parent"))?;
|
||||
fs::write(output_path, output)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -15,6 +15,7 @@ await rm(outputDir, { recursive: true, force: true });
|
||||
|
||||
await build({
|
||||
entryPoints: {
|
||||
"api-client": path.join(sourceDir, "api", "client.ts"),
|
||||
app: path.join(sourceDir, "app.js"),
|
||||
"ui-core": path.join(sourceDir, "ui-core.js"),
|
||||
"map-core": path.join(sourceDir, "map-core.js"),
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build.mjs",
|
||||
"generate-types": "cargo run -p trx-frontend-http --example generate_typescript",
|
||||
"typecheck": "tsc --project tsconfig.json",
|
||||
"lint": "eslint \"src/**/*.ts\" \"tests/**/*.mjs\" build.mjs --no-error-on-unmatched-pattern",
|
||||
"test": "node --test tests/*.test.mjs",
|
||||
"verify-generated": "npm run build && git diff --exit-code -- ../assets/web/generated"
|
||||
"verify-generated": "npm run generate-types && npm run build && git diff --exit-code -- ../assets/web/generated src/api/generated.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.2",
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import type {
|
||||
DecoderDescriptor,
|
||||
RigListResponse,
|
||||
RigSnapshot,
|
||||
} from "./generated";
|
||||
|
||||
export class ApiError extends Error {
|
||||
public constructor(
|
||||
public readonly status: number,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
type Validator<T> = (value: unknown) => value is T;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function isRigSnapshot(value: unknown): value is RigSnapshot {
|
||||
if (!isRecord(value) || !isRecord(value.info) || !isRecord(value.status)) {
|
||||
return false;
|
||||
}
|
||||
const { info, status } = value;
|
||||
return (
|
||||
typeof value.initialized === "boolean" &&
|
||||
typeof info.manufacturer === "string" &&
|
||||
typeof info.model === "string" &&
|
||||
isRecord(status.freq) &&
|
||||
typeof status.freq.hz === "number" &&
|
||||
(typeof status.mode === "string" || isRecord(status.mode)) &&
|
||||
typeof status.tx_en === "boolean"
|
||||
);
|
||||
}
|
||||
|
||||
export function isRigListResponse(value: unknown): value is RigListResponse {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
(value.active_remote === null || typeof value.active_remote === "string") &&
|
||||
Array.isArray(value.rigs) &&
|
||||
value.rigs.every(
|
||||
(rig) =>
|
||||
isRecord(rig) &&
|
||||
typeof rig.remote === "string" &&
|
||||
typeof rig.manufacturer === "string" &&
|
||||
typeof rig.model === "string" &&
|
||||
Array.isArray(rig.supported_modes) &&
|
||||
typeof rig.tx === "boolean" &&
|
||||
typeof rig.filter_controls === "boolean" &&
|
||||
typeof rig.initialized === "boolean",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isDecoderRegistry(value: unknown): value is DecoderDescriptor[] {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.every(
|
||||
(decoder) =>
|
||||
isRecord(decoder) &&
|
||||
typeof decoder.id === "string" &&
|
||||
typeof decoder.label === "string" &&
|
||||
(decoder.activation === "mode_bound" || decoder.activation === "toggle") &&
|
||||
Array.isArray(decoder.active_modes) &&
|
||||
decoder.active_modes.every((mode) => typeof mode === "string") &&
|
||||
typeof decoder.background_decode === "boolean" &&
|
||||
typeof decoder.bookmark_selectable === "boolean",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export class TrxApi {
|
||||
public constructor(private readonly baseUrl = "") {}
|
||||
|
||||
public async get<T>(path: string, validate: Validator<T>): Promise<T> {
|
||||
return this.request(path, { cache: "no-store" }, validate);
|
||||
}
|
||||
|
||||
public async post<T>(
|
||||
path: string,
|
||||
body: unknown,
|
||||
validate: Validator<T>,
|
||||
): Promise<T> {
|
||||
return this.request(
|
||||
path,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
validate,
|
||||
);
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
validate: Validator<T>,
|
||||
): Promise<T> {
|
||||
const response = await fetch(`${this.baseUrl}${path}`, init);
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new ApiError(response.status, detail || response.statusText);
|
||||
}
|
||||
const value: unknown = await response.json();
|
||||
if (!validate(value)) {
|
||||
throw new ApiError(response.status, `Malformed response from ${path}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeServerEvent<T>(
|
||||
event: MessageEvent<string>,
|
||||
validate: Validator<T>,
|
||||
): T {
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(event.data) as unknown;
|
||||
} catch (error: unknown) {
|
||||
throw new TypeError("Server event is not valid JSON", { cause: error });
|
||||
}
|
||||
if (!validate(value)) {
|
||||
throw new TypeError("Server event has an unexpected shape");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Generated by `cargo run -p trx-frontend-http --example generate_typescript`.
|
||||
// Do not edit manually.
|
||||
|
||||
export type Band = { low_hz: number, high_hz: number, tx_allowed: boolean, };
|
||||
|
||||
export type Freq = { hz: number, };
|
||||
|
||||
export type RigMode = "LSB" | "USB" | "CW" | "CWR" | "AM" | "SAM" | "WFM" | "FM" | "AIS" | "VDES" | "DIG" | "PKT" | { "Other": string };
|
||||
|
||||
export type RigAccessMethod = { "Serial": { path: string, baud: number, } } | { "Tcp": { addr: string, } };
|
||||
|
||||
export type RigCapabilities = { min_freq_step_hz: number, supported_bands: Array<Band>, supported_modes: Array<RigMode>, num_vfos: number, lock: boolean, lockable: boolean, attenuator: boolean, preamp: boolean, rit: boolean, rpt: boolean, split: boolean,
|
||||
/**
|
||||
* Backend supports transmit: PTT, power on/off, TX meters, TX audio.
|
||||
*/
|
||||
tx: boolean,
|
||||
/**
|
||||
* Backend supports get_tx_limit / set_tx_limit.
|
||||
*/
|
||||
tx_limit: boolean,
|
||||
/**
|
||||
* Backend supports toggle_vfo.
|
||||
*/
|
||||
vfo_switch: boolean,
|
||||
/**
|
||||
* Backend supports runtime filter adjustment (bandwidth).
|
||||
*/
|
||||
filter_controls: boolean,
|
||||
/**
|
||||
* Backend returns a meaningful RX signal strength value.
|
||||
*/
|
||||
signal_meter: boolean, };
|
||||
|
||||
export type RigInfo = { manufacturer: string, model: string, revision: string, capabilities: RigCapabilities, access: RigAccessMethod, };
|
||||
|
||||
export type RigVfoEntry = { name: string, freq: Freq, mode: RigMode | null, };
|
||||
|
||||
export type RigVfo = { entries: Array<RigVfoEntry>,
|
||||
/**
|
||||
* Index into `entries` for the active VFO, if known.
|
||||
*/
|
||||
active: number | null, };
|
||||
|
||||
export type RigTxStatus = { power: number | null, limit: number | null, swr: number | null, alc: number | null, };
|
||||
|
||||
export type RigRxStatus = { sig: number | null, };
|
||||
|
||||
export type RigStatus = { freq: Freq, mode: RigMode, tx_en: boolean, vfo: RigVfo | null, tx: RigTxStatus | null, rx: RigRxStatus | null, lock: boolean | null, };
|
||||
|
||||
export type DecoderConfig = { aprs_decode_enabled: boolean, hf_aprs_decode_enabled: boolean, cw_decode_enabled: boolean, ft8_decode_enabled: boolean, ft4_decode_enabled: boolean, ft2_decode_enabled: boolean, wspr_decode_enabled: boolean, lrpt_decode_enabled: boolean, wefax_decode_enabled: boolean, recorder_enabled: boolean, };
|
||||
|
||||
export type WfmDenoiseLevel = "off" | "auto" | "low" | "medium" | "high";
|
||||
|
||||
export type RigFilterState = { bandwidth_hz: number, cw_center_hz: number, sdr_gain_db?: number | null, sdr_lna_gain_db?: number | null, sdr_agc_enabled?: boolean | null, sdr_squelch_enabled?: boolean | null, sdr_squelch_threshold_db?: number | null, sdr_nb_enabled?: boolean | null, sdr_nb_threshold?: number | null, wfm_deemphasis_us: number, wfm_stereo: boolean, wfm_stereo_detected: boolean, wfm_denoise: WfmDenoiseLevel,
|
||||
/**
|
||||
* Co-Channel Interference level (0–100 scale).
|
||||
*/
|
||||
wfm_cci: number,
|
||||
/**
|
||||
* Adjacent Channel Interference level (0–100 scale).
|
||||
*/
|
||||
wfm_aci: number,
|
||||
/**
|
||||
* SAM stereo width (0.0 = mono, 1.0 = full stereo).
|
||||
*/
|
||||
sam_stereo_width: number,
|
||||
/**
|
||||
* SAM carrier synchronization enabled.
|
||||
*/
|
||||
sam_carrier_sync: boolean, };
|
||||
|
||||
export type RdsData = { pi?: number | null, program_service?: string | null, radio_text?: string | null, program_type_name_long?: string | null, pty?: number | null, pty_name?: string | null, traffic_program?: boolean | null, traffic_announcement?: boolean | null, music?: boolean | null, stereo?: boolean | null, artificial_head?: boolean | null, compressed?: boolean | null, dynamic_pty?: boolean | null, alternative_frequencies_hz?: Array<number> | null, };
|
||||
|
||||
export type SpectrumData = {
|
||||
/**
|
||||
* FFT magnitude bins in dBFS, FFT-shifted so DC (centre frequency) is at index N/2.
|
||||
*/
|
||||
bins: Array<number>,
|
||||
/**
|
||||
* Centre frequency of the SDR capture in Hz.
|
||||
*/
|
||||
center_hz: number,
|
||||
/**
|
||||
* SDR capture sample rate in Hz; the displayed span is ±sample_rate/2.
|
||||
*/
|
||||
sample_rate: number,
|
||||
/**
|
||||
* Decoded Radio Data System state, when available for WFM.
|
||||
*/
|
||||
rds?: RdsData | null, };
|
||||
|
||||
export type VchanRdsEntry = {
|
||||
/**
|
||||
* Virtual channel UUID.
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* Latest RDS data, if decoded.
|
||||
*/
|
||||
rds?: RdsData | null,
|
||||
/**
|
||||
* Channel signal level in dBFS.
|
||||
*/
|
||||
signal_db?: number | null, };
|
||||
|
||||
export type RigSnapshot = { info: RigInfo, status: RigStatus, band: string | null, enabled: boolean | null, initialized: boolean, server_callsign?: string | null, server_version?: string | null, server_build_date?: string | null, server_latitude?: number | null, server_longitude?: number | null, pskreporter_status?: string | null, aprs_is_status?: string | null, cw_auto: boolean, cw_wpm: number, cw_tone_hz: number, filter?: RigFilterState | null, spectrum?: SpectrumData | null,
|
||||
/**
|
||||
* Per-virtual-channel RDS snapshots, when available.
|
||||
*/
|
||||
vchan_rds?: Array<VchanRdsEntry> | null, aprs_decode_enabled: boolean, hf_aprs_decode_enabled: boolean, cw_decode_enabled: boolean, ft8_decode_enabled: boolean, ft4_decode_enabled: boolean, ft2_decode_enabled: boolean, wspr_decode_enabled: boolean, lrpt_decode_enabled: boolean, wefax_decode_enabled: boolean, recorder_enabled: boolean, };
|
||||
|
||||
export type RigListItem = { remote: string, display_name: string | null, manufacturer: string, model: string, supported_modes: Array<RigMode>, tx: boolean, filter_controls: boolean, initialized: boolean, latitude: number | null, longitude: number | null, };
|
||||
|
||||
export type RigListResponse = { active_remote: string | null, rigs: Array<RigListItem>, };
|
||||
|
||||
export type DecoderActivation = "mode_bound" | "toggle";
|
||||
|
||||
export type DecoderDescriptor = {
|
||||
/**
|
||||
* Machine identifier, e.g. `"ft8"`, `"aprs"`.
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* Human-readable label, e.g. `"FT8"`, `"APRS"`.
|
||||
*/
|
||||
label: string,
|
||||
/**
|
||||
* How the decoder is activated.
|
||||
*/
|
||||
activation: DecoderActivation,
|
||||
/**
|
||||
* Rig modes where this decoder operates (upper-case).
|
||||
*/
|
||||
active_modes: Array<string>,
|
||||
/**
|
||||
* Whether the decoder can run on SDR virtual channels
|
||||
* (background-decode / scheduler).
|
||||
*/
|
||||
background_decode: boolean,
|
||||
/**
|
||||
* Whether this decoder should appear in bookmark forms.
|
||||
*/
|
||||
bookmark_selectable: boolean, };
|
||||
|
||||
@@ -8,7 +8,7 @@ mod assets;
|
||||
mod bookmarks;
|
||||
mod decoder;
|
||||
pub mod recorder;
|
||||
mod rig;
|
||||
pub mod rig;
|
||||
mod sse;
|
||||
mod vchan;
|
||||
|
||||
|
||||
@@ -382,26 +382,26 @@ pub async fn set_sam_carrier_sync(
|
||||
// Rig list / selection
|
||||
// ============================================================================
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct RigListItem {
|
||||
remote: String,
|
||||
display_name: Option<String>,
|
||||
manufacturer: String,
|
||||
model: String,
|
||||
supported_modes: Vec<trx_core::RigMode>,
|
||||
tx: bool,
|
||||
filter_controls: bool,
|
||||
initialized: bool,
|
||||
#[derive(serde::Serialize, ts_rs::TS)]
|
||||
pub struct RigListItem {
|
||||
pub remote: String,
|
||||
pub display_name: Option<String>,
|
||||
pub manufacturer: String,
|
||||
pub model: String,
|
||||
pub supported_modes: Vec<trx_core::RigMode>,
|
||||
pub tx: bool,
|
||||
pub filter_controls: bool,
|
||||
pub initialized: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
latitude: Option<f64>,
|
||||
pub latitude: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
longitude: Option<f64>,
|
||||
pub longitude: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct RigListResponse {
|
||||
active_remote: Option<String>,
|
||||
rigs: Vec<RigListItem>,
|
||||
#[derive(serde::Serialize, ts_rs::TS)]
|
||||
pub struct RigListResponse {
|
||||
pub active_remote: Option<String>,
|
||||
pub rigs: Vec<RigListItem>,
|
||||
}
|
||||
|
||||
fn build_rig_list_payload(context: &FrontendRuntimeContext) -> RigListResponse {
|
||||
|
||||
@@ -14,5 +14,6 @@ serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
ts-rs = { version = "12.0.1", features = ["uuid-impl"] }
|
||||
sgp4 = "2"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
const SPEED_OF_LIGHT_M_PER_S: f64 = 299_792_458.0;
|
||||
|
||||
/// Supported band range in Hz.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
|
||||
pub struct Band {
|
||||
#[ts(type = "number")]
|
||||
pub low_hz: u64,
|
||||
#[ts(type = "number")]
|
||||
pub high_hz: u64,
|
||||
pub tx_allowed: bool,
|
||||
}
|
||||
@@ -23,8 +26,9 @@ impl Band {
|
||||
}
|
||||
|
||||
/// Frequency wrapper (Hz).
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, TS)]
|
||||
pub struct Freq {
|
||||
#[ts(type = "number")]
|
||||
pub hz: u64,
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::radio::freq::{Band, Freq};
|
||||
use crate::{DynResult, RigMode};
|
||||
@@ -21,14 +22,14 @@ pub mod response;
|
||||
pub mod state;
|
||||
|
||||
/// How this backend communicates with the rig.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
|
||||
pub enum RigAccessMethod {
|
||||
Serial { path: String, baud: u32 },
|
||||
Tcp { addr: String },
|
||||
}
|
||||
|
||||
/// Static info describing a rig backend.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
||||
pub struct RigInfo {
|
||||
pub manufacturer: String,
|
||||
pub model: String,
|
||||
@@ -37,9 +38,10 @@ pub struct RigInfo {
|
||||
pub access: RigAccessMethod,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
|
||||
pub struct RigCapabilities {
|
||||
#[serde(default = "default_min_freq_step_hz")]
|
||||
#[ts(type = "number")]
|
||||
pub min_freq_step_hz: u64,
|
||||
pub supported_bands: Vec<Band>,
|
||||
pub supported_modes: Vec<RigMode>,
|
||||
@@ -314,7 +316,7 @@ pub trait RigSdr: Send {
|
||||
}
|
||||
|
||||
/// Snapshot of a rig's status that every backend can expose.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
||||
pub struct RigStatus {
|
||||
pub freq: Freq,
|
||||
pub mode: RigMode,
|
||||
@@ -330,21 +332,21 @@ pub trait RigStatusProvider {
|
||||
fn status(&self) -> RigStatus;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
||||
pub struct RigVfo {
|
||||
pub entries: Vec<RigVfoEntry>,
|
||||
/// Index into `entries` for the active VFO, if known.
|
||||
pub active: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
||||
pub struct RigVfoEntry {
|
||||
pub name: String,
|
||||
pub freq: Freq,
|
||||
pub mode: Option<RigMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
||||
pub struct RigTxStatus {
|
||||
pub power: Option<u8>,
|
||||
pub limit: Option<u8>,
|
||||
@@ -352,7 +354,7 @@ pub struct RigTxStatus {
|
||||
pub alc: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
||||
pub struct RigRxStatus {
|
||||
pub sig: Option<f64>,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::radio::freq::Freq;
|
||||
@@ -12,7 +13,7 @@ use crate::rig::{RigControl, RigInfo, RigRxStatus, RigStatus, RigStatusProvider,
|
||||
///
|
||||
/// Flattened into `RigState` and `RigSnapshot` so the JSON wire format is
|
||||
/// unchanged (backward compatible with existing clients).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, TS)]
|
||||
pub struct DecoderConfig {
|
||||
#[serde(default)]
|
||||
pub aprs_decode_enabled: bool,
|
||||
@@ -113,7 +114,7 @@ pub struct RigState {
|
||||
}
|
||||
|
||||
/// Mode supported by the rig.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
|
||||
pub enum RigMode {
|
||||
LSB,
|
||||
USB,
|
||||
@@ -315,7 +316,7 @@ impl RigState {
|
||||
}
|
||||
|
||||
/// Current filter/DSP state for backends that support runtime filter adjustment.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
||||
pub struct RigFilterState {
|
||||
pub bandwidth_hz: u32,
|
||||
pub cw_center_hz: u32,
|
||||
@@ -355,7 +356,7 @@ pub struct RigFilterState {
|
||||
pub sam_carrier_sync: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum WfmDenoiseLevel {
|
||||
Off,
|
||||
@@ -386,11 +387,12 @@ fn default_wfm_denoise_level() -> WfmDenoiseLevel {
|
||||
}
|
||||
|
||||
/// Spectrum data from SDR backends (FFT magnitude over the full capture bandwidth).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
||||
pub struct SpectrumData {
|
||||
/// FFT magnitude bins in dBFS, FFT-shifted so DC (centre frequency) is at index N/2.
|
||||
pub bins: Vec<f32>,
|
||||
/// Centre frequency of the SDR capture in Hz.
|
||||
#[ts(type = "number")]
|
||||
pub center_hz: u64,
|
||||
/// SDR capture sample rate in Hz; the displayed span is ±sample_rate/2.
|
||||
pub sample_rate: u32,
|
||||
@@ -400,7 +402,7 @@ pub struct SpectrumData {
|
||||
}
|
||||
|
||||
/// Live RDS metadata decoded from a WFM broadcast.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, TS)]
|
||||
pub struct RdsData {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pi: Option<u16>,
|
||||
@@ -437,7 +439,7 @@ pub struct RdsData {
|
||||
/// `PartialEq` intentionally ignores `signal_db` so that rapidly-changing
|
||||
/// signal levels do not cause the main state snapshot to diff on every poll
|
||||
/// cycle (signal_db flows through the spectrum SSE instead).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
pub struct VchanRdsEntry {
|
||||
/// Virtual channel UUID.
|
||||
pub id: Uuid,
|
||||
@@ -456,7 +458,7 @@ impl PartialEq for VchanRdsEntry {
|
||||
}
|
||||
|
||||
/// Read-only projection of state shared with clients.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
|
||||
pub struct RigSnapshot {
|
||||
pub info: RigInfo,
|
||||
pub status: RigStatus,
|
||||
|
||||
@@ -15,3 +15,4 @@ ft2 = []
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
trx-core = { path = "../trx-core" }
|
||||
ts-rs = "12.0.1"
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
//! their decoder knowledge from [`DECODER_REGISTRY`].
|
||||
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
/// How a decoder is activated.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DecoderActivation {
|
||||
/// Automatically active when the rig mode matches.
|
||||
@@ -25,7 +26,7 @@ pub enum DecoderActivation {
|
||||
}
|
||||
|
||||
/// Static descriptor for a single decoder.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, TS)]
|
||||
pub struct DecoderDescriptor {
|
||||
/// Machine identifier, e.g. `"ft8"`, `"aprs"`.
|
||||
pub id: &'static str,
|
||||
|
||||
Reference in New Issue
Block a user