initial commit
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -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 }
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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>>,
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user