Gpyes integration (#11)

* Introduce core modules: device management, bus communication, and discovery protocol. Adds system device interface, virtual hardware bus, and device discovery logic. Includes tests for all components.

* improve map
- Fix typos in variable and function names (`vessle` to `vessel`).
- Add `update_vessel_data_with_gps` function to enable GPS integration for vessel data updates.
- Integrate real GPS data into vessel systems and UI components (speed, heading, etc.).
- Initialize speed gauge display at 0 kts.
- Include `useEffect` in `MapNext` to log and potentially handle `vesselPosition` changes.

**Add compass heading update system using GPS heading data.**

- Remove `UserLocationMarker` component and related code from `MapNext.tsx`
- Simplify logic for layer selection and navigation within `App.tsx`
- Replace map style 'Bathymetry' with 'OSM' in layer options

improve map

* update image

---------

Co-authored-by: geoffsee <>
This commit is contained in:
Geoff Seemueller
2025-07-20 15:51:33 -04:00
committed by GitHub
parent 2311f43d97
commit e029ef48fc
28 changed files with 4557 additions and 207 deletions

338
crates/hardware/src/bus.rs Normal file
View File

@@ -0,0 +1,338 @@
//! Virtual Hardware Bus Module
//!
//! Provides a communication infrastructure for virtual hardware devices
use crate::{HardwareError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tracing::{debug, error, info, warn};
use uuid::Uuid;
/// Unique address for devices on the hardware bus
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BusAddress {
pub id: Uuid,
pub name: String,
}
impl BusAddress {
/// Create a new bus address with a generated UUID
pub fn new(name: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
name: name.into(),
}
}
/// Create a bus address with a specific UUID
pub fn with_id(id: Uuid, name: impl Into<String>) -> Self {
Self {
id,
name: name.into(),
}
}
}
/// Message types that can be sent over the hardware bus
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BusMessage {
/// Data message with payload
Data {
from: BusAddress,
to: BusAddress,
payload: Vec<u8>,
message_id: Uuid,
},
/// Control message for bus management
Control {
from: BusAddress,
command: ControlCommand,
message_id: Uuid,
},
/// Broadcast message to all devices
Broadcast {
from: BusAddress,
payload: Vec<u8>,
message_id: Uuid,
},
/// Acknowledgment message
Ack {
to: BusAddress,
original_message_id: Uuid,
message_id: Uuid,
},
}
/// Control commands for bus management
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ControlCommand {
/// Register a device on the bus
Register { address: BusAddress },
/// Unregister a device from the bus
Unregister { address: BusAddress },
/// Ping a device
Ping { target: BusAddress },
/// Pong response to ping
Pong { from: BusAddress },
/// Request device list
ListDevices,
/// Response with device list
DeviceList { devices: Vec<BusAddress> },
}
impl BusMessage {
/// Get the message ID
pub fn message_id(&self) -> Uuid {
match self {
BusMessage::Data { message_id, .. } => *message_id,
BusMessage::Control { message_id, .. } => *message_id,
BusMessage::Broadcast { message_id, .. } => *message_id,
BusMessage::Ack { message_id, .. } => *message_id,
}
}
/// Get the sender address if available
pub fn from(&self) -> Option<&BusAddress> {
match self {
BusMessage::Data { from, .. } => Some(from),
BusMessage::Control { from, .. } => Some(from),
BusMessage::Broadcast { from, .. } => Some(from),
BusMessage::Ack { .. } => None,
}
}
}
/// Device connection handle for the hardware bus
pub struct DeviceConnection {
pub address: BusAddress,
pub sender: mpsc::UnboundedSender<BusMessage>,
pub receiver: mpsc::UnboundedReceiver<BusMessage>,
}
/// Virtual Hardware Bus implementation
pub struct HardwareBus {
devices: Arc<RwLock<HashMap<BusAddress, mpsc::UnboundedSender<BusMessage>>>>,
message_log: Arc<RwLock<Vec<BusMessage>>>,
}
impl Default for HardwareBus {
fn default() -> Self {
Self::new()
}
}
impl HardwareBus {
/// Create a new hardware bus
pub fn new() -> Self {
Self {
devices: Arc::new(RwLock::new(HashMap::new())),
message_log: Arc::new(RwLock::new(Vec::new())),
}
}
/// Connect a device to the bus
pub async fn connect_device(&self, address: BusAddress) -> Result<DeviceConnection> {
let (tx, rx) = mpsc::unbounded_channel();
{
let mut devices = self.devices.write().await;
if devices.contains_key(&address) {
return Err(HardwareError::generic(format!(
"Device {} already connected", address.name
)));
}
devices.insert(address.clone(), tx.clone());
}
info!("Device {} connected to bus", address.name);
// Send registration message to all other devices
let register_msg = BusMessage::Control {
from: address.clone(),
command: ControlCommand::Register {
address: address.clone(),
},
message_id: Uuid::new_v4(),
};
self.broadcast_message(register_msg).await?;
Ok(DeviceConnection {
address,
sender: tx,
receiver: rx,
})
}
/// Disconnect a device from the bus
pub async fn disconnect_device(&self, address: &BusAddress) -> Result<()> {
{
let mut devices = self.devices.write().await;
devices.remove(address);
}
info!("Device {} disconnected from bus", address.name);
// Send unregistration message to all other devices
let unregister_msg = BusMessage::Control {
from: address.clone(),
command: ControlCommand::Unregister {
address: address.clone(),
},
message_id: Uuid::new_v4(),
};
self.broadcast_message(unregister_msg).await?;
Ok(())
}
/// Send a message to a specific device
pub async fn send_message(&self, message: BusMessage) -> Result<()> {
// Log the message
{
let mut log = self.message_log.write().await;
log.push(message.clone());
}
match &message {
BusMessage::Data { to, .. } => {
let devices = self.devices.read().await;
if let Some(sender) = devices.get(to) {
sender.send(message).map_err(|_| {
HardwareError::bus_communication("Failed to send message to device")
})?;
} else {
return Err(HardwareError::device_not_found(&to.name));
}
}
BusMessage::Broadcast { .. } => {
self.broadcast_message(message).await?;
}
BusMessage::Control { .. } => {
self.broadcast_message(message).await?;
}
BusMessage::Ack { to, .. } => {
let devices = self.devices.read().await;
if let Some(sender) = devices.get(to) {
sender.send(message).map_err(|_| {
HardwareError::bus_communication("Failed to send ACK to device")
})?;
} else {
warn!("Attempted to send ACK to unknown device: {}", to.name);
}
}
}
Ok(())
}
/// Broadcast a message to all connected devices
async fn broadcast_message(&self, message: BusMessage) -> Result<()> {
let devices = self.devices.read().await;
let sender_address = message.from();
for (address, sender) in devices.iter() {
// Don't send message back to sender
if let Some(from) = sender_address {
if address == from {
continue;
}
}
if let Err(_) = sender.send(message.clone()) {
error!("Failed to broadcast message to device: {}", address.name);
}
}
Ok(())
}
/// Get list of connected devices
pub async fn get_connected_devices(&self) -> Vec<BusAddress> {
let devices = self.devices.read().await;
devices.keys().cloned().collect()
}
/// Get message history
pub async fn get_message_history(&self) -> Vec<BusMessage> {
let log = self.message_log.read().await;
log.clone()
}
/// Clear message history
pub async fn clear_message_history(&self) {
let mut log = self.message_log.write().await;
log.clear();
}
/// Check if a device is connected
pub async fn is_device_connected(&self, address: &BusAddress) -> bool {
let devices = self.devices.read().await;
devices.contains_key(address)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio_test;
#[tokio::test]
async fn test_bus_creation() {
let bus = HardwareBus::new();
assert_eq!(bus.get_connected_devices().await.len(), 0);
}
#[tokio::test]
async fn test_device_connection() {
let bus = HardwareBus::new();
let address = BusAddress::new("test_device");
let connection = bus.connect_device(address.clone()).await.unwrap();
assert_eq!(connection.address, address);
assert!(bus.is_device_connected(&address).await);
}
#[tokio::test]
async fn test_device_disconnection() {
let bus = HardwareBus::new();
let address = BusAddress::new("test_device");
let _connection = bus.connect_device(address.clone()).await.unwrap();
assert!(bus.is_device_connected(&address).await);
bus.disconnect_device(&address).await.unwrap();
assert!(!bus.is_device_connected(&address).await);
}
#[tokio::test]
async fn test_message_sending() {
let bus = HardwareBus::new();
let addr1 = BusAddress::new("device1");
let addr2 = BusAddress::new("device2");
let mut conn1 = bus.connect_device(addr1.clone()).await.unwrap();
let _conn2 = bus.connect_device(addr2.clone()).await.unwrap();
let message = BusMessage::Data {
from: addr2.clone(),
to: addr1.clone(),
payload: b"test data".to_vec(),
message_id: Uuid::new_v4(),
};
bus.send_message(message.clone()).await.unwrap();
// Check if message was received
let received = conn1.receiver.recv().await.unwrap();
match received {
BusMessage::Data { payload, .. } => {
assert_eq!(payload, b"test data");
}
_ => panic!("Expected data message"),
}
}
}

View File

@@ -0,0 +1,430 @@
//! System Device Module
//!
//! Defines the interface and behavior for virtual hardware devices
use crate::{BusAddress, BusMessage, HardwareError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{Duration, SystemTime};
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
use uuid::Uuid;
/// Device capabilities that can be advertised
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DeviceCapability {
/// GPS positioning capability
Gps,
/// Radar detection capability
Radar,
/// AIS (Automatic Identification System) capability
Ais,
/// Engine monitoring capability
Engine,
/// Navigation capability
Navigation,
/// Communication capability
Communication,
/// Sensor data capability
Sensor,
/// Custom capability with name
Custom(String),
}
impl DeviceCapability {
/// Get the capability name as a string
pub fn name(&self) -> &str {
match self {
DeviceCapability::Gps => "GPS",
DeviceCapability::Radar => "Radar",
DeviceCapability::Ais => "AIS",
DeviceCapability::Engine => "Engine",
DeviceCapability::Navigation => "Navigation",
DeviceCapability::Communication => "Communication",
DeviceCapability::Sensor => "Sensor",
DeviceCapability::Custom(name) => name,
}
}
}
/// Current status of a device
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DeviceStatus {
/// Device is initializing
Initializing,
/// Device is online and operational
Online,
/// Device is offline
Offline,
/// Device has encountered an error
Error { message: String },
/// Device is in maintenance mode
Maintenance,
}
/// Device configuration parameters
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceConfig {
/// Device name
pub name: String,
/// Device capabilities
pub capabilities: Vec<DeviceCapability>,
/// Update interval in milliseconds
pub update_interval_ms: u64,
/// Maximum message queue size
pub max_queue_size: usize,
/// Device-specific configuration
pub custom_config: HashMap<String, String>,
}
impl Default for DeviceConfig {
fn default() -> Self {
Self {
name: "Unknown Device".to_string(),
capabilities: vec![],
update_interval_ms: 1000,
max_queue_size: 100,
custom_config: HashMap::new(),
}
}
}
/// Device information for discovery and identification
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceInfo {
/// Device address
pub address: BusAddress,
/// Device configuration
pub config: DeviceConfig,
/// Current status
pub status: DeviceStatus,
/// Last seen timestamp
pub last_seen: SystemTime,
/// Device version
pub version: String,
/// Manufacturer information
pub manufacturer: String,
}
/// Trait for implementing system devices
#[async_trait::async_trait]
pub trait SystemDevice: Send + Sync {
/// Initialize the device
async fn initialize(&mut self) -> Result<()>;
/// Start the device operation
async fn start(&mut self) -> Result<()>;
/// Stop the device operation
async fn stop(&mut self) -> Result<()>;
/// Get device information
fn get_info(&self) -> DeviceInfo;
/// Get current device status
fn get_status(&self) -> DeviceStatus;
/// Handle incoming bus message
async fn handle_message(&mut self, message: BusMessage) -> Result<Option<BusMessage>>;
/// Process device-specific logic (called periodically)
async fn process(&mut self) -> Result<Vec<BusMessage>>;
/// Get device capabilities
fn get_capabilities(&self) -> Vec<DeviceCapability>;
/// Update device configuration
async fn update_config(&mut self, config: DeviceConfig) -> Result<()>;
}
/// Base implementation for system devices
pub struct BaseSystemDevice {
pub info: DeviceInfo,
pub message_sender: Option<mpsc::UnboundedSender<BusMessage>>,
pub message_receiver: Option<mpsc::UnboundedReceiver<BusMessage>>,
pub is_running: bool,
}
impl BaseSystemDevice {
/// Create a new base system device
pub fn new(config: DeviceConfig) -> Self {
let address = BusAddress::new(&config.name);
let info = DeviceInfo {
address,
config,
status: DeviceStatus::Initializing,
last_seen: SystemTime::now(),
version: "1.0.0".to_string(),
manufacturer: "Virtual Hardware".to_string(),
};
Self {
info,
message_sender: None,
message_receiver: None,
is_running: false,
}
}
/// Set the message channels
pub fn set_message_channels(
&mut self,
sender: mpsc::UnboundedSender<BusMessage>,
receiver: mpsc::UnboundedReceiver<BusMessage>,
) {
self.message_sender = Some(sender);
self.message_receiver = Some(receiver);
}
/// Send a message through the bus
pub async fn send_message(&self, message: BusMessage) -> Result<()> {
if let Some(sender) = &self.message_sender {
sender.send(message).map_err(|_| {
HardwareError::bus_communication("Failed to send message from device")
})?;
} else {
return Err(HardwareError::generic("Device not connected to bus"));
}
Ok(())
}
/// Update device status
pub fn set_status(&mut self, status: DeviceStatus) {
self.info.status = status;
self.info.last_seen = SystemTime::now();
}
/// Check if device is running
pub fn is_running(&self) -> bool {
self.is_running
}
}
#[async_trait::async_trait]
impl SystemDevice for BaseSystemDevice {
async fn initialize(&mut self) -> Result<()> {
info!("Initializing device: {}", self.info.config.name);
self.set_status(DeviceStatus::Initializing);
// Simulate initialization delay
tokio::time::sleep(Duration::from_millis(100)).await;
self.set_status(DeviceStatus::Online);
Ok(())
}
async fn start(&mut self) -> Result<()> {
info!("Starting device: {}", self.info.config.name);
self.is_running = true;
self.set_status(DeviceStatus::Online);
Ok(())
}
async fn stop(&mut self) -> Result<()> {
info!("Stopping device: {}", self.info.config.name);
self.is_running = false;
self.set_status(DeviceStatus::Offline);
Ok(())
}
fn get_info(&self) -> DeviceInfo {
self.info.clone()
}
fn get_status(&self) -> DeviceStatus {
self.info.status.clone()
}
async fn handle_message(&mut self, message: BusMessage) -> Result<Option<BusMessage>> {
debug!("Device {} received message: {:?}", self.info.config.name, message);
match message {
BusMessage::Control { command, .. } => {
match command {
crate::bus::ControlCommand::Ping { target } => {
if target == self.info.address {
let pong = BusMessage::Control {
from: self.info.address.clone(),
command: crate::bus::ControlCommand::Pong {
from: self.info.address.clone(),
},
message_id: Uuid::new_v4(),
};
return Ok(Some(pong));
}
}
_ => {}
}
}
_ => {}
}
Ok(None)
}
async fn process(&mut self) -> Result<Vec<BusMessage>> {
// Base implementation does nothing
Ok(vec![])
}
fn get_capabilities(&self) -> Vec<DeviceCapability> {
self.info.config.capabilities.clone()
}
async fn update_config(&mut self, config: DeviceConfig) -> Result<()> {
info!("Updating config for device: {}", self.info.config.name);
self.info.config = config;
Ok(())
}
}
/// Device manager for handling multiple devices
pub struct DeviceManager {
devices: HashMap<BusAddress, Box<dyn SystemDevice>>,
}
impl DeviceManager {
/// Create a new device manager
pub fn new() -> Self {
Self {
devices: HashMap::new(),
}
}
/// Add a device to the manager
pub fn add_device(&mut self, device: Box<dyn SystemDevice>) {
let address = device.get_info().address.clone();
self.devices.insert(address, device);
}
/// Remove a device from the manager
pub fn remove_device(&mut self, address: &BusAddress) -> Option<Box<dyn SystemDevice>> {
self.devices.remove(address)
}
/// Get a device by address
pub fn get_device(&self, address: &BusAddress) -> Option<&dyn SystemDevice> {
self.devices.get(address).map(|d| d.as_ref())
}
/// Get a mutable device by address
pub fn get_device_mut(&mut self, address: &BusAddress) -> Option<&mut Box<dyn SystemDevice>> {
self.devices.get_mut(address)
}
/// Initialize all devices
pub async fn initialize_all(&mut self) -> Result<()> {
for device in self.devices.values_mut() {
device.initialize().await?;
}
Ok(())
}
/// Start all devices
pub async fn start_all(&mut self) -> Result<()> {
for device in self.devices.values_mut() {
device.start().await?;
}
Ok(())
}
/// Stop all devices
pub async fn stop_all(&mut self) -> Result<()> {
for device in self.devices.values_mut() {
device.stop().await?;
}
Ok(())
}
/// Get all device information
pub fn get_all_device_info(&self) -> Vec<DeviceInfo> {
self.devices.values().map(|d| d.get_info()).collect()
}
/// Process all devices
pub async fn process_all(&mut self) -> Result<Vec<BusMessage>> {
let mut messages = Vec::new();
for device in self.devices.values_mut() {
let device_messages = device.process().await?;
messages.extend(device_messages);
}
Ok(messages)
}
}
impl Default for DeviceManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_device_creation() {
let config = DeviceConfig {
name: "Test Device".to_string(),
capabilities: vec![DeviceCapability::Gps],
..Default::default()
};
let device = BaseSystemDevice::new(config);
assert_eq!(device.info.config.name, "Test Device");
assert_eq!(device.info.status, DeviceStatus::Initializing);
}
#[tokio::test]
async fn test_device_initialization() {
let config = DeviceConfig {
name: "Test Device".to_string(),
..Default::default()
};
let mut device = BaseSystemDevice::new(config);
device.initialize().await.unwrap();
assert_eq!(device.get_status(), DeviceStatus::Online);
}
#[tokio::test]
async fn test_device_start_stop() {
let config = DeviceConfig {
name: "Test Device".to_string(),
..Default::default()
};
let mut device = BaseSystemDevice::new(config);
device.start().await.unwrap();
assert!(device.is_running());
assert_eq!(device.get_status(), DeviceStatus::Online);
device.stop().await.unwrap();
assert!(!device.is_running());
assert_eq!(device.get_status(), DeviceStatus::Offline);
}
#[tokio::test]
async fn test_device_manager() {
let mut manager = DeviceManager::new();
let config = DeviceConfig {
name: "Test Device".to_string(),
..Default::default()
};
let device = Box::new(BaseSystemDevice::new(config));
let address = device.get_info().address.clone();
manager.add_device(device);
assert!(manager.get_device(&address).is_some());
manager.initialize_all().await.unwrap();
manager.start_all().await.unwrap();
let info = manager.get_all_device_info();
assert_eq!(info.len(), 1);
assert_eq!(info[0].status, DeviceStatus::Online);
}
}

View File

@@ -0,0 +1,561 @@
//! Discovery Protocol Module
//!
//! Provides device discovery and capability advertisement functionality
use crate::{BusAddress, BusMessage, DeviceCapability, DeviceInfo, HardwareError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{Duration, SystemTime};
use tokio::sync::{mpsc, RwLock};
use tracing::{debug, info, warn};
use uuid::Uuid;
use std::sync::Arc;
/// Discovery message types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DiscoveryMessage {
/// Announce device presence and capabilities
Announce {
device_info: DeviceInfo,
timestamp: SystemTime,
},
/// Request device information
Discover {
requester: BusAddress,
filter: Option<DiscoveryFilter>,
timestamp: SystemTime,
},
/// Response to discovery request
DiscoverResponse {
devices: Vec<DeviceInfo>,
responder: BusAddress,
timestamp: SystemTime,
},
/// Heartbeat to maintain presence
Heartbeat {
device: BusAddress,
timestamp: SystemTime,
},
/// Device going offline notification
Goodbye {
device: BusAddress,
timestamp: SystemTime,
},
}
/// Filter criteria for device discovery
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoveryFilter {
/// Filter by device capabilities
pub capabilities: Option<Vec<DeviceCapability>>,
/// Filter by device name pattern
pub name_pattern: Option<String>,
/// Filter by manufacturer
pub manufacturer: Option<String>,
/// Filter by minimum version
pub min_version: Option<String>,
}
impl DiscoveryFilter {
/// Create a new empty filter
pub fn new() -> Self {
Self {
capabilities: None,
name_pattern: None,
manufacturer: None,
min_version: None,
}
}
/// Filter by capabilities
pub fn with_capabilities(mut self, capabilities: Vec<DeviceCapability>) -> Self {
self.capabilities = Some(capabilities);
self
}
/// Filter by name pattern
pub fn with_name_pattern(mut self, pattern: impl Into<String>) -> Self {
self.name_pattern = Some(pattern.into());
self
}
/// Filter by manufacturer
pub fn with_manufacturer(mut self, manufacturer: impl Into<String>) -> Self {
self.manufacturer = Some(manufacturer.into());
self
}
/// Check if device matches this filter
pub fn matches(&self, device_info: &DeviceInfo) -> bool {
// Check capabilities
if let Some(required_caps) = &self.capabilities {
let device_caps = &device_info.config.capabilities;
if !required_caps.iter().all(|cap| device_caps.contains(cap)) {
return false;
}
}
// Check name pattern (simple substring match)
if let Some(pattern) = &self.name_pattern {
if !device_info.config.name.contains(pattern) {
return false;
}
}
// Check manufacturer
if let Some(manufacturer) = &self.manufacturer {
if device_info.manufacturer != *manufacturer {
return false;
}
}
// Check version (simple string comparison for now)
if let Some(min_version) = &self.min_version {
if device_info.version < *min_version {
return false;
}
}
true
}
}
impl Default for DiscoveryFilter {
fn default() -> Self {
Self::new()
}
}
/// Discovery protocol configuration
#[derive(Debug, Clone)]
pub struct DiscoveryConfig {
/// How often to send heartbeat messages (in seconds)
pub heartbeat_interval: Duration,
/// How long to wait before considering a device offline (in seconds)
pub device_timeout: Duration,
/// How often to clean up expired devices (in seconds)
pub cleanup_interval: Duration,
/// Maximum number of devices to track
pub max_devices: usize,
}
impl Default for DiscoveryConfig {
fn default() -> Self {
Self {
heartbeat_interval: Duration::from_secs(30),
device_timeout: Duration::from_secs(90),
cleanup_interval: Duration::from_secs(60),
max_devices: 1000,
}
}
}
/// Discovery protocol implementation
pub struct DiscoveryProtocol {
/// Local device information
local_device: DeviceInfo,
/// Known devices registry
known_devices: Arc<RwLock<HashMap<BusAddress, DeviceInfo>>>,
/// Configuration
config: DiscoveryConfig,
/// Message sender for bus communication
message_sender: Option<mpsc::UnboundedSender<BusMessage>>,
/// Discovery message receiver
discovery_receiver: Option<mpsc::UnboundedReceiver<DiscoveryMessage>>,
/// Running state
is_running: bool,
}
impl DiscoveryProtocol {
/// Create a new discovery protocol instance
pub fn new(local_device: DeviceInfo, config: DiscoveryConfig) -> Self {
Self {
local_device,
known_devices: Arc::new(RwLock::new(HashMap::new())),
config,
message_sender: None,
discovery_receiver: None,
is_running: false,
}
}
/// Set the message sender for bus communication
pub fn set_message_sender(&mut self, sender: mpsc::UnboundedSender<BusMessage>) {
self.message_sender = Some(sender);
}
/// Set the discovery message receiver
pub fn set_discovery_receiver(&mut self, receiver: mpsc::UnboundedReceiver<DiscoveryMessage>) {
self.discovery_receiver = Some(receiver);
}
/// Start the discovery protocol
pub async fn start(&mut self) -> Result<()> {
info!("Starting discovery protocol for device: {}", self.local_device.config.name);
self.is_running = true;
// Send initial announcement
self.announce_device().await?;
Ok(())
}
/// Stop the discovery protocol
pub async fn stop(&mut self) -> Result<()> {
info!("Stopping discovery protocol for device: {}", self.local_device.config.name);
self.is_running = false;
// Send goodbye message
self.send_goodbye().await?;
Ok(())
}
/// Announce this device to the network
pub async fn announce_device(&self) -> Result<()> {
let announcement = DiscoveryMessage::Announce {
device_info: self.local_device.clone(),
timestamp: SystemTime::now(),
};
self.send_discovery_message(announcement).await
}
/// Send heartbeat to maintain presence
pub async fn send_heartbeat(&self) -> Result<()> {
let heartbeat = DiscoveryMessage::Heartbeat {
device: self.local_device.address.clone(),
timestamp: SystemTime::now(),
};
self.send_discovery_message(heartbeat).await
}
/// Send goodbye message when going offline
pub async fn send_goodbye(&self) -> Result<()> {
let goodbye = DiscoveryMessage::Goodbye {
device: self.local_device.address.clone(),
timestamp: SystemTime::now(),
};
self.send_discovery_message(goodbye).await
}
/// Discover devices on the network
pub async fn discover_devices(&self, filter: Option<DiscoveryFilter>) -> Result<()> {
let discover_msg = DiscoveryMessage::Discover {
requester: self.local_device.address.clone(),
filter,
timestamp: SystemTime::now(),
};
self.send_discovery_message(discover_msg).await
}
/// Get all known devices
pub async fn get_known_devices(&self) -> Vec<DeviceInfo> {
let devices = self.known_devices.read().await;
devices.values().cloned().collect()
}
/// Get devices matching a filter
pub async fn get_devices_by_filter(&self, filter: &DiscoveryFilter) -> Vec<DeviceInfo> {
let devices = self.known_devices.read().await;
devices
.values()
.filter(|device| filter.matches(device))
.cloned()
.collect()
}
/// Get device by address
pub async fn get_device(&self, address: &BusAddress) -> Option<DeviceInfo> {
let devices = self.known_devices.read().await;
devices.get(address).cloned()
}
/// Handle incoming discovery message
pub async fn handle_discovery_message(&self, message: DiscoveryMessage) -> Result<()> {
match message {
DiscoveryMessage::Announce { device_info, .. } => {
self.handle_device_announcement(device_info).await
}
DiscoveryMessage::Discover { requester, filter, .. } => {
self.handle_discovery_request(requester, filter).await
}
DiscoveryMessage::DiscoverResponse { devices, .. } => {
self.handle_discovery_response(devices).await
}
DiscoveryMessage::Heartbeat { device, timestamp } => {
self.handle_heartbeat(device, timestamp).await
}
DiscoveryMessage::Goodbye { device, .. } => {
self.handle_goodbye(device).await
}
}
}
/// Handle device announcement
async fn handle_device_announcement(&self, device_info: DeviceInfo) -> Result<()> {
info!("Device announced: {}", device_info.config.name);
let mut devices = self.known_devices.write().await;
devices.insert(device_info.address.clone(), device_info);
Ok(())
}
/// Handle discovery request
async fn handle_discovery_request(
&self,
requester: BusAddress,
filter: Option<DiscoveryFilter>,
) -> Result<()> {
debug!("Discovery request from: {}", requester.name);
let devices = self.known_devices.read().await;
let mut matching_devices = vec![self.local_device.clone()]; // Include self
// Add matching known devices
for device in devices.values() {
if let Some(ref filter) = filter {
if filter.matches(device) {
matching_devices.push(device.clone());
}
} else {
matching_devices.push(device.clone());
}
}
drop(devices); // Release the lock
let response = DiscoveryMessage::DiscoverResponse {
devices: matching_devices,
responder: self.local_device.address.clone(),
timestamp: SystemTime::now(),
};
self.send_discovery_message(response).await
}
/// Handle discovery response
async fn handle_discovery_response(&self, devices: Vec<DeviceInfo>) -> Result<()> {
debug!("Received discovery response with {} devices", devices.len());
let mut known_devices = self.known_devices.write().await;
for device in devices {
// Don't add ourselves
if device.address != self.local_device.address {
known_devices.insert(device.address.clone(), device);
}
}
Ok(())
}
/// Handle heartbeat message
async fn handle_heartbeat(&self, device: BusAddress, timestamp: SystemTime) -> Result<()> {
debug!("Heartbeat from device: {}", device.name);
let mut devices = self.known_devices.write().await;
if let Some(device_info) = devices.get_mut(&device) {
device_info.last_seen = timestamp;
}
Ok(())
}
/// Handle goodbye message
async fn handle_goodbye(&self, device: BusAddress) -> Result<()> {
info!("Device going offline: {}", device.name);
let mut devices = self.known_devices.write().await;
devices.remove(&device);
Ok(())
}
/// Clean up expired devices
pub async fn cleanup_expired_devices(&self) -> Result<()> {
let now = SystemTime::now();
let timeout = self.config.device_timeout;
let mut devices = self.known_devices.write().await;
let mut expired_devices = Vec::new();
for (address, device_info) in devices.iter() {
if let Ok(elapsed) = now.duration_since(device_info.last_seen) {
if elapsed > timeout {
expired_devices.push(address.clone());
}
}
}
for address in expired_devices {
warn!("Removing expired device: {}", address.name);
devices.remove(&address);
}
Ok(())
}
/// Send discovery message over the bus
async fn send_discovery_message(&self, discovery_msg: DiscoveryMessage) -> Result<()> {
if let Some(sender) = &self.message_sender {
let payload = serde_json::to_vec(&discovery_msg)?;
let bus_message = BusMessage::Broadcast {
from: self.local_device.address.clone(),
payload,
message_id: Uuid::new_v4(),
};
sender.send(bus_message).map_err(|_| {
HardwareError::bus_communication("Failed to send discovery message")
})?;
} else {
return Err(HardwareError::generic("Discovery protocol not connected to bus"));
}
Ok(())
}
/// Run the discovery protocol main loop
pub async fn run(&mut self) -> Result<()> {
let mut heartbeat_timer = tokio::time::interval(self.config.heartbeat_interval);
let mut cleanup_timer = tokio::time::interval(self.config.cleanup_interval);
while self.is_running {
tokio::select! {
_ = heartbeat_timer.tick() => {
if let Err(e) = self.send_heartbeat().await {
warn!("Failed to send heartbeat: {}", e);
}
}
_ = cleanup_timer.tick() => {
if let Err(e) = self.cleanup_expired_devices().await {
warn!("Failed to cleanup expired devices: {}", e);
}
}
// Handle incoming discovery messages if receiver is set
msg = async {
if let Some(ref mut receiver) = self.discovery_receiver {
receiver.recv().await
} else {
std::future::pending().await
}
} => {
if let Some(discovery_msg) = msg {
if let Err(e) = self.handle_discovery_message(discovery_msg).await {
warn!("Failed to handle discovery message: {}", e);
}
}
}
}
}
Ok(())
}
/// Check if the protocol is running
pub fn is_running(&self) -> bool {
self.is_running
}
/// Get the number of known devices
pub async fn device_count(&self) -> usize {
let devices = self.known_devices.read().await;
devices.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DeviceConfig, DeviceStatus};
fn create_test_device_info(name: &str) -> DeviceInfo {
DeviceInfo {
address: BusAddress::new(name),
config: DeviceConfig {
name: name.to_string(),
capabilities: vec![DeviceCapability::Gps],
..Default::default()
},
status: DeviceStatus::Online,
last_seen: SystemTime::now(),
version: "1.0.0".to_string(),
manufacturer: "Test Manufacturer".to_string(),
}
}
#[tokio::test]
async fn test_discovery_protocol_creation() {
let device_info = create_test_device_info("test_device");
let config = DiscoveryConfig::default();
let protocol = DiscoveryProtocol::new(device_info, config);
assert!(!protocol.is_running());
assert_eq!(protocol.device_count().await, 0);
}
#[tokio::test]
async fn test_device_announcement() {
let device_info = create_test_device_info("test_device");
let config = DiscoveryConfig::default();
let protocol = DiscoveryProtocol::new(device_info.clone(), config);
let other_device = create_test_device_info("other_device");
protocol.handle_device_announcement(other_device.clone()).await.unwrap();
let known_devices = protocol.get_known_devices().await;
assert_eq!(known_devices.len(), 1);
assert_eq!(known_devices[0].config.name, "other_device");
}
#[tokio::test]
async fn test_discovery_filter() {
let filter = DiscoveryFilter::new()
.with_capabilities(vec![DeviceCapability::Gps])
.with_name_pattern("test");
let device_info = create_test_device_info("test_device");
assert!(filter.matches(&device_info));
let other_device = DeviceInfo {
address: BusAddress::new("other"),
config: DeviceConfig {
name: "other".to_string(),
capabilities: vec![DeviceCapability::Radar],
..Default::default()
},
status: DeviceStatus::Online,
last_seen: SystemTime::now(),
version: "1.0.0".to_string(),
manufacturer: "Test".to_string(),
};
assert!(!filter.matches(&other_device));
}
#[tokio::test]
async fn test_device_cleanup() {
let device_info = create_test_device_info("test_device");
let mut config = DiscoveryConfig::default();
config.device_timeout = Duration::from_millis(100);
let protocol = DiscoveryProtocol::new(device_info, config);
// Add a device with old timestamp
let mut old_device = create_test_device_info("old_device");
old_device.last_seen = SystemTime::now() - Duration::from_secs(200);
protocol.handle_device_announcement(old_device).await.unwrap();
assert_eq!(protocol.device_count().await, 1);
// Wait and cleanup
tokio::time::sleep(Duration::from_millis(150)).await;
protocol.cleanup_expired_devices().await.unwrap();
assert_eq!(protocol.device_count().await, 0);
}
}

View File

@@ -0,0 +1,72 @@
//! Error types for the hardware abstraction layer
use thiserror::Error;
/// Result type alias for hardware operations
pub type Result<T> = std::result::Result<T, HardwareError>;
/// Common error types for hardware operations
#[derive(Error, Debug)]
pub enum HardwareError {
/// Device not found on the bus
#[error("Device not found: {device_id}")]
DeviceNotFound { device_id: String },
/// Bus communication error
#[error("Bus communication error: {message}")]
BusCommunicationError { message: String },
/// Device is not responding
#[error("Device not responding: {device_id}")]
DeviceNotResponding { device_id: String },
/// Invalid device capability
#[error("Invalid device capability: {capability}")]
InvalidCapability { capability: String },
/// Discovery protocol error
#[error("Discovery protocol error: {message}")]
DiscoveryError { message: String },
/// Device initialization error
#[error("Device initialization failed: {device_id}, reason: {reason}")]
InitializationError { device_id: String, reason: String },
/// Serialization/Deserialization error
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
/// Generic hardware error
#[error("Hardware error: {message}")]
Generic { message: String },
}
impl HardwareError {
/// Create a new generic hardware error
pub fn generic(message: impl Into<String>) -> Self {
Self::Generic {
message: message.into(),
}
}
/// Create a new bus communication error
pub fn bus_communication(message: impl Into<String>) -> Self {
Self::BusCommunicationError {
message: message.into(),
}
}
/// Create a new device not found error
pub fn device_not_found(device_id: impl Into<String>) -> Self {
Self::DeviceNotFound {
device_id: device_id.into(),
}
}
/// Create a new discovery error
pub fn discovery_error(message: impl Into<String>) -> Self {
Self::DiscoveryError {
message: message.into(),
}
}
}

View File

@@ -0,0 +1,534 @@
//! GPS Device Implementation
//!
//! This module provides a GPS device that integrates with the hardware abstraction layer.
//! It uses NMEA sentence parsing to extract location data from GPS hardware and broadcasts
//! location updates via the hardware bus.
use crate::{
BusAddress, BusMessage, DeviceCapability, DeviceConfig, DeviceInfo, DeviceStatus,
HardwareError, Result, SystemDevice,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::{BufRead, BufReader, ErrorKind};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::Mutex;
use tracing::{debug, error, info, warn};
use uuid::Uuid;
/// GPS location data structure
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LocationData {
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub altitude: Option<f64>,
pub speed: Option<f64>,
pub timestamp: Option<String>,
pub fix_quality: Option<u8>,
pub satellites: Option<u8>,
}
impl Default for LocationData {
fn default() -> Self {
LocationData {
latitude: None,
longitude: None,
altitude: None,
speed: None,
timestamp: None,
fix_quality: None,
satellites: None,
}
}
}
/// GNSS parser for NMEA sentences
pub struct GnssParser;
impl GnssParser {
pub fn new() -> Self {
GnssParser
}
pub fn parse_sentence(&self, sentence: &str) -> Option<LocationData> {
if sentence.is_empty() || !sentence.starts_with('$') {
return None;
}
let parts: Vec<&str> = sentence.split(',').collect();
if parts.is_empty() {
return None;
}
let sentence_type = parts[0];
match sentence_type {
"$GPGGA" | "$GNGGA" => self.parse_gpgga(&parts),
"$GPRMC" | "$GNRMC" => self.parse_gprmc(&parts),
_ => None,
}
}
fn parse_gpgga(&self, parts: &[&str]) -> Option<LocationData> {
if parts.len() < 15 {
return None;
}
let mut location = LocationData::default();
// Parse timestamp (field 1)
if !parts[1].is_empty() {
location.timestamp = Some(parts[1].to_string());
}
// Parse latitude (fields 2 and 3)
if !parts[2].is_empty() && !parts[3].is_empty() {
if let Ok(lat_raw) = parts[2].parse::<f64>() {
let degrees = (lat_raw / 100.0).floor();
let minutes = lat_raw - (degrees * 100.0);
let mut latitude = degrees + (minutes / 60.0);
if parts[3] == "S" {
latitude = -latitude;
}
location.latitude = Some(latitude);
}
}
// Parse longitude (fields 4 and 5)
if !parts[4].is_empty() && !parts[5].is_empty() {
if let Ok(lon_raw) = parts[4].parse::<f64>() {
let degrees = (lon_raw / 100.0).floor();
let minutes = lon_raw - (degrees * 100.0);
let mut longitude = degrees + (minutes / 60.0);
if parts[5] == "W" {
longitude = -longitude;
}
location.longitude = Some(longitude);
}
}
// Parse fix quality (field 6)
if !parts[6].is_empty() {
if let Ok(quality) = parts[6].parse::<u8>() {
location.fix_quality = Some(quality);
}
}
// Parse number of satellites (field 7)
if !parts[7].is_empty() {
if let Ok(sats) = parts[7].parse::<u8>() {
location.satellites = Some(sats);
}
}
// Parse altitude (field 9)
if !parts[9].is_empty() {
if let Ok(alt) = parts[9].parse::<f64>() {
location.altitude = Some(alt);
}
}
Some(location)
}
fn parse_gprmc(&self, parts: &[&str]) -> Option<LocationData> {
if parts.len() < 12 {
return None;
}
let mut location = LocationData::default();
// Parse timestamp (field 1)
if !parts[1].is_empty() {
location.timestamp = Some(parts[1].to_string());
}
// Check if data is valid (field 2)
if parts[2] != "A" {
return None; // Invalid data
}
// Parse latitude (fields 3 and 4)
if !parts[3].is_empty() && !parts[4].is_empty() {
if let Ok(lat_raw) = parts[3].parse::<f64>() {
let degrees = (lat_raw / 100.0).floor();
let minutes = lat_raw - (degrees * 100.0);
let mut latitude = degrees + (minutes / 60.0);
if parts[4] == "S" {
latitude = -latitude;
}
location.latitude = Some(latitude);
}
}
// Parse longitude (fields 5 and 6)
if !parts[5].is_empty() && !parts[6].is_empty() {
if let Ok(lon_raw) = parts[5].parse::<f64>() {
let degrees = (lon_raw / 100.0).floor();
let minutes = lon_raw - (degrees * 100.0);
let mut longitude = degrees + (minutes / 60.0);
if parts[6] == "W" {
longitude = -longitude;
}
location.longitude = Some(longitude);
}
}
// Parse speed (field 7) - in knots
if !parts[7].is_empty() {
if let Ok(speed_knots) = parts[7].parse::<f64>() {
location.speed = Some(speed_knots);
}
}
Some(location)
}
}
/// GPS Device configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GpsDeviceConfig {
pub device_paths: Vec<String>,
pub baud_rate: u32,
pub timeout_ms: u64,
pub auto_reconnect: bool,
pub broadcast_interval_ms: u64,
}
impl Default for GpsDeviceConfig {
fn default() -> Self {
GpsDeviceConfig {
device_paths: vec![
"/dev/tty.usbmodem2101".to_string(),
"/dev/cu.usbmodem2101".to_string(),
"/dev/ttyUSB0".to_string(),
"/dev/ttyACM0".to_string(),
],
baud_rate: 9600,
timeout_ms: 1000,
auto_reconnect: true,
broadcast_interval_ms: 1000,
}
}
}
/// GPS Device implementation
pub struct GpsDevice {
device_info: DeviceInfo,
gps_config: GpsDeviceConfig,
parser: GnssParser,
last_location: Option<LocationData>,
serial_port: Option<Arc<Mutex<Box<dyn serialport::SerialPort>>>>,
running: bool,
}
impl GpsDevice {
pub fn new(gps_config: GpsDeviceConfig) -> Self {
let address = BusAddress::new("GPS_DEVICE");
// Create device config for the hardware abstraction layer
let device_config = DeviceConfig {
name: "GPS Device".to_string(),
capabilities: vec![DeviceCapability::Gps, DeviceCapability::Navigation],
update_interval_ms: gps_config.broadcast_interval_ms,
max_queue_size: 100,
custom_config: HashMap::new(),
};
let device_info = DeviceInfo {
address,
config: device_config,
status: DeviceStatus::Offline,
last_seen: SystemTime::now(),
version: "1.0.0".to_string(),
manufacturer: "YachtPit".to_string(),
};
GpsDevice {
device_info,
gps_config,
parser: GnssParser::new(),
last_location: None,
serial_port: None,
running: false,
}
}
pub fn with_address(mut self, address: BusAddress) -> Self {
self.device_info.address = address;
self
}
async fn try_connect_serial(&mut self) -> Result<()> {
for device_path in &self.gps_config.device_paths {
debug!("Attempting to connect to GPS device at: {}", device_path);
match serialport::new(device_path, self.gps_config.baud_rate)
.timeout(Duration::from_millis(self.gps_config.timeout_ms))
.open()
{
Ok(port) => {
info!("Successfully connected to GPS device at {}", device_path);
self.serial_port = Some(Arc::new(Mutex::new(port)));
return Ok(());
}
Err(e) => {
debug!("Failed to connect to {}: {}", device_path, e);
}
}
}
Err(HardwareError::generic(
"Could not connect to any GPS device"
))
}
async fn read_and_parse_gps_data(&mut self) -> Result<Vec<BusMessage>> {
let mut messages = Vec::new();
if let Some(ref serial_port) = self.serial_port {
let mut port_guard = serial_port.lock().await;
let mut reader = BufReader::new(port_guard.as_mut());
let mut line = String::new();
// Try to read a line with timeout handling
match reader.read_line(&mut line) {
Ok(0) => {
// EOF - connection lost
warn!("GPS device connection lost (EOF)");
drop(port_guard); // Release the lock before modifying self
self.serial_port = None;
self.device_info.status = DeviceStatus::Offline;
}
Ok(_) => {
let sentence = line.trim();
if !sentence.is_empty() {
debug!("Received GPS sentence: {}", sentence);
if let Some(location) = self.parser.parse_sentence(sentence) {
info!("Parsed GPS location: {:?}", location);
self.last_location = Some(location.clone());
// Create broadcast message with location data
if let Ok(payload) = serde_json::to_vec(&location) {
let message = BusMessage::Broadcast {
from: self.device_info.address.clone(),
payload,
message_id: Uuid::new_v4(),
};
messages.push(message);
}
}
}
}
Err(e) => {
match e.kind() {
ErrorKind::TimedOut => {
// Timeout is normal, just continue
debug!("GPS read timeout - continuing");
}
ErrorKind::Interrupted => {
// Interrupted system call - continue
debug!("GPS read interrupted - continuing");
}
_ => {
error!("Error reading from GPS device: {}", e);
drop(port_guard); // Release the lock before modifying self
self.serial_port = None;
self.device_info.status = DeviceStatus::Error {
message: format!("GPS read error: {}", e),
};
}
}
}
}
}
Ok(messages)
}
pub fn get_last_location(&self) -> Option<&LocationData> {
self.last_location.as_ref()
}
}
#[async_trait::async_trait]
impl SystemDevice for GpsDevice {
async fn initialize(&mut self) -> Result<()> {
info!("Initializing GPS device");
self.device_info.status = DeviceStatus::Initializing;
self.device_info.last_seen = SystemTime::now();
// Try to connect to GPS hardware
match self.try_connect_serial().await {
Ok(()) => {
self.device_info.status = DeviceStatus::Online;
info!("GPS device initialized successfully");
Ok(())
}
Err(e) => {
warn!("GPS device initialization failed: {}", e);
self.device_info.status = DeviceStatus::Error {
message: format!("Initialization failed: {}", e),
};
// Return error when hardware GPS is not available
Err(e)
}
}
}
async fn start(&mut self) -> Result<()> {
info!("Starting GPS device");
self.running = true;
self.device_info.last_seen = SystemTime::now();
if self.serial_port.is_some() {
self.device_info.status = DeviceStatus::Online;
} else {
// Try to reconnect if auto-reconnect is enabled
if self.gps_config.auto_reconnect {
if let Ok(()) = self.try_connect_serial().await {
self.device_info.status = DeviceStatus::Online;
}
}
}
Ok(())
}
async fn stop(&mut self) -> Result<()> {
info!("Stopping GPS device");
self.running = false;
self.serial_port = None;
self.device_info.status = DeviceStatus::Offline;
self.device_info.last_seen = SystemTime::now();
Ok(())
}
fn get_info(&self) -> DeviceInfo {
self.device_info.clone()
}
fn get_status(&self) -> DeviceStatus {
self.device_info.status.clone()
}
async fn handle_message(&mut self, message: BusMessage) -> Result<Option<BusMessage>> {
debug!("GPS device received message: {:?}", message);
self.device_info.last_seen = SystemTime::now();
match message {
BusMessage::Control { command, .. } => {
match command {
crate::bus::ControlCommand::Ping { target } => {
if target == self.device_info.address {
return Ok(Some(BusMessage::Control {
from: self.device_info.address.clone(),
command: crate::bus::ControlCommand::Pong {
from: self.device_info.address.clone(),
},
message_id: Uuid::new_v4(),
}));
}
}
_ => {}
}
}
_ => {}
}
Ok(None)
}
async fn process(&mut self) -> Result<Vec<BusMessage>> {
if !self.running {
return Ok(Vec::new());
}
self.device_info.last_seen = SystemTime::now();
// Try to read GPS data if connected
if self.serial_port.is_some() {
self.read_and_parse_gps_data().await
} else if self.gps_config.auto_reconnect {
// Try to reconnect
if let Ok(()) = self.try_connect_serial().await {
info!("GPS device reconnected successfully");
self.device_info.status = DeviceStatus::Online;
}
Ok(Vec::new())
} else {
Ok(Vec::new())
}
}
fn get_capabilities(&self) -> Vec<DeviceCapability> {
self.device_info.config.capabilities.clone()
}
async fn update_config(&mut self, _config: DeviceConfig) -> Result<()> {
// For now, we don't support dynamic config updates
// In a real implementation, you might want to parse the config
// and update the GPS-specific settings
warn!("GPS device config update not implemented");
self.device_info.last_seen = SystemTime::now();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gnss_parser_creation() {
let parser = GnssParser::new();
// Parser should be created successfully
}
#[test]
fn test_parse_gpgga_sentence() {
let parser = GnssParser::new();
let sentence = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";
let result = parser.parse_sentence(sentence);
assert!(result.is_some());
let location = result.unwrap();
assert!(location.latitude.is_some());
assert!(location.longitude.is_some());
assert!(location.altitude.is_some());
assert!((location.latitude.unwrap() - 48.1173).abs() < 0.001);
assert!((location.longitude.unwrap() - 11.5167).abs() < 0.001);
}
#[test]
fn test_gps_device_creation() {
let config = GpsDeviceConfig::default();
let device = GpsDevice::new(config);
assert_eq!(device.get_status(), DeviceStatus::Offline);
assert!(device.get_capabilities().contains(&DeviceCapability::Gps));
}
#[test]
fn test_location_data_serialization() {
let location = LocationData {
latitude: Some(48.1173),
longitude: Some(11.5167),
altitude: Some(545.4),
speed: Some(22.4),
timestamp: Some("123519".to_string()),
fix_quality: Some(1),
satellites: Some(8),
};
let serialized = serde_json::to_string(&location).unwrap();
let deserialized: LocationData = serde_json::from_str(&serialized).unwrap();
assert_eq!(location, deserialized);
}
}

View File

@@ -0,0 +1,27 @@
//! Virtual Hardware Abstraction Layer
//!
//! This crate provides a common abstraction for virtual hardware components
//! including a hardware bus, system devices, and discovery protocols.
#![allow(clippy::type_complexity)]
pub mod bus;
pub mod device;
pub mod discovery_protocol;
pub mod error;
// Re-export main types
pub use bus::{HardwareBus, BusMessage, BusAddress};
pub use device::{SystemDevice, DeviceCapability, DeviceStatus, DeviceInfo, DeviceConfig};
pub use discovery_protocol::{DiscoveryProtocol, DiscoveryMessage};
pub use error::{HardwareError, Result};
/// Common traits and types used throughout the hardware abstraction layer
pub mod prelude {
pub use crate::{
HardwareBus, BusMessage, BusAddress,
SystemDevice, DeviceCapability, DeviceStatus, DeviceInfo, DeviceConfig,
DiscoveryProtocol, DiscoveryMessage,
HardwareError, Result,
};
}