rig: integrate controller and rig task updates
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
//! Rig event notification system.
|
||||
//!
|
||||
//! This module provides typed event notifications for rig state changes,
|
||||
//! allowing frontends and other components to react to specific events.
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::radio::freq::Freq;
|
||||
use crate::rig::state::RigMode;
|
||||
use crate::rig::{RigRxStatus, RigTxStatus};
|
||||
|
||||
use super::machine::RigMachineState;
|
||||
|
||||
/// Unique identifier for a registered listener.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ListenerId(u64);
|
||||
|
||||
impl ListenerId {
|
||||
fn new() -> Self {
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
Self(COUNTER.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for components that want to receive rig events.
|
||||
///
|
||||
/// Implementors receive typed notifications when rig state changes.
|
||||
/// All methods have default no-op implementations, so listeners can
|
||||
/// selectively override only the events they care about.
|
||||
pub trait RigListener: Send + Sync {
|
||||
/// Called when the operating frequency changes.
|
||||
fn on_frequency_change(&self, _old: Option<Freq>, _new: Freq) {}
|
||||
|
||||
/// Called when the operating mode changes.
|
||||
fn on_mode_change(&self, _old: Option<&RigMode>, _new: &RigMode) {}
|
||||
|
||||
/// Called when PTT state changes.
|
||||
fn on_ptt_change(&self, _transmitting: bool) {}
|
||||
|
||||
/// Called when the rig state machine transitions.
|
||||
fn on_state_change(&self, _old: &RigMachineState, _new: &RigMachineState) {}
|
||||
|
||||
/// Called when meter readings are updated.
|
||||
fn on_meter_update(&self, _rx: Option<&RigRxStatus>, _tx: Option<&RigTxStatus>) {}
|
||||
|
||||
/// Called when the panel lock state changes.
|
||||
fn on_lock_change(&self, _locked: bool) {}
|
||||
|
||||
/// Called when the rig powers on or off.
|
||||
fn on_power_change(&self, _powered: bool) {}
|
||||
}
|
||||
|
||||
/// Manages registered listeners and dispatches events.
|
||||
pub struct RigEventEmitter {
|
||||
listeners: Vec<(ListenerId, Arc<dyn RigListener>)>,
|
||||
}
|
||||
|
||||
impl Default for RigEventEmitter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RigEventEmitter {
|
||||
/// Create a new event emitter with no listeners.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
listeners: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a listener to receive events.
|
||||
/// Returns an ID that can be used to unregister the listener.
|
||||
pub fn register(&mut self, listener: Arc<dyn RigListener>) -> ListenerId {
|
||||
let id = ListenerId::new();
|
||||
self.listeners.push((id, listener));
|
||||
id
|
||||
}
|
||||
|
||||
/// Unregister a listener by its ID.
|
||||
pub fn unregister(&mut self, id: ListenerId) {
|
||||
self.listeners.retain(|(lid, _)| *lid != id);
|
||||
}
|
||||
|
||||
/// Get the number of registered listeners.
|
||||
pub fn listener_count(&self) -> usize {
|
||||
self.listeners.len()
|
||||
}
|
||||
|
||||
/// Notify all listeners of a frequency change.
|
||||
pub fn notify_frequency_change(&self, old: Option<Freq>, new: Freq) {
|
||||
for (_, listener) in &self.listeners {
|
||||
listener.on_frequency_change(old, new);
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify all listeners of a mode change.
|
||||
pub fn notify_mode_change(&self, old: Option<&RigMode>, new: &RigMode) {
|
||||
for (_, listener) in &self.listeners {
|
||||
listener.on_mode_change(old, new);
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify all listeners of a PTT state change.
|
||||
pub fn notify_ptt_change(&self, transmitting: bool) {
|
||||
for (_, listener) in &self.listeners {
|
||||
listener.on_ptt_change(transmitting);
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify all listeners of a state machine transition.
|
||||
pub fn notify_state_change(&self, old: &RigMachineState, new: &RigMachineState) {
|
||||
for (_, listener) in &self.listeners {
|
||||
listener.on_state_change(old, new);
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify all listeners of updated meter readings.
|
||||
pub fn notify_meter_update(&self, rx: Option<&RigRxStatus>, tx: Option<&RigTxStatus>) {
|
||||
for (_, listener) in &self.listeners {
|
||||
listener.on_meter_update(rx, tx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify all listeners of a lock state change.
|
||||
pub fn notify_lock_change(&self, locked: bool) {
|
||||
for (_, listener) in &self.listeners {
|
||||
listener.on_lock_change(locked);
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify all listeners of a power state change.
|
||||
pub fn notify_power_change(&self, powered: bool) {
|
||||
for (_, listener) in &self.listeners {
|
||||
listener.on_power_change(powered);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
struct TestListener {
|
||||
freq_changed: AtomicBool,
|
||||
ptt_changed: AtomicBool,
|
||||
}
|
||||
|
||||
impl TestListener {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
freq_changed: AtomicBool::new(false),
|
||||
ptt_changed: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RigListener for TestListener {
|
||||
fn on_frequency_change(&self, _old: Option<Freq>, _new: Freq) {
|
||||
self.freq_changed.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn on_ptt_change(&self, _transmitting: bool) {
|
||||
self.ptt_changed.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_and_notify() {
|
||||
let mut emitter = RigEventEmitter::new();
|
||||
let listener = Arc::new(TestListener::new());
|
||||
let id = emitter.register(listener.clone());
|
||||
|
||||
assert_eq!(emitter.listener_count(), 1);
|
||||
|
||||
emitter.notify_frequency_change(None, Freq { hz: 14_200_000 });
|
||||
assert!(listener.freq_changed.load(Ordering::Relaxed));
|
||||
assert!(!listener.ptt_changed.load(Ordering::Relaxed));
|
||||
|
||||
emitter.notify_ptt_change(true);
|
||||
assert!(listener.ptt_changed.load(Ordering::Relaxed));
|
||||
|
||||
emitter.unregister(id);
|
||||
assert_eq!(emitter.listener_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_listeners() {
|
||||
let mut emitter = RigEventEmitter::new();
|
||||
let listener1 = Arc::new(TestListener::new());
|
||||
let listener2 = Arc::new(TestListener::new());
|
||||
|
||||
emitter.register(listener1.clone());
|
||||
emitter.register(listener2.clone());
|
||||
|
||||
emitter.notify_frequency_change(Some(Freq { hz: 7_000_000 }), Freq { hz: 14_200_000 });
|
||||
|
||||
assert!(listener1.freq_changed.load(Ordering::Relaxed));
|
||||
assert!(listener2.freq_changed.load(Ordering::Relaxed));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
//! Command executor implementation that bridges to RigCat.
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use crate::radio::freq::Freq;
|
||||
use crate::rig::state::RigMode;
|
||||
use crate::rig::RigCat;
|
||||
use crate::DynResult;
|
||||
|
||||
use super::handlers::CommandExecutor;
|
||||
|
||||
/// Executor that delegates to a RigCat implementation.
|
||||
pub struct RigCatExecutor<'a> {
|
||||
rig: &'a mut dyn RigCat,
|
||||
}
|
||||
|
||||
impl<'a> RigCatExecutor<'a> {
|
||||
pub fn new(rig: &'a mut dyn RigCat) -> Self {
|
||||
Self { rig }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CommandExecutor for RigCatExecutor<'a> {
|
||||
fn set_freq<'b>(
|
||||
&'b mut self,
|
||||
freq: Freq,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'b>> {
|
||||
self.rig.set_freq(freq)
|
||||
}
|
||||
|
||||
fn set_mode<'b>(
|
||||
&'b mut self,
|
||||
mode: RigMode,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'b>> {
|
||||
self.rig.set_mode(mode)
|
||||
}
|
||||
|
||||
fn set_ptt<'b>(
|
||||
&'b mut self,
|
||||
ptt: bool,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'b>> {
|
||||
self.rig.set_ptt(ptt)
|
||||
}
|
||||
|
||||
fn power_on<'b>(&'b mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'b>> {
|
||||
self.rig.power_on()
|
||||
}
|
||||
|
||||
fn power_off<'b>(&'b mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'b>> {
|
||||
self.rig.power_off()
|
||||
}
|
||||
|
||||
fn toggle_vfo<'b>(&'b mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'b>> {
|
||||
self.rig.toggle_vfo()
|
||||
}
|
||||
|
||||
fn lock<'b>(&'b mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'b>> {
|
||||
self.rig.lock()
|
||||
}
|
||||
|
||||
fn unlock<'b>(&'b mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'b>> {
|
||||
self.rig.unlock()
|
||||
}
|
||||
|
||||
fn get_tx_limit<'b>(&'b mut self) -> Pin<Box<dyn Future<Output = DynResult<u8>> + Send + 'b>> {
|
||||
self.rig.get_tx_limit()
|
||||
}
|
||||
|
||||
fn set_tx_limit<'b>(
|
||||
&'b mut self,
|
||||
limit: u8,
|
||||
) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'b>> {
|
||||
self.rig.set_tx_limit(limit)
|
||||
}
|
||||
|
||||
fn refresh_state<'b>(&'b mut self) -> Pin<Box<dyn Future<Output = DynResult<()>> + Send + 'b>> {
|
||||
// This is a no-op for the executor - the controller handles state refresh
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
//! Rig controller components.
|
||||
//!
|
||||
//! This module contains the core control logic for managing rig state,
|
||||
//! handling commands, emitting events, and configuring operational policies.
|
||||
|
||||
pub mod events;
|
||||
pub mod executor;
|
||||
pub mod handlers;
|
||||
pub mod machine;
|
||||
pub mod policies;
|
||||
|
||||
pub use events::{ListenerId, RigEventEmitter, RigListener};
|
||||
pub use executor::RigCatExecutor;
|
||||
pub use handlers::{
|
||||
command_from_rig_command, CommandContext, CommandExecutor, CommandResult, RigCommandHandler,
|
||||
ValidationResult,
|
||||
};
|
||||
pub use machine::{
|
||||
ReadyStateData, RigEvent, RigMachineState, RigStateError, RigStateMachine,
|
||||
TransmittingStateData,
|
||||
};
|
||||
pub use policies::{AdaptivePolling, ExponentialBackoff, PollingPolicy, RetryPolicy};
|
||||
@@ -0,0 +1,288 @@
|
||||
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
//! Rig operational policies for retry and polling behavior.
|
||||
//!
|
||||
//! This module provides configurable policies that control how the rig
|
||||
//! controller handles retries after failures and polling intervals.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::rig::response::RigError;
|
||||
|
||||
/// Policy for retrying failed operations.
|
||||
pub trait RetryPolicy: Send + Sync {
|
||||
/// Determine if the operation should be retried.
|
||||
fn should_retry(&self, attempt: u32, error: &RigError) -> bool;
|
||||
|
||||
/// Get the delay before the next retry attempt.
|
||||
fn delay(&self, attempt: u32) -> Duration;
|
||||
|
||||
/// Get the maximum number of attempts allowed.
|
||||
fn max_attempts(&self) -> u32;
|
||||
}
|
||||
|
||||
/// Exponential backoff retry policy.
|
||||
///
|
||||
/// Delays increase exponentially with each retry attempt,
|
||||
/// up to a configured maximum delay.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExponentialBackoff {
|
||||
max_attempts: u32,
|
||||
base_delay: Duration,
|
||||
max_delay: Duration,
|
||||
}
|
||||
|
||||
impl ExponentialBackoff {
|
||||
/// Create a new exponential backoff policy.
|
||||
pub fn new(max_attempts: u32, base_delay: Duration, max_delay: Duration) -> Self {
|
||||
Self {
|
||||
max_attempts,
|
||||
base_delay,
|
||||
max_delay,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a policy with sensible defaults for rig communication.
|
||||
pub fn default_rig() -> Self {
|
||||
Self {
|
||||
max_attempts: 3,
|
||||
base_delay: Duration::from_millis(100),
|
||||
max_delay: Duration::from_secs(2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ExponentialBackoff {
|
||||
fn default() -> Self {
|
||||
Self::default_rig()
|
||||
}
|
||||
}
|
||||
|
||||
impl RetryPolicy for ExponentialBackoff {
|
||||
fn should_retry(&self, attempt: u32, error: &RigError) -> bool {
|
||||
if attempt >= self.max_attempts {
|
||||
return false;
|
||||
}
|
||||
// Only retry transient errors
|
||||
error.is_transient()
|
||||
}
|
||||
|
||||
fn delay(&self, attempt: u32) -> Duration {
|
||||
let multiplier = 2u32.saturating_pow(attempt);
|
||||
let delay = self.base_delay.saturating_mul(multiplier);
|
||||
delay.min(self.max_delay)
|
||||
}
|
||||
|
||||
fn max_attempts(&self) -> u32 {
|
||||
self.max_attempts
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixed delay retry policy.
|
||||
///
|
||||
/// Uses a constant delay between retry attempts.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FixedDelay {
|
||||
max_attempts: u32,
|
||||
delay: Duration,
|
||||
}
|
||||
|
||||
impl FixedDelay {
|
||||
/// Create a new fixed delay policy.
|
||||
pub fn new(max_attempts: u32, delay: Duration) -> Self {
|
||||
Self {
|
||||
max_attempts,
|
||||
delay,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RetryPolicy for FixedDelay {
|
||||
fn should_retry(&self, attempt: u32, error: &RigError) -> bool {
|
||||
attempt < self.max_attempts && error.is_transient()
|
||||
}
|
||||
|
||||
fn delay(&self, _attempt: u32) -> Duration {
|
||||
self.delay
|
||||
}
|
||||
|
||||
fn max_attempts(&self) -> u32 {
|
||||
self.max_attempts
|
||||
}
|
||||
}
|
||||
|
||||
/// No retry policy - operations fail immediately.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct NoRetry;
|
||||
|
||||
impl RetryPolicy for NoRetry {
|
||||
fn should_retry(&self, _attempt: u32, _error: &RigError) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn delay(&self, _attempt: u32) -> Duration {
|
||||
Duration::ZERO
|
||||
}
|
||||
|
||||
fn max_attempts(&self) -> u32 {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
/// Policy for polling the rig for status updates.
|
||||
pub trait PollingPolicy: Send + Sync {
|
||||
/// Get the interval between polls.
|
||||
fn interval(&self, transmitting: bool) -> Duration;
|
||||
|
||||
/// Determine if polling should occur given the current state.
|
||||
fn should_poll(&self, transmitting: bool) -> bool;
|
||||
}
|
||||
|
||||
/// Adaptive polling policy.
|
||||
///
|
||||
/// Uses different intervals depending on whether the rig is transmitting.
|
||||
/// Polls more frequently during TX to track power/SWR meters.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AdaptivePolling {
|
||||
idle_interval: Duration,
|
||||
active_interval: Duration,
|
||||
}
|
||||
|
||||
impl AdaptivePolling {
|
||||
/// Create a new adaptive polling policy.
|
||||
pub fn new(idle_interval: Duration, active_interval: Duration) -> Self {
|
||||
Self {
|
||||
idle_interval,
|
||||
active_interval,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a policy with sensible defaults for rig polling.
|
||||
pub fn default_rig() -> Self {
|
||||
Self {
|
||||
idle_interval: Duration::from_millis(500),
|
||||
active_interval: Duration::from_millis(100),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AdaptivePolling {
|
||||
fn default() -> Self {
|
||||
Self::default_rig()
|
||||
}
|
||||
}
|
||||
|
||||
impl PollingPolicy for AdaptivePolling {
|
||||
fn interval(&self, transmitting: bool) -> Duration {
|
||||
if transmitting {
|
||||
self.active_interval
|
||||
} else {
|
||||
self.idle_interval
|
||||
}
|
||||
}
|
||||
|
||||
fn should_poll(&self, _transmitting: bool) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixed polling policy.
|
||||
///
|
||||
/// Uses a constant interval regardless of rig state.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FixedPolling {
|
||||
interval: Duration,
|
||||
}
|
||||
|
||||
impl FixedPolling {
|
||||
/// Create a new fixed polling policy.
|
||||
pub fn new(interval: Duration) -> Self {
|
||||
Self { interval }
|
||||
}
|
||||
}
|
||||
|
||||
impl PollingPolicy for FixedPolling {
|
||||
fn interval(&self, _transmitting: bool) -> Duration {
|
||||
self.interval
|
||||
}
|
||||
|
||||
fn should_poll(&self, _transmitting: bool) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// No polling policy - disables automatic polling.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct NoPolling;
|
||||
|
||||
impl PollingPolicy for NoPolling {
|
||||
fn interval(&self, _transmitting: bool) -> Duration {
|
||||
Duration::MAX
|
||||
}
|
||||
|
||||
fn should_poll(&self, _transmitting: bool) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_exponential_backoff_delays() {
|
||||
let policy = ExponentialBackoff::new(5, Duration::from_millis(100), Duration::from_secs(1));
|
||||
|
||||
assert_eq!(policy.delay(0), Duration::from_millis(100));
|
||||
assert_eq!(policy.delay(1), Duration::from_millis(200));
|
||||
assert_eq!(policy.delay(2), Duration::from_millis(400));
|
||||
assert_eq!(policy.delay(3), Duration::from_millis(800));
|
||||
// Should cap at max_delay
|
||||
assert_eq!(policy.delay(4), Duration::from_secs(1));
|
||||
assert_eq!(policy.delay(5), Duration::from_secs(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exponential_backoff_should_retry() {
|
||||
let policy = ExponentialBackoff::new(3, Duration::from_millis(100), Duration::from_secs(1));
|
||||
|
||||
let transient = RigError::timeout();
|
||||
let fatal = RigError::not_supported("test");
|
||||
|
||||
assert!(policy.should_retry(0, &transient));
|
||||
assert!(policy.should_retry(1, &transient));
|
||||
assert!(policy.should_retry(2, &transient));
|
||||
assert!(!policy.should_retry(3, &transient)); // exceeded max attempts
|
||||
|
||||
assert!(!policy.should_retry(0, &fatal)); // not transient
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixed_delay() {
|
||||
let policy = FixedDelay::new(3, Duration::from_millis(500));
|
||||
|
||||
assert_eq!(policy.delay(0), Duration::from_millis(500));
|
||||
assert_eq!(policy.delay(1), Duration::from_millis(500));
|
||||
assert_eq!(policy.delay(5), Duration::from_millis(500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_polling() {
|
||||
let policy = AdaptivePolling::new(Duration::from_millis(500), Duration::from_millis(100));
|
||||
|
||||
assert_eq!(policy.interval(false), Duration::from_millis(500));
|
||||
assert_eq!(policy.interval(true), Duration::from_millis(100));
|
||||
assert!(policy.should_poll(false));
|
||||
assert!(policy.should_poll(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_polling() {
|
||||
let policy = NoPolling;
|
||||
|
||||
assert!(!policy.should_poll(false));
|
||||
assert!(!policy.should_poll(true));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user