core: make rig snapshot serializable
This commit is contained in:
@@ -33,9 +33,9 @@ impl Ft817 {
|
||||
let builder = tokio_serial::new(path, baud);
|
||||
let port = builder.open_native_async()?;
|
||||
let info = RigInfo {
|
||||
manufacturer: "Yaesu",
|
||||
model: "FT-817",
|
||||
revision: "",
|
||||
manufacturer: "Yaesu".to_string(),
|
||||
model: "FT-817".to_string(),
|
||||
revision: "".to_string(),
|
||||
capabilities: RigCapabilities {
|
||||
supported_bands: vec![
|
||||
// Transmit-capable amateur bands
|
||||
|
||||
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::rig::state::RigSnapshot;
|
||||
|
||||
/// Command received from network clients (JSON).
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "snake_case")]
|
||||
pub enum ClientCommand {
|
||||
GetState,
|
||||
@@ -17,12 +17,22 @@ pub enum ClientCommand {
|
||||
PowerOn,
|
||||
PowerOff,
|
||||
ToggleVfo,
|
||||
Lock,
|
||||
Unlock,
|
||||
GetTxLimit,
|
||||
SetTxLimit { limit: u8 },
|
||||
}
|
||||
|
||||
/// Envelope for client commands with optional authentication token.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ClientEnvelope {
|
||||
pub token: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub cmd: ClientCommand,
|
||||
}
|
||||
|
||||
/// Response sent to network clients over TCP.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ClientResponse {
|
||||
pub success: bool,
|
||||
pub state: Option<RigSnapshot>,
|
||||
|
||||
@@ -0,0 +1,613 @@
|
||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
//! Command handlers for rig operations.
|
||||
//!
|
||||
//! This module provides a trait-based command system where each command
|
||||
//! is encapsulated in its own struct with validation and execution logic.
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use crate::radio::freq::Freq;
|
||||
use crate::rig::state::RigMode;
|
||||
use crate::DynResult;
|
||||
|
||||
use super::machine::RigMachineState;
|
||||
|
||||
/// Result of command validation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ValidationResult {
|
||||
/// Command can be executed.
|
||||
Ok,
|
||||
/// Command cannot be executed due to current state.
|
||||
InvalidState(String),
|
||||
/// Command parameters are invalid.
|
||||
InvalidParams(String),
|
||||
/// Panel is locked.
|
||||
Locked,
|
||||
}
|
||||
|
||||
impl ValidationResult {
|
||||
pub fn is_ok(&self) -> bool {
|
||||
matches!(self, Self::Ok)
|
||||
}
|
||||
}
|
||||
|
||||
/// Context provided to commands for execution.
|
||||
/// This allows commands to access rig state without owning it.
|
||||
pub trait CommandContext: Send {
|
||||
/// Get the current state machine state.
|
||||
fn state(&self) -> &RigMachineState;
|
||||
|
||||
/// Check if the panel is locked.
|
||||
fn is_locked(&self) -> bool {
|
||||
self.state().is_locked()
|
||||
}
|
||||
|
||||
/// Check if the rig is initialized.
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.state().is_initialized()
|
||||
}
|
||||
|
||||
/// Check if the rig is transmitting.
|
||||
fn is_transmitting(&self) -> bool {
|
||||
self.state().is_transmitting()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for rig commands following the Command Pattern.
|
||||
///
|
||||
/// Each command encapsulates:
|
||||
/// - Validation logic (`can_execute`)
|
||||
/// - Execution logic (`execute`)
|
||||
/// - Optional description for logging
|
||||
pub trait RigCommandHandler: Debug + Send + Sync {
|
||||
/// Human-readable name of the command.
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Validate if the command can be executed in the current context.
|
||||
fn can_execute(&self, ctx: &dyn CommandContext) -> ValidationResult;
|
||||
|
||||
/// Execute the command. Returns the result of the operation.
|
||||
/// The actual rig interaction is done via the executor passed to the pipeline.
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>>;
|
||||
}
|
||||
|
||||
/// Executor interface for commands to interact with the rig.
|
||||
/// This abstracts the actual rig communication from the command logic.
|
||||
pub trait CommandExecutor: Send {
|
||||
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 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>>;
|
||||
|
||||
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 refresh_state<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'a>>;
|
||||
}
|
||||
|
||||
/// Result of command execution containing any state updates.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CommandResult {
|
||||
/// Command executed successfully with no state change needed.
|
||||
Ok,
|
||||
/// Command executed and frequency was updated.
|
||||
FreqUpdated(Freq),
|
||||
/// Command executed and mode was updated.
|
||||
ModeUpdated(RigMode),
|
||||
/// Command executed and PTT state was updated.
|
||||
PttUpdated(bool),
|
||||
/// Command executed and power state was updated.
|
||||
PowerUpdated(bool),
|
||||
/// Command executed and lock state was updated.
|
||||
LockUpdated(bool),
|
||||
/// Command executed and TX limit was updated.
|
||||
TxLimitUpdated(u8),
|
||||
/// Command requires state refresh from rig.
|
||||
RefreshRequired,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Concrete Command Implementations
|
||||
// ============================================================================
|
||||
|
||||
/// Command to set the rig frequency.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SetFreqCommand {
|
||||
pub freq: Freq,
|
||||
}
|
||||
|
||||
impl SetFreqCommand {
|
||||
pub fn new(freq: Freq) -> Self {
|
||||
Self { freq }
|
||||
}
|
||||
}
|
||||
|
||||
impl RigCommandHandler for SetFreqCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"SetFreq"
|
||||
}
|
||||
|
||||
fn can_execute(&self, ctx: &dyn CommandContext) -> ValidationResult {
|
||||
if !ctx.is_initialized() {
|
||||
return ValidationResult::InvalidState("Rig not initialized".into());
|
||||
}
|
||||
if ctx.is_locked() {
|
||||
return ValidationResult::Locked;
|
||||
}
|
||||
if self.freq.hz == 0 {
|
||||
return ValidationResult::InvalidParams("Frequency cannot be 0 Hz".into());
|
||||
}
|
||||
ValidationResult::Ok
|
||||
}
|
||||
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
executor.set_freq(self.freq).await?;
|
||||
Ok(CommandResult::FreqUpdated(self.freq))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to set the rig mode.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SetModeCommand {
|
||||
pub mode: RigMode,
|
||||
}
|
||||
|
||||
impl SetModeCommand {
|
||||
pub fn new(mode: RigMode) -> Self {
|
||||
Self { mode }
|
||||
}
|
||||
}
|
||||
|
||||
impl RigCommandHandler for SetModeCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"SetMode"
|
||||
}
|
||||
|
||||
fn can_execute(&self, ctx: &dyn CommandContext) -> ValidationResult {
|
||||
if !ctx.is_initialized() {
|
||||
return ValidationResult::InvalidState("Rig not initialized".into());
|
||||
}
|
||||
if ctx.is_locked() {
|
||||
return ValidationResult::Locked;
|
||||
}
|
||||
ValidationResult::Ok
|
||||
}
|
||||
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>> {
|
||||
let mode = self.mode.clone();
|
||||
Box::pin(async move {
|
||||
executor.set_mode(mode.clone()).await?;
|
||||
Ok(CommandResult::ModeUpdated(mode))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to set PTT state.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SetPttCommand {
|
||||
pub ptt: bool,
|
||||
}
|
||||
|
||||
impl SetPttCommand {
|
||||
pub fn new(ptt: bool) -> Self {
|
||||
Self { ptt }
|
||||
}
|
||||
}
|
||||
|
||||
impl RigCommandHandler for SetPttCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"SetPtt"
|
||||
}
|
||||
|
||||
fn can_execute(&self, ctx: &dyn CommandContext) -> ValidationResult {
|
||||
if !ctx.is_initialized() {
|
||||
return ValidationResult::InvalidState("Rig not initialized".into());
|
||||
}
|
||||
ValidationResult::Ok
|
||||
}
|
||||
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>> {
|
||||
let ptt = self.ptt;
|
||||
Box::pin(async move {
|
||||
executor.set_ptt(ptt).await?;
|
||||
Ok(CommandResult::PttUpdated(ptt))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to power on the rig.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PowerOnCommand;
|
||||
|
||||
impl RigCommandHandler for PowerOnCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"PowerOn"
|
||||
}
|
||||
|
||||
fn can_execute(&self, _ctx: &dyn CommandContext) -> ValidationResult {
|
||||
// Power on can always be attempted
|
||||
ValidationResult::Ok
|
||||
}
|
||||
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
executor.power_on().await?;
|
||||
Ok(CommandResult::PowerUpdated(true))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to power off the rig.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PowerOffCommand;
|
||||
|
||||
impl RigCommandHandler for PowerOffCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"PowerOff"
|
||||
}
|
||||
|
||||
fn can_execute(&self, ctx: &dyn CommandContext) -> ValidationResult {
|
||||
if ctx.is_transmitting() {
|
||||
return ValidationResult::InvalidState("Cannot power off while transmitting".into());
|
||||
}
|
||||
ValidationResult::Ok
|
||||
}
|
||||
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
executor.power_off().await?;
|
||||
Ok(CommandResult::PowerUpdated(false))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to toggle VFO.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToggleVfoCommand;
|
||||
|
||||
impl RigCommandHandler for ToggleVfoCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"ToggleVfo"
|
||||
}
|
||||
|
||||
fn can_execute(&self, ctx: &dyn CommandContext) -> ValidationResult {
|
||||
if !ctx.is_initialized() {
|
||||
return ValidationResult::InvalidState("Rig not initialized".into());
|
||||
}
|
||||
if ctx.is_locked() {
|
||||
return ValidationResult::Locked;
|
||||
}
|
||||
ValidationResult::Ok
|
||||
}
|
||||
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
executor.toggle_vfo().await?;
|
||||
Ok(CommandResult::RefreshRequired)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to lock the panel.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LockCommand;
|
||||
|
||||
impl RigCommandHandler for LockCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"Lock"
|
||||
}
|
||||
|
||||
fn can_execute(&self, ctx: &dyn CommandContext) -> ValidationResult {
|
||||
if !ctx.is_initialized() {
|
||||
return ValidationResult::InvalidState("Rig not initialized".into());
|
||||
}
|
||||
ValidationResult::Ok
|
||||
}
|
||||
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
executor.lock().await?;
|
||||
Ok(CommandResult::LockUpdated(true))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to unlock the panel.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UnlockCommand;
|
||||
|
||||
impl RigCommandHandler for UnlockCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"Unlock"
|
||||
}
|
||||
|
||||
fn can_execute(&self, _ctx: &dyn CommandContext) -> ValidationResult {
|
||||
// Unlock can always be attempted
|
||||
ValidationResult::Ok
|
||||
}
|
||||
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
executor.unlock().await?;
|
||||
Ok(CommandResult::LockUpdated(false))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to get TX limit.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetTxLimitCommand;
|
||||
|
||||
impl RigCommandHandler for GetTxLimitCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"GetTxLimit"
|
||||
}
|
||||
|
||||
fn can_execute(&self, ctx: &dyn CommandContext) -> ValidationResult {
|
||||
if !ctx.is_initialized() {
|
||||
return ValidationResult::InvalidState("Rig not initialized".into());
|
||||
}
|
||||
ValidationResult::Ok
|
||||
}
|
||||
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let limit = executor.get_tx_limit().await?;
|
||||
Ok(CommandResult::TxLimitUpdated(limit))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to set TX limit.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SetTxLimitCommand {
|
||||
pub limit: u8,
|
||||
}
|
||||
|
||||
impl SetTxLimitCommand {
|
||||
pub fn new(limit: u8) -> Self {
|
||||
Self { limit }
|
||||
}
|
||||
}
|
||||
|
||||
impl RigCommandHandler for SetTxLimitCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"SetTxLimit"
|
||||
}
|
||||
|
||||
fn can_execute(&self, ctx: &dyn CommandContext) -> ValidationResult {
|
||||
if !ctx.is_initialized() {
|
||||
return ValidationResult::InvalidState("Rig not initialized".into());
|
||||
}
|
||||
ValidationResult::Ok
|
||||
}
|
||||
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>> {
|
||||
let limit = self.limit;
|
||||
Box::pin(async move {
|
||||
executor.set_tx_limit(limit).await?;
|
||||
Ok(CommandResult::TxLimitUpdated(limit))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Command to get current state snapshot.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GetSnapshotCommand;
|
||||
|
||||
impl RigCommandHandler for GetSnapshotCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"GetSnapshot"
|
||||
}
|
||||
|
||||
fn can_execute(&self, _ctx: &dyn CommandContext) -> ValidationResult {
|
||||
// Getting snapshot can always be attempted
|
||||
ValidationResult::Ok
|
||||
}
|
||||
|
||||
fn execute<'a>(
|
||||
&'a self,
|
||||
executor: &'a mut dyn CommandExecutor,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<CommandResult>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
executor.refresh_state().await?;
|
||||
Ok(CommandResult::RefreshRequired)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Command Factory
|
||||
// ============================================================================
|
||||
|
||||
use crate::rig::command::RigCommand;
|
||||
|
||||
/// Convert from the existing RigCommand enum to a command handler.
|
||||
pub fn command_from_rig_command(cmd: RigCommand) -> Box<dyn RigCommandHandler> {
|
||||
match cmd {
|
||||
RigCommand::GetSnapshot => Box::new(GetSnapshotCommand),
|
||||
RigCommand::SetFreq(freq) => Box::new(SetFreqCommand::new(freq)),
|
||||
RigCommand::SetMode(mode) => Box::new(SetModeCommand::new(mode)),
|
||||
RigCommand::SetPtt(ptt) => Box::new(SetPttCommand::new(ptt)),
|
||||
RigCommand::PowerOn => Box::new(PowerOnCommand),
|
||||
RigCommand::PowerOff => Box::new(PowerOffCommand),
|
||||
RigCommand::ToggleVfo => Box::new(ToggleVfoCommand),
|
||||
RigCommand::GetTxLimit => Box::new(GetTxLimitCommand),
|
||||
RigCommand::SetTxLimit(limit) => Box::new(SetTxLimitCommand::new(limit)),
|
||||
RigCommand::Lock => Box::new(LockCommand),
|
||||
RigCommand::Unlock => Box::new(UnlockCommand),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct MockContext {
|
||||
state: RigMachineState,
|
||||
}
|
||||
|
||||
impl CommandContext for MockContext {
|
||||
fn state(&self) -> &RigMachineState {
|
||||
&self.state
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_freq_validation_locked() {
|
||||
use crate::rig::controller::machine::ReadyStateData;
|
||||
use crate::rig::{RigAccessMethod, RigCapabilities, RigInfo};
|
||||
|
||||
let ctx = MockContext {
|
||||
state: RigMachineState::Ready(ReadyStateData {
|
||||
rig_info: RigInfo {
|
||||
manufacturer: "Test".to_string(),
|
||||
model: "Mock".to_string(),
|
||||
revision: "1.0".to_string(),
|
||||
capabilities: RigCapabilities {
|
||||
supported_bands: vec![],
|
||||
supported_modes: vec![],
|
||||
num_vfos: 2,
|
||||
lock: false,
|
||||
lockable: true,
|
||||
attenuator: false,
|
||||
preamp: false,
|
||||
rit: false,
|
||||
rpt: false,
|
||||
split: false,
|
||||
},
|
||||
access: RigAccessMethod::Serial {
|
||||
path: "/dev/test".to_string(),
|
||||
baud: 9600,
|
||||
},
|
||||
},
|
||||
freq: Freq { hz: 14_200_000 },
|
||||
mode: RigMode::USB,
|
||||
vfo: None,
|
||||
rx: None,
|
||||
tx_limit: None,
|
||||
locked: true, // Panel is locked
|
||||
}),
|
||||
};
|
||||
|
||||
let cmd = SetFreqCommand::new(Freq { hz: 14_300_000 });
|
||||
let result = cmd.can_execute(&ctx);
|
||||
assert!(matches!(result, ValidationResult::Locked));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_freq_validation_not_initialized() {
|
||||
let ctx = MockContext {
|
||||
state: RigMachineState::Disconnected,
|
||||
};
|
||||
|
||||
let cmd = SetFreqCommand::new(Freq { hz: 14_300_000 });
|
||||
let result = cmd.can_execute(&ctx);
|
||||
assert!(matches!(result, ValidationResult::InvalidState(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_power_off_while_transmitting() {
|
||||
use crate::rig::controller::machine::TransmittingStateData;
|
||||
use crate::rig::{RigAccessMethod, RigCapabilities, RigInfo};
|
||||
|
||||
let ctx = MockContext {
|
||||
state: RigMachineState::Transmitting(TransmittingStateData {
|
||||
rig_info: RigInfo {
|
||||
manufacturer: "Test".to_string(),
|
||||
model: "Mock".to_string(),
|
||||
revision: "1.0".to_string(),
|
||||
capabilities: RigCapabilities {
|
||||
supported_bands: vec![],
|
||||
supported_modes: vec![],
|
||||
num_vfos: 2,
|
||||
lock: false,
|
||||
lockable: true,
|
||||
attenuator: false,
|
||||
preamp: false,
|
||||
rit: false,
|
||||
rpt: false,
|
||||
split: false,
|
||||
},
|
||||
access: RigAccessMethod::Serial {
|
||||
path: "/dev/test".to_string(),
|
||||
baud: 9600,
|
||||
},
|
||||
},
|
||||
freq: Freq { hz: 14_200_000 },
|
||||
mode: RigMode::USB,
|
||||
vfo: None,
|
||||
tx: None,
|
||||
locked: false,
|
||||
}),
|
||||
};
|
||||
|
||||
let cmd = PowerOffCommand;
|
||||
let result = cmd.can_execute(&ctx);
|
||||
assert!(matches!(result, ValidationResult::InvalidState(_)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
//! Rig state machine for lifecycle management.
|
||||
//!
|
||||
//! This module provides an explicit state machine for managing rig states,
|
||||
//! making state transitions clear and preventing invalid states.
|
||||
|
||||
use std::fmt;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::radio::freq::Freq;
|
||||
use crate::rig::state::RigMode;
|
||||
use crate::rig::{RigInfo, RigRxStatus, RigStatus, RigTxStatus, RigVfo};
|
||||
|
||||
/// Events that can trigger state transitions in the rig state machine.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RigEvent {
|
||||
/// Connection to rig established
|
||||
Connected,
|
||||
/// Rig initialization complete
|
||||
Initialized,
|
||||
/// Rig powered on
|
||||
PoweredOn,
|
||||
/// Rig powered off
|
||||
PoweredOff,
|
||||
/// PTT engaged (transmitting)
|
||||
PttOn,
|
||||
/// PTT released (receiving)
|
||||
PttOff,
|
||||
/// Error occurred
|
||||
Error(RigStateError),
|
||||
/// Recovery from error
|
||||
Recovered,
|
||||
/// Disconnect requested or detected
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
/// Error information stored in error state.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RigStateError {
|
||||
pub message: String,
|
||||
pub recoverable: bool,
|
||||
pub occurred_at: Option<u64>, // Unix timestamp, Option for serialization
|
||||
}
|
||||
|
||||
impl RigStateError {
|
||||
pub fn new(message: impl Into<String>, recoverable: bool) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
recoverable,
|
||||
occurred_at: Some(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transient(message: impl Into<String>) -> Self {
|
||||
Self::new(message, true)
|
||||
}
|
||||
|
||||
pub fn fatal(message: impl Into<String>) -> Self {
|
||||
Self::new(message, false)
|
||||
}
|
||||
}
|
||||
|
||||
/// The current state of the rig state machine.
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
#[serde(tag = "state", content = "data")]
|
||||
pub enum RigMachineState {
|
||||
/// Initial state, not connected to rig
|
||||
#[default]
|
||||
Disconnected,
|
||||
/// Connecting to rig backend
|
||||
Connecting { started_at: Option<u64> },
|
||||
/// Connected but not yet initialized
|
||||
Initializing { rig_info: Option<RigInfo> },
|
||||
/// Rig is powered off but connected
|
||||
PoweredOff { rig_info: RigInfo },
|
||||
/// Rig is ready and idle (receiving)
|
||||
Ready(ReadyStateData),
|
||||
/// Rig is transmitting
|
||||
Transmitting(TransmittingStateData),
|
||||
/// Error state
|
||||
Error {
|
||||
error: RigStateError,
|
||||
previous_state: Box<RigMachineState>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Data held when rig is in Ready state.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ReadyStateData {
|
||||
pub rig_info: RigInfo,
|
||||
pub freq: Freq,
|
||||
pub mode: RigMode,
|
||||
pub vfo: Option<RigVfo>,
|
||||
pub rx: Option<RigRxStatus>,
|
||||
pub tx_limit: Option<u8>,
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
/// Data held when rig is in Transmitting state.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct TransmittingStateData {
|
||||
pub rig_info: RigInfo,
|
||||
pub freq: Freq,
|
||||
pub mode: RigMode,
|
||||
pub vfo: Option<RigVfo>,
|
||||
pub tx: Option<RigTxStatus>,
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for RigMachineState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Disconnected => write!(f, "Disconnected"),
|
||||
Self::Connecting { .. } => write!(f, "Connecting"),
|
||||
Self::Initializing { .. } => write!(f, "Initializing"),
|
||||
Self::PoweredOff { .. } => write!(f, "PoweredOff"),
|
||||
Self::Ready(_) => write!(f, "Ready"),
|
||||
Self::Transmitting(_) => write!(f, "Transmitting"),
|
||||
Self::Error { error, .. } => write!(f, "Error({})", error.message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RigMachineState {
|
||||
/// Check if the rig is in a state where commands can be executed.
|
||||
pub fn can_execute_commands(&self) -> bool {
|
||||
matches!(self, Self::Ready(_) | Self::Transmitting(_))
|
||||
}
|
||||
|
||||
/// Check if the rig is initialized.
|
||||
pub fn is_initialized(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Ready(_) | Self::Transmitting(_) | Self::PoweredOff { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if the rig is transmitting.
|
||||
pub fn is_transmitting(&self) -> bool {
|
||||
matches!(self, Self::Transmitting(_))
|
||||
}
|
||||
|
||||
/// Check if the rig is in an error state.
|
||||
pub fn is_error(&self) -> bool {
|
||||
matches!(self, Self::Error { .. })
|
||||
}
|
||||
|
||||
/// Check if the panel is locked.
|
||||
pub fn is_locked(&self) -> bool {
|
||||
match self {
|
||||
Self::Ready(data) => data.locked,
|
||||
Self::Transmitting(data) => data.locked,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current frequency if available.
|
||||
pub fn freq(&self) -> Option<Freq> {
|
||||
match self {
|
||||
Self::Ready(data) => Some(data.freq),
|
||||
Self::Transmitting(data) => Some(data.freq),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current mode if available.
|
||||
pub fn mode(&self) -> Option<&RigMode> {
|
||||
match self {
|
||||
Self::Ready(data) => Some(&data.mode),
|
||||
Self::Transmitting(data) => Some(&data.mode),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get rig info if available.
|
||||
pub fn rig_info(&self) -> Option<&RigInfo> {
|
||||
match self {
|
||||
Self::Initializing { rig_info } => rig_info.as_ref(),
|
||||
Self::PoweredOff { rig_info } => Some(rig_info),
|
||||
Self::Ready(data) => Some(&data.rig_info),
|
||||
Self::Transmitting(data) => Some(&data.rig_info),
|
||||
Self::Error { previous_state, .. } => previous_state.rig_info(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to RigStatus for compatibility with existing code.
|
||||
pub fn to_rig_status(&self) -> Option<RigStatus> {
|
||||
match self {
|
||||
Self::Ready(data) => Some(RigStatus {
|
||||
freq: data.freq,
|
||||
mode: data.mode.clone(),
|
||||
tx_en: false,
|
||||
vfo: data.vfo.clone(),
|
||||
tx: Some(RigTxStatus {
|
||||
power: Some(0),
|
||||
limit: data.tx_limit,
|
||||
swr: Some(0.0),
|
||||
alc: None,
|
||||
}),
|
||||
rx: data.rx.clone(),
|
||||
lock: Some(data.locked),
|
||||
}),
|
||||
Self::Transmitting(data) => Some(RigStatus {
|
||||
freq: data.freq,
|
||||
mode: data.mode.clone(),
|
||||
tx_en: true,
|
||||
vfo: data.vfo.clone(),
|
||||
tx: data.tx.clone(),
|
||||
rx: Some(RigRxStatus { sig: Some(0) }),
|
||||
lock: Some(data.locked),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The rig state machine that manages state transitions.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RigStateMachine {
|
||||
state: RigMachineState,
|
||||
transition_count: u64,
|
||||
last_transition: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Default for RigStateMachine {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RigStateMachine {
|
||||
/// Create a new state machine in the Disconnected state.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: RigMachineState::Disconnected,
|
||||
transition_count: 0,
|
||||
last_transition: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current state.
|
||||
pub fn state(&self) -> &RigMachineState {
|
||||
&self.state
|
||||
}
|
||||
|
||||
/// Get the number of state transitions that have occurred.
|
||||
pub fn transition_count(&self) -> u64 {
|
||||
self.transition_count
|
||||
}
|
||||
|
||||
/// Get the time since the last transition.
|
||||
pub fn time_in_state(&self) -> Option<Duration> {
|
||||
self.last_transition.map(|t| t.elapsed())
|
||||
}
|
||||
|
||||
/// Process an event and potentially transition to a new state.
|
||||
/// Returns true if a transition occurred.
|
||||
pub fn process_event(&mut self, event: RigEvent) -> bool {
|
||||
let new_state = self.next_state(event);
|
||||
if let Some(state) = new_state {
|
||||
self.state = state;
|
||||
self.transition_count += 1;
|
||||
self.last_transition = Some(Instant::now());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine the next state based on current state and event.
|
||||
fn next_state(&self, event: RigEvent) -> Option<RigMachineState> {
|
||||
match (&self.state, event) {
|
||||
// From Disconnected
|
||||
(RigMachineState::Disconnected, RigEvent::Connected) => {
|
||||
Some(RigMachineState::Connecting {
|
||||
started_at: Some(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
// From Connecting
|
||||
(RigMachineState::Connecting { .. }, RigEvent::Initialized) => {
|
||||
Some(RigMachineState::Initializing { rig_info: None })
|
||||
}
|
||||
|
||||
// From Initializing
|
||||
(RigMachineState::Initializing { rig_info }, RigEvent::PoweredOn) => {
|
||||
rig_info.as_ref().map(|info| {
|
||||
RigMachineState::Ready(ReadyStateData {
|
||||
rig_info: info.clone(),
|
||||
freq: Freq { hz: 0 },
|
||||
mode: RigMode::USB,
|
||||
vfo: None,
|
||||
rx: None,
|
||||
tx_limit: None,
|
||||
locked: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
(RigMachineState::Initializing { .. }, RigEvent::PoweredOff) => {
|
||||
// Stay in initializing, rig is off
|
||||
None
|
||||
}
|
||||
|
||||
// From PoweredOff
|
||||
(RigMachineState::PoweredOff { rig_info }, RigEvent::PoweredOn) => {
|
||||
Some(RigMachineState::Ready(ReadyStateData {
|
||||
rig_info: rig_info.clone(),
|
||||
freq: Freq { hz: 0 },
|
||||
mode: RigMode::USB,
|
||||
vfo: None,
|
||||
rx: None,
|
||||
tx_limit: None,
|
||||
locked: false,
|
||||
}))
|
||||
}
|
||||
|
||||
// From Ready
|
||||
(RigMachineState::Ready(data), RigEvent::PttOn) => {
|
||||
Some(RigMachineState::Transmitting(TransmittingStateData {
|
||||
rig_info: data.rig_info.clone(),
|
||||
freq: data.freq,
|
||||
mode: data.mode.clone(),
|
||||
vfo: data.vfo.clone(),
|
||||
tx: Some(RigTxStatus {
|
||||
power: None,
|
||||
limit: data.tx_limit,
|
||||
swr: None,
|
||||
alc: None,
|
||||
}),
|
||||
locked: data.locked,
|
||||
}))
|
||||
}
|
||||
(RigMachineState::Ready(data), RigEvent::PoweredOff) => {
|
||||
Some(RigMachineState::PoweredOff {
|
||||
rig_info: data.rig_info.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
// From Transmitting
|
||||
(RigMachineState::Transmitting(data), RigEvent::PttOff) => {
|
||||
Some(RigMachineState::Ready(ReadyStateData {
|
||||
rig_info: data.rig_info.clone(),
|
||||
freq: data.freq,
|
||||
mode: data.mode.clone(),
|
||||
vfo: data.vfo.clone(),
|
||||
rx: None,
|
||||
tx_limit: data.tx.as_ref().and_then(|t| t.limit),
|
||||
locked: data.locked,
|
||||
}))
|
||||
}
|
||||
(RigMachineState::Transmitting(data), RigEvent::PoweredOff) => {
|
||||
Some(RigMachineState::PoweredOff {
|
||||
rig_info: data.rig_info.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
// Error transitions (from any state)
|
||||
(current, RigEvent::Error(error)) => Some(RigMachineState::Error {
|
||||
error,
|
||||
previous_state: Box::new(current.clone()),
|
||||
}),
|
||||
|
||||
// Recovery from error
|
||||
(
|
||||
RigMachineState::Error {
|
||||
error,
|
||||
previous_state,
|
||||
},
|
||||
RigEvent::Recovered,
|
||||
) => {
|
||||
if error.recoverable {
|
||||
Some(*previous_state.clone())
|
||||
} else {
|
||||
Some(RigMachineState::Disconnected)
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect from any state
|
||||
(_, RigEvent::Disconnected) => Some(RigMachineState::Disconnected),
|
||||
|
||||
// Invalid transition - stay in current state
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Force set the state (for initialization or recovery).
|
||||
pub fn set_state(&mut self, state: RigMachineState) {
|
||||
self.state = state;
|
||||
self.transition_count += 1;
|
||||
self.last_transition = Some(Instant::now());
|
||||
}
|
||||
|
||||
/// Update Ready state data in place.
|
||||
pub fn update_ready_data<F>(&mut self, f: F)
|
||||
where
|
||||
F: FnOnce(&mut ReadyStateData),
|
||||
{
|
||||
if let RigMachineState::Ready(ref mut data) = self.state {
|
||||
f(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update Transmitting state data in place.
|
||||
pub fn update_transmitting_data<F>(&mut self, f: F)
|
||||
where
|
||||
F: FnOnce(&mut TransmittingStateData),
|
||||
{
|
||||
if let RigMachineState::Transmitting(ref mut data) = self.state {
|
||||
f(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn mock_rig_info() -> RigInfo {
|
||||
use crate::rig::{RigAccessMethod, RigCapabilities};
|
||||
|
||||
RigInfo {
|
||||
manufacturer: "Test".to_string(),
|
||||
model: "Mock".to_string(),
|
||||
revision: "1.0".to_string(),
|
||||
capabilities: RigCapabilities {
|
||||
supported_bands: vec![],
|
||||
supported_modes: vec![],
|
||||
num_vfos: 2,
|
||||
lock: false,
|
||||
lockable: true,
|
||||
attenuator: false,
|
||||
preamp: false,
|
||||
rit: false,
|
||||
rpt: false,
|
||||
split: false,
|
||||
},
|
||||
access: RigAccessMethod::Serial {
|
||||
path: "/dev/test".to_string(),
|
||||
baud: 9600,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initial_state() {
|
||||
let sm = RigStateMachine::new();
|
||||
assert!(matches!(sm.state(), RigMachineState::Disconnected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connect_transition() {
|
||||
let mut sm = RigStateMachine::new();
|
||||
assert!(sm.process_event(RigEvent::Connected));
|
||||
assert!(matches!(sm.state(), RigMachineState::Connecting { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_lifecycle() {
|
||||
let mut sm = RigStateMachine::new();
|
||||
|
||||
// Connect
|
||||
sm.process_event(RigEvent::Connected);
|
||||
assert!(matches!(sm.state(), RigMachineState::Connecting { .. }));
|
||||
|
||||
// Initialize
|
||||
sm.process_event(RigEvent::Initialized);
|
||||
assert!(matches!(sm.state(), RigMachineState::Initializing { .. }));
|
||||
|
||||
// Set rig info and power on
|
||||
sm.set_state(RigMachineState::Initializing {
|
||||
rig_info: Some(mock_rig_info()),
|
||||
});
|
||||
sm.process_event(RigEvent::PoweredOn);
|
||||
assert!(matches!(sm.state(), RigMachineState::Ready(_)));
|
||||
|
||||
// Transmit
|
||||
sm.process_event(RigEvent::PttOn);
|
||||
assert!(matches!(sm.state(), RigMachineState::Transmitting(_)));
|
||||
assert!(sm.state().is_transmitting());
|
||||
|
||||
// Back to ready
|
||||
sm.process_event(RigEvent::PttOff);
|
||||
assert!(matches!(sm.state(), RigMachineState::Ready(_)));
|
||||
|
||||
// Power off
|
||||
sm.process_event(RigEvent::PoweredOff);
|
||||
assert!(matches!(sm.state(), RigMachineState::PoweredOff { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_and_recovery() {
|
||||
let mut sm = RigStateMachine::new();
|
||||
sm.process_event(RigEvent::Connected);
|
||||
sm.process_event(RigEvent::Initialized);
|
||||
sm.set_state(RigMachineState::Initializing {
|
||||
rig_info: Some(mock_rig_info()),
|
||||
});
|
||||
sm.process_event(RigEvent::PoweredOn);
|
||||
|
||||
// Trigger error
|
||||
sm.process_event(RigEvent::Error(RigStateError::transient("Test error")));
|
||||
assert!(sm.state().is_error());
|
||||
|
||||
// Recover
|
||||
sm.process_event(RigEvent::Recovered);
|
||||
assert!(matches!(sm.state(), RigMachineState::Ready(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_transition() {
|
||||
let mut sm = RigStateMachine::new();
|
||||
|
||||
// Can't transmit from disconnected
|
||||
let transitioned = sm.process_event(RigEvent::PttOn);
|
||||
assert!(!transitioned);
|
||||
assert!(matches!(sm.state(), RigMachineState::Disconnected));
|
||||
}
|
||||
}
|
||||
+13
-12
@@ -5,7 +5,7 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::radio::freq::{Band, Freq};
|
||||
use crate::{DynResult, RigMode};
|
||||
@@ -15,28 +15,29 @@ pub type RigStatusFuture<'a> =
|
||||
Pin<Box<dyn Future<Output = DynResult<(Freq, RigMode, Option<RigVfo>)>> + Send + 'a>>;
|
||||
|
||||
pub mod command;
|
||||
pub mod controller;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod state;
|
||||
|
||||
/// How this backend communicates with the rig.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum RigAccessMethod {
|
||||
Serial { path: String, baud: u32 },
|
||||
Tcp { addr: String },
|
||||
}
|
||||
|
||||
/// Static info describing a rig backend.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RigInfo {
|
||||
pub manufacturer: &'static str,
|
||||
pub model: &'static str,
|
||||
pub revision: &'static str,
|
||||
pub manufacturer: String,
|
||||
pub model: String,
|
||||
pub revision: String,
|
||||
pub capabilities: RigCapabilities,
|
||||
pub access: RigAccessMethod,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RigCapabilities {
|
||||
pub supported_bands: Vec<Band>,
|
||||
pub supported_modes: Vec<RigMode>,
|
||||
@@ -99,7 +100,7 @@ pub trait RigCat: Rig + Send {
|
||||
}
|
||||
|
||||
/// Snapshot of a rig's status that every backend can expose.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RigStatus {
|
||||
pub freq: Freq,
|
||||
pub mode: RigMode,
|
||||
@@ -115,21 +116,21 @@ pub trait RigStatusProvider {
|
||||
fn status(&self) -> RigStatus;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RigVfo {
|
||||
pub entries: Vec<RigVfoEntry>,
|
||||
/// Index into `entries` for the active VFO, if known.
|
||||
pub active: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RigVfoEntry {
|
||||
pub name: String,
|
||||
pub freq: Freq,
|
||||
pub mode: Option<RigMode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RigTxStatus {
|
||||
pub power: Option<u8>,
|
||||
pub limit: Option<u8>,
|
||||
@@ -137,7 +138,7 @@ pub struct RigTxStatus {
|
||||
pub alc: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RigRxStatus {
|
||||
pub sig: Option<i32>,
|
||||
}
|
||||
|
||||
@@ -6,18 +6,82 @@ use serde::Serialize;
|
||||
|
||||
/// Error type returned by rig requests.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RigError(pub String);
|
||||
pub struct RigError {
|
||||
pub message: String,
|
||||
pub kind: RigErrorKind,
|
||||
}
|
||||
|
||||
/// Classification of rig errors for retry decisions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
pub enum RigErrorKind {
|
||||
/// Temporary failure that may succeed on retry (timeout, busy).
|
||||
Transient,
|
||||
/// Permanent failure that won't be fixed by retrying.
|
||||
Permanent,
|
||||
}
|
||||
|
||||
pub type RigResult<T> = Result<T, RigError>;
|
||||
|
||||
impl RigError {
|
||||
/// Create a new transient error.
|
||||
pub fn transient(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
kind: RigErrorKind::Transient,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new permanent error.
|
||||
pub fn permanent(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
kind: RigErrorKind::Permanent,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a timeout error (transient).
|
||||
pub fn timeout() -> Self {
|
||||
Self::transient("operation timed out")
|
||||
}
|
||||
|
||||
/// Create a not supported error (permanent).
|
||||
pub fn not_supported(operation: &str) -> Self {
|
||||
Self::permanent(format!("operation not supported: {}", operation))
|
||||
}
|
||||
|
||||
/// Create a communication error (transient).
|
||||
pub fn communication(message: impl Into<String>) -> Self {
|
||||
Self::transient(message)
|
||||
}
|
||||
|
||||
/// Create an invalid state error (permanent).
|
||||
pub fn invalid_state(message: impl Into<String>) -> Self {
|
||||
Self::permanent(message)
|
||||
}
|
||||
|
||||
/// Check if this error is transient and may succeed on retry.
|
||||
pub fn is_transient(&self) -> bool {
|
||||
self.kind == RigErrorKind::Transient
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for RigError {
|
||||
fn from(value: String) -> Self {
|
||||
RigError(value)
|
||||
// Default to transient for backwards compatibility
|
||||
RigError::transient(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for RigError {
|
||||
fn from(value: &str) -> Self {
|
||||
RigError(value.to_string())
|
||||
RigError::transient(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RigError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RigError {}
|
||||
|
||||
@@ -83,7 +83,7 @@ impl RigState {
|
||||
}
|
||||
|
||||
/// Read-only projection of state shared with clients.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RigSnapshot {
|
||||
pub info: RigInfo,
|
||||
pub status: RigStatus,
|
||||
|
||||
@@ -16,8 +16,10 @@ use trx_core::{ClientResponse, RigCommand, RigMode, RigRequest, RigSnapshot, Rig
|
||||
|
||||
use crate::server::status;
|
||||
|
||||
const FAVICON_BYTES: &[u8] =
|
||||
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/trx-favicon.png"));
|
||||
const FAVICON_BYTES: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/assets/trx-favicon.png"
|
||||
));
|
||||
const LOGO_BYTES: &[u8] =
|
||||
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/trx-logo.png"));
|
||||
|
||||
@@ -218,7 +220,7 @@ async fn send_command(
|
||||
Ok(Err(err)) => Ok(HttpResponse::BadRequest().json(ClientResponse {
|
||||
success: false,
|
||||
state: None,
|
||||
error: Some(err.0),
|
||||
error: Some(err.message),
|
||||
})),
|
||||
Err(e) => Err(actix_web::error::ErrorInternalServerError(format!(
|
||||
"rig response channel error: {e:?}"
|
||||
@@ -262,9 +264,9 @@ impl Default for RigInfoPlaceholder {
|
||||
impl From<RigInfoPlaceholder> for RigInfo {
|
||||
fn from(_: RigInfoPlaceholder) -> Self {
|
||||
RigInfo {
|
||||
manufacturer: "Unknown",
|
||||
model: "Rig",
|
||||
revision: "",
|
||||
manufacturer: "Unknown".to_string(),
|
||||
model: "Rig".to_string(),
|
||||
revision: "".to_string(),
|
||||
capabilities: RigCapabilities {
|
||||
supported_bands: vec![],
|
||||
supported_modes: vec![],
|
||||
|
||||
Reference in New Issue
Block a user