initial commit

Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2025-11-24 22:02:23 +01:00
commit 025eb237b2
37 changed files with 5175 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
#
# SPDX-License-Identifier: BSD-2-Clause
[package]
name = "trx-core"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { workspace = true, features = ["full"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tracing = { workspace = true }
+30
View File
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
use serde::{Deserialize, Serialize};
use crate::rig::state::RigSnapshot;
/// Command received from network clients (JSON).
#[derive(Debug, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum ClientCommand {
GetState,
SetFreq { freq_hz: u64 },
SetMode { mode: String },
SetPtt { ptt: bool },
PowerOn,
PowerOff,
ToggleVfo,
GetTxLimit,
SetTxLimit { limit: u8 },
}
/// Response sent to network clients over TCP.
#[derive(Debug, Serialize)]
pub struct ClientResponse {
pub success: bool,
pub state: Option<RigSnapshot>,
pub error: Option<String>,
}
+16
View File
@@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
pub mod client;
pub mod math;
pub mod radio;
pub mod rig;
pub type DynResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
pub use client::{ClientCommand, ClientResponse};
pub use rig::command::RigCommand;
pub use rig::request::RigRequest;
pub use rig::response::{RigError, RigResult};
pub use rig::state::{RigMode, RigSnapshot, RigState};
+48
View File
@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
use crate::DynResult;
/// Encode frequency in Hz into 4 BCD bytes (10 Hz resolution) used by Yaesu CAT.
pub fn encode_freq_bcd(freq_hz: u64) -> DynResult<[u8; 4]> {
if !freq_hz.is_multiple_of(10) {
return Err("frequency must be a multiple of 10 Hz for CAT encoding".into());
}
let mut n = freq_hz / 10; // FT-817 uses 10 Hz units.
if n > 99_999_999 {
return Err("frequency out of range for CAT BCD encoding".into());
}
let mut digits = [0u8; 8];
for i in (0..8).rev() {
digits[i] = (n % 10) as u8;
n /= 10;
}
let mut out = [0u8; 4];
for i in 0..4 {
out[i] = (digits[i * 2] << 4) | digits[i * 2 + 1];
}
Ok(out)
}
/// Decode 4 BCD bytes (10 Hz resolution) into frequency in Hz.
pub fn decode_freq_bcd(bytes: [u8; 4]) -> DynResult<u64> {
let mut value = 0u64;
for b in bytes {
let high = (b >> 4) & 0x0F;
let low = b & 0x0F;
if high >= 10 || low >= 10 {
return Err("invalid BCD digit in frequency".into());
}
value = value * 10 + u64::from(high);
value = value * 10 + u64::from(low);
}
Ok(value * 10) // Convert back to Hz from 10 Hz units.
}
+7
View File
@@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
pub mod bcd;
pub use bcd::{decode_freq_bcd, encode_freq_bcd};
+72
View File
@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
use serde::{Deserialize, Serialize};
const SPEED_OF_LIGHT_M_PER_S: f64 = 299_792_458.0;
/// Supported band range in Hz.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Band {
pub low_hz: u64,
pub high_hz: u64,
pub tx_allowed: bool,
}
impl Band {
/// Midpoint frequency of the band in Hz.
#[must_use]
pub fn center_hz(&self) -> u64 {
u64::midpoint(self.low_hz, self.high_hz)
}
}
/// Frequency wrapper (Hz).
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Freq {
pub hz: u64,
}
impl Freq {
#[must_use]
pub fn new(hz: u64) -> Self {
Self { hz }
}
/// Return the band name for this frequency, if any, using the provided band list.
pub fn band_name(&self, bands: &[Band]) -> Option<String> {
band_for_freq(bands, self).map(band_name)
}
}
/// Find the band that contains the given frequency (inclusive), if any.
pub fn band_for_freq<'a>(bands: &'a [Band], freq: &Freq) -> Option<&'a Band> {
bands
.iter()
.find(|b| freq.hz >= b.low_hz && freq.hz <= b.high_hz)
}
/// Convert a frequency in Hz to a human-friendly wavelength string.
///
/// Values above one meter are rounded to the nearest meter; shorter wavelengths
/// are shown in centimeters.
pub fn wavelength_label(freq_hz: u64) -> String {
if freq_hz == 0 {
return "-".to_string();
}
let wavelength_m = SPEED_OF_LIGHT_M_PER_S / (freq_hz as f64);
if wavelength_m >= 1.0 {
format!("{:.0}m", wavelength_m.round())
} else {
format!("{:.0}cm", (wavelength_m * 100.0).round())
}
}
/// Derive a human-friendly band label from a band's wavelength.
///
/// The label is computed from the wavelength at the band's center frequency.
pub fn band_name(band: &Band) -> String {
wavelength_label(band.center_hz())
}
+7
View File
@@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
pub mod freq;
pub use freq::{band_for_freq, band_name, wavelength_label, Band, Freq};
+22
View File
@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
use crate::radio::freq::Freq;
use crate::RigMode;
/// Internal command handled by the rig task.
#[derive(Debug, Clone)]
pub enum RigCommand {
GetSnapshot,
SetFreq(Freq),
SetMode(RigMode),
SetPtt(bool),
PowerOn,
PowerOff,
ToggleVfo,
GetTxLimit,
SetTxLimit(u8),
Lock,
Unlock,
}
+155
View File
@@ -0,0 +1,155 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
use std::future::Future;
use std::pin::Pin;
use serde::Serialize;
use crate::radio::freq::{Band, Freq};
use crate::{DynResult, RigMode};
/// Alias to reduce type complexity in RigCat.
pub type RigStatusFuture<'a> =
Pin<Box<dyn Future<Output = DynResult<(Freq, RigMode, Option<RigVfo>)>> + Send + 'a>>;
pub mod command;
pub mod request;
pub mod response;
pub mod state;
/// How this backend communicates with the rig.
#[derive(Debug, Clone, Serialize)]
pub enum RigAccessMethod {
Serial { path: String, baud: u32 },
Tcp { addr: String },
}
/// Static info describing a rig backend.
#[derive(Debug, Clone, Serialize)]
pub struct RigInfo {
pub manufacturer: &'static str,
pub model: &'static str,
pub revision: &'static str,
pub capabilities: RigCapabilities,
pub access: RigAccessMethod,
}
#[derive(Debug, Clone, Serialize)]
pub struct RigCapabilities {
pub supported_bands: Vec<Band>,
pub supported_modes: Vec<RigMode>,
pub num_vfos: usize,
pub lock: bool,
pub lockable: bool,
pub attenuator: bool,
pub preamp: bool,
pub rit: bool,
pub rpt: bool,
pub split: bool,
}
/// Common interface for rig backends.
pub trait Rig {
fn info(&self) -> &RigInfo;
}
/// Common CAT control operations any rig backend should implement.
pub trait RigCat: Rig + Send {
fn get_status<'a>(&'a mut self) -> RigStatusFuture<'a>;
fn set_freq<'a>(
&'a mut self,
freq: Freq,
) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>>;
fn set_mode<'a>(
&'a mut self,
mode: RigMode,
) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>>;
fn set_ptt<'a>(
&'a mut self,
ptt: bool,
) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>>;
fn power_on<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>>;
fn power_off<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>>;
fn get_signal_strength<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = DynResult<u8>> + Send + 'a>>;
fn get_tx_power<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = DynResult<u8>> + Send + 'a>>;
fn get_tx_limit<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = DynResult<u8>> + Send + 'a>>;
fn set_tx_limit<'a>(
&'a mut self,
limit: u8,
) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>>;
fn toggle_vfo<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>>;
fn lock<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>>;
fn unlock<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>>;
}
/// Snapshot of a rig's status that every backend can expose.
#[derive(Debug, Clone, Serialize)]
pub struct RigStatus {
pub freq: Freq,
pub mode: RigMode,
pub tx_en: bool,
pub vfo: Option<RigVfo>,
pub tx: Option<RigTxStatus>,
pub rx: Option<RigRxStatus>,
pub lock: Option<bool>,
}
/// Trait for presenting rig status in a backend-agnostic way.
pub trait RigStatusProvider {
fn status(&self) -> RigStatus;
}
#[derive(Debug, Clone, Serialize)]
pub struct RigVfo {
pub entries: Vec<RigVfoEntry>,
/// Index into `entries` for the active VFO, if known.
pub active: Option<usize>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RigVfoEntry {
pub name: String,
pub freq: Freq,
pub mode: Option<RigMode>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RigTxStatus {
pub power: Option<u8>,
pub limit: Option<u8>,
pub swr: Option<f32>,
pub alc: Option<u8>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RigRxStatus {
pub sig: Option<i32>,
}
/// Configurable control settings that can be pushed to the rig.
#[derive(Debug, Clone, Serialize)]
pub struct RigControl {
pub enabled: Option<bool>,
pub lock: Option<bool>,
pub clar_hz: Option<i32>,
pub clar_on: Option<bool>,
pub rpt_offset_hz: Option<i32>,
pub ctcss_hz: Option<f32>,
pub dcs_code: Option<u16>,
}
+14
View File
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
use tokio::sync::oneshot;
use crate::{RigCommand, RigResult, RigSnapshot};
/// Request sent to the rig task.
#[derive(Debug)]
pub struct RigRequest {
pub cmd: RigCommand,
pub respond_to: oneshot::Sender<RigResult<RigSnapshot>>,
}
+23
View File
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
use serde::Serialize;
/// Error type returned by rig requests.
#[derive(Debug, Clone, Serialize)]
pub struct RigError(pub String);
pub type RigResult<T> = Result<T, RigError>;
impl From<String> for RigError {
fn from(value: String) -> Self {
RigError(value)
}
}
impl From<&str> for RigError {
fn from(value: &str) -> Self {
RigError(value.to_string())
}
}
+93
View File
@@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause
use serde::{Deserialize, Serialize};
use crate::rig::{RigControl, RigInfo, RigStatus, RigStatusProvider};
/// Simple transceiver state representation held by the rig task.
#[derive(Debug, Clone, Serialize)]
pub struct RigState {
#[serde(skip_deserializing)]
pub rig_info: Option<RigInfo>,
pub status: RigStatus,
pub initialized: bool,
#[serde(skip_serializing, skip_deserializing)]
pub control: RigControl,
}
/// Mode supported by the rig.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RigMode {
LSB,
USB,
CW,
CWR,
AM,
WFM,
FM,
DIG,
PKT,
Other(String),
}
impl RigStatusProvider for RigState {
fn status(&self) -> RigStatus {
self.status.clone()
}
}
impl RigState {
pub fn band_name(&self) -> Option<String> {
self.rig_info.as_ref().and_then(|info| {
self.status
.freq
.band_name(&info.capabilities.supported_bands)
})
}
/// Produce an immutable snapshot suitable for sharing with clients.
pub fn snapshot(&self) -> Option<RigSnapshot> {
let info = self.rig_info.clone()?;
Some(RigSnapshot {
info,
status: self.status.clone(),
band: self.band_name(),
enabled: self.control.enabled,
initialized: self.initialized,
})
}
/// Apply a frequency change into the state.
pub fn apply_freq(&mut self, freq: crate::radio::freq::Freq) {
self.status.freq = freq;
}
/// Apply a mode change into the state.
pub fn apply_mode(&mut self, mode: RigMode) {
self.status.mode = mode;
}
/// Apply a PTT change, resetting meters on TX off.
pub fn apply_ptt(&mut self, ptt: bool) {
self.status.tx_en = ptt;
self.status.lock = self.control.lock;
if !ptt {
if let Some(tx) = self.status.tx.as_mut() {
tx.power = Some(0);
tx.swr = Some(0.0);
}
}
}
}
/// Read-only projection of state shared with clients.
#[derive(Debug, Clone, Serialize)]
pub struct RigSnapshot {
pub info: RigInfo,
pub status: RigStatus,
pub band: Option<String>,
pub enabled: Option<bool>,
pub initialized: bool,
}