wireband-edge 0.4.1

Lightweight Wire.Band client — semantic data middleware for any domain (IoT, AI/ML, DeFi, legal, geospatial, supply chain, and more)
Documentation
//! Hardware and software watchdog.
//!
//! Kicks `/dev/watchdog` on Linux at a configurable interval and emits
//! [`EDGE_WDT_KICK`] to the Wire.Band backend so fleet dashboards can
//! confirm device liveness without a separate heartbeat mechanism.
//!
//! On non-Linux targets the hardware kick is a no-op; the software event
//! is always emitted.

use std::time::{Duration, SystemTime, UNIX_EPOCH};

use tokio::time;
use tracing::{debug, info, warn};

use crate::client::WireBandClient;
use crate::frame;
use crate::symbols::EDGE_WDT_KICK;

fn unix_ts() -> f64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs_f64()
}

/// Watchdog: kicks hardware + emits `EDGE_WDT_KICK` to the backend.
///
/// # Example
///
/// ```ignore
/// use std::time::Duration;
/// use wireband_edge::agent::Watchdog;
///
/// tokio::spawn(Watchdog::new(Duration::from_secs(30)).run(client.clone()));
/// ```
pub struct Watchdog {
    interval:    Duration,
    device_path: String,
}

impl Watchdog {
    /// Create a watchdog that kicks every `interval`.
    pub fn new(interval: Duration) -> Self {
        Self {
            interval,
            device_path: "/dev/watchdog".to_string(),
        }
    }

    /// Override the watchdog device path (default: `/dev/watchdog`).
    pub fn with_device(mut self, path: impl Into<String>) -> Self {
        self.device_path = path.into();
        self
    }

    /// Run the watchdog loop — kicks periodically until the task is cancelled.
    ///
    /// Spawn this as a background task:
    /// ```ignore
    /// tokio::spawn(watchdog.run(client.clone()));
    /// ```
    pub async fn run(self, client: WireBandClient) {
        info!(
            interval_ms = self.interval.as_millis(),
            device = %self.device_path,
            "Watchdog started"
        );

        let mut ticker = time::interval(self.interval);
        loop {
            ticker.tick().await;
            self.kick(&client).await;
        }
    }

    // -----------------------------------------------------------------------
    // Internal
    // -----------------------------------------------------------------------

    async fn kick(&self, client: &WireBandClient) {
        // Hardware kick — Linux only
        #[cfg(target_os = "linux")]
        {
            use std::io::Write;
            match std::fs::OpenOptions::new().write(true).open(&self.device_path) {
                Ok(mut f) => {
                    if let Err(e) = f.write_all(b"1") {
                        warn!(device = %self.device_path, err = %e, "Watchdog kick failed");
                    } else {
                        debug!(device = %self.device_path, "Hardware watchdog kicked");
                    }
                }
                Err(e) => {
                    // /dev/watchdog may not exist in dev/test environments
                    debug!(device = %self.device_path, err = %e, "Hardware watchdog not available");
                }
            }
        }

        // Software event — always emitted
        let ts    = unix_ts();
        let topic = "watchdog/kick".to_string();
        let data  = serde_json::json!({ "ts": ts });
        let encoded = frame::encode(EDGE_WDT_KICK, &topic, &data);
        client.buffer_event(topic, EDGE_WDT_KICK, encoded, ts).await;
        debug!("Watchdog kick event buffered");
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_device_path() {
        let w = Watchdog::new(Duration::from_secs(30));
        assert_eq!(w.device_path, "/dev/watchdog");
    }

    #[test]
    fn custom_device_path() {
        let w = Watchdog::new(Duration::from_secs(10))
            .with_device("/dev/watchdog0");
        assert_eq!(w.device_path, "/dev/watchdog0");
    }
}