rig: integrate controller and rig task updates

This commit is contained in:
2026-01-18 09:20:10 +01:00
parent 1be08b245c
commit a941c77039
5 changed files with 1336 additions and 0 deletions
+207
View File
@@ -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(()) })
}
}
+26
View File
@@ -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};
+288
View File
@@ -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));
}
}