feat: generate typed frontend API contracts

This commit is contained in:
sjg
2026-08-01 11:58:09 +02:00
parent 888f793eb8
commit bbc53d56b0
16 changed files with 504 additions and 38 deletions
Generated
+35
View File
@@ -2645,6 +2645,15 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -3137,6 +3146,7 @@ dependencies = [
"sgp4",
"tokio",
"tracing",
"ts-rs",
"uuid",
]
@@ -3195,6 +3205,7 @@ dependencies = [
"trx-core",
"trx-frontend",
"trx-protocol",
"ts-rs",
"uuid",
]
@@ -3238,6 +3249,7 @@ dependencies = [
"serde",
"serde_json",
"trx-core",
"ts-rs",
]
[[package]]
@@ -3333,6 +3345,29 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ts-rs"
version = "12.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
dependencies = [
"thiserror 2.0.18",
"ts-rs-macros",
"uuid",
]
[[package]]
name = "ts-rs-macros"
version = "12.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
dependencies = [
"proc-macro2",
"quote",
"syn",
"termcolor",
]
[[package]]
name = "typenum"
version = "1.20.0"
@@ -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 (0100 scale).
*/
wfm_cci: number,
/**
* Adjacent Channel Interference level (0100 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 {
+1
View File
@@ -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"] }
+6 -2
View File
@@ -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,
}
+10 -8
View File
@@ -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>,
}
+10 -8
View File
@@ -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,
+1
View File
@@ -15,3 +15,4 @@ ft2 = []
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
trx-core = { path = "../trx-core" }
ts-rs = "12.0.1"
+3 -2
View File
@@ -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,