mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00
compiles, no display
This commit is contained in:
8
crates/datalink/Cargo.toml
Normal file
8
crates/datalink/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "datalink"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
376
crates/datalink/src/lib.rs
Normal file
376
crates/datalink/src/lib.rs
Normal file
@@ -0,0 +1,376 @@
|
||||
//! Virtual Data-Link Abstraction
|
||||
//!
|
||||
//! This crate provides a common abstraction for data communication links
|
||||
//! that can be used by various vessel systems like AIS, GPS, Radar, etc.
|
||||
//!
|
||||
//! The abstraction allows systems to receive and transmit data through
|
||||
//! different transport mechanisms (serial, network, simulation, etc.)
|
||||
//! without being tightly coupled to the specific implementation.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that can occur in the data-link layer
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DataLinkError {
|
||||
#[error("Connection failed: {0}")]
|
||||
ConnectionFailed(String),
|
||||
#[error("Data parsing error: {0}")]
|
||||
ParseError(String),
|
||||
#[error("Timeout occurred")]
|
||||
Timeout,
|
||||
#[error("Invalid configuration: {0}")]
|
||||
InvalidConfig(String),
|
||||
#[error("Transport error: {0}")]
|
||||
TransportError(String),
|
||||
}
|
||||
|
||||
/// Result type for data-link operations
|
||||
pub type DataLinkResult<T> = Result<T, DataLinkError>;
|
||||
|
||||
/// Represents a generic data message that can be transmitted over the data-link
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DataMessage {
|
||||
/// Unique identifier for the message type
|
||||
pub message_type: String,
|
||||
/// Source identifier (e.g., MMSI for AIS, device ID for GPS)
|
||||
pub source_id: String,
|
||||
/// Timestamp when the message was created/received
|
||||
pub timestamp: SystemTime,
|
||||
/// Raw message payload
|
||||
pub payload: Vec<u8>,
|
||||
/// Parsed message data as key-value pairs
|
||||
pub data: HashMap<String, String>,
|
||||
/// Signal strength or quality indicator (0-100)
|
||||
pub signal_quality: Option<u8>,
|
||||
}
|
||||
|
||||
impl DataMessage {
|
||||
/// Create a new data message
|
||||
pub fn new(message_type: String, source_id: String, payload: Vec<u8>) -> Self {
|
||||
Self {
|
||||
message_type,
|
||||
source_id,
|
||||
timestamp: SystemTime::now(),
|
||||
payload,
|
||||
data: HashMap::new(),
|
||||
signal_quality: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add parsed data to the message
|
||||
pub fn with_data(mut self, key: String, value: String) -> Self {
|
||||
self.data.insert(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set signal quality
|
||||
pub fn with_signal_quality(mut self, quality: u8) -> Self {
|
||||
self.signal_quality = Some(quality.min(100));
|
||||
self
|
||||
}
|
||||
|
||||
/// Get a data value by key
|
||||
pub fn get_data(&self, key: &str) -> Option<&String> {
|
||||
self.data.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for a data-link connection
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DataLinkConfig {
|
||||
/// Connection type (e.g., "serial", "tcp", "udp", "simulation")
|
||||
pub connection_type: String,
|
||||
/// Connection parameters (port, baud rate, IP address, etc.)
|
||||
pub parameters: HashMap<String, String>,
|
||||
/// Timeout for operations
|
||||
pub timeout: Duration,
|
||||
/// Whether to auto-reconnect on failure
|
||||
pub auto_reconnect: bool,
|
||||
}
|
||||
|
||||
impl DataLinkConfig {
|
||||
/// Create a new configuration
|
||||
pub fn new(connection_type: String) -> Self {
|
||||
Self {
|
||||
connection_type,
|
||||
parameters: HashMap::new(),
|
||||
timeout: Duration::from_secs(5),
|
||||
auto_reconnect: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a parameter to the configuration
|
||||
pub fn with_parameter(mut self, key: String, value: String) -> Self {
|
||||
self.parameters.insert(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set timeout
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Status of a data-link connection
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DataLinkStatus {
|
||||
/// Connection is active and receiving data
|
||||
Connected,
|
||||
/// Connection is being established
|
||||
Connecting,
|
||||
/// Connection is disconnected
|
||||
Disconnected,
|
||||
/// Connection has an error
|
||||
Error(String),
|
||||
}
|
||||
|
||||
/// Trait for data-link receivers that can receive messages
|
||||
pub trait DataLinkReceiver: Send + Sync {
|
||||
/// Get the current status of the data-link
|
||||
fn status(&self) -> DataLinkStatus;
|
||||
|
||||
/// Receive the next available message, if any
|
||||
fn receive_message(&mut self) -> DataLinkResult<Option<DataMessage>>;
|
||||
|
||||
/// Receive all available messages
|
||||
fn receive_all_messages(&mut self) -> DataLinkResult<Vec<DataMessage>> {
|
||||
let mut messages = Vec::new();
|
||||
while let Some(message) = self.receive_message()? {
|
||||
messages.push(message);
|
||||
}
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
/// Connect to the data source
|
||||
fn connect(&mut self, config: &DataLinkConfig) -> DataLinkResult<()>;
|
||||
|
||||
/// Disconnect from the data source
|
||||
fn disconnect(&mut self) -> DataLinkResult<()>;
|
||||
|
||||
/// Check if the connection is active
|
||||
fn is_connected(&self) -> bool {
|
||||
matches!(self.status(), DataLinkStatus::Connected)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for data-link transmitters that can send messages
|
||||
pub trait DataLinkTransmitter: Send + Sync {
|
||||
/// Get the current status of the data-link
|
||||
fn status(&self) -> DataLinkStatus;
|
||||
|
||||
/// Send a message through the data-link
|
||||
fn send_message(&mut self, message: &DataMessage) -> DataLinkResult<()>;
|
||||
|
||||
/// Connect to the data destination
|
||||
fn connect(&mut self, config: &DataLinkConfig) -> DataLinkResult<()>;
|
||||
|
||||
/// Disconnect from the data destination
|
||||
fn disconnect(&mut self) -> DataLinkResult<()>;
|
||||
|
||||
/// Check if the connection is active
|
||||
fn is_connected(&self) -> bool {
|
||||
matches!(self.status(), DataLinkStatus::Connected)
|
||||
}
|
||||
}
|
||||
|
||||
/// Combined trait for bidirectional data-links
|
||||
pub trait DataLink: DataLinkReceiver + DataLinkTransmitter {}
|
||||
|
||||
/// Automatic implementation for types that implement both receiver and transmitter
|
||||
impl<T> DataLink for T where T: DataLinkReceiver + DataLinkTransmitter {}
|
||||
|
||||
/// A simulation data-link for testing and demonstration purposes
|
||||
pub struct SimulationDataLink {
|
||||
status: DataLinkStatus,
|
||||
config: Option<DataLinkConfig>,
|
||||
message_queue: Vec<DataMessage>,
|
||||
}
|
||||
|
||||
impl SimulationDataLink {
|
||||
/// Create a new simulation data-link
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
status: DataLinkStatus::Disconnected,
|
||||
config: None,
|
||||
message_queue: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a simulated message to the queue
|
||||
pub fn add_simulated_message(&mut self, message: DataMessage) {
|
||||
self.message_queue.push(message);
|
||||
}
|
||||
|
||||
/// Generate sample AIS messages for testing
|
||||
pub fn generate_sample_ais_messages(&mut self) {
|
||||
let messages = vec![
|
||||
DataMessage::new(
|
||||
"AIS_POSITION".to_string(),
|
||||
"987654321".to_string(),
|
||||
b"!AIVDM,1,1,,A,15M8J7001G?UJH@E=4R0S>0@0<0M,0*7B".to_vec(),
|
||||
)
|
||||
.with_data("vessel_name".to_string(), "M/Y SERENITY".to_string())
|
||||
.with_data("mmsi".to_string(), "987654321".to_string())
|
||||
.with_data("latitude".to_string(), "37.7749".to_string())
|
||||
.with_data("longitude".to_string(), "-122.4194".to_string())
|
||||
.with_data("speed".to_string(), "12.5".to_string())
|
||||
.with_data("course".to_string(), "180".to_string())
|
||||
.with_signal_quality(85),
|
||||
|
||||
DataMessage::new(
|
||||
"AIS_POSITION".to_string(),
|
||||
"456789123".to_string(),
|
||||
b"!AIVDM,1,1,,A,15M8J7001G?UJH@E=4R0S>0@0<0M,0*7B".to_vec(),
|
||||
)
|
||||
.with_data("vessel_name".to_string(), "CARGO VESSEL ATLANTIS".to_string())
|
||||
.with_data("mmsi".to_string(), "456789123".to_string())
|
||||
.with_data("latitude".to_string(), "37.7849".to_string())
|
||||
.with_data("longitude".to_string(), "-122.4094".to_string())
|
||||
.with_data("speed".to_string(), "18.2".to_string())
|
||||
.with_data("course".to_string(), "090".to_string())
|
||||
.with_signal_quality(92),
|
||||
|
||||
DataMessage::new(
|
||||
"AIS_POSITION".to_string(),
|
||||
"789123456".to_string(),
|
||||
b"!AIVDM,1,1,,A,15M8J7001G?UJH@E=4R0S>0@0M,0*7B".to_vec(),
|
||||
)
|
||||
.with_data("vessel_name".to_string(), "S/Y WIND DANCER".to_string())
|
||||
.with_data("mmsi".to_string(), "789123456".to_string())
|
||||
.with_data("latitude".to_string(), "37.7649".to_string())
|
||||
.with_data("longitude".to_string(), "-122.4294".to_string())
|
||||
.with_data("speed".to_string(), "6.8".to_string())
|
||||
.with_data("course".to_string(), "225".to_string())
|
||||
.with_signal_quality(78),
|
||||
];
|
||||
|
||||
for message in messages {
|
||||
self.message_queue.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SimulationDataLink {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DataLinkReceiver for SimulationDataLink {
|
||||
fn status(&self) -> DataLinkStatus {
|
||||
self.status.clone()
|
||||
}
|
||||
|
||||
fn receive_message(&mut self) -> DataLinkResult<Option<DataMessage>> {
|
||||
if matches!(self.status, DataLinkStatus::Connected) && !self.message_queue.is_empty() {
|
||||
Ok(Some(self.message_queue.remove(0)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(&mut self, config: &DataLinkConfig) -> DataLinkResult<()> {
|
||||
if config.connection_type == "simulation" {
|
||||
self.config = Some(config.clone());
|
||||
self.status = DataLinkStatus::Connected;
|
||||
// Generate some sample messages when connecting
|
||||
self.generate_sample_ais_messages();
|
||||
Ok(())
|
||||
} else {
|
||||
Err(DataLinkError::InvalidConfig(
|
||||
"SimulationDataLink only supports 'simulation' connection type".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn disconnect(&mut self) -> DataLinkResult<()> {
|
||||
self.status = DataLinkStatus::Disconnected;
|
||||
self.config = None;
|
||||
self.message_queue.clear();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl DataLinkTransmitter for SimulationDataLink {
|
||||
fn status(&self) -> DataLinkStatus {
|
||||
// Delegate to the receiver implementation
|
||||
<Self as DataLinkReceiver>::status(self)
|
||||
}
|
||||
|
||||
fn send_message(&mut self, _message: &DataMessage) -> DataLinkResult<()> {
|
||||
if <Self as DataLinkReceiver>::is_connected(self) {
|
||||
// In simulation mode, we just acknowledge the send
|
||||
Ok(())
|
||||
} else {
|
||||
Err(DataLinkError::ConnectionFailed(
|
||||
"Not connected".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(&mut self, config: &DataLinkConfig) -> DataLinkResult<()> {
|
||||
<Self as DataLinkReceiver>::connect(self, config)
|
||||
}
|
||||
|
||||
fn disconnect(&mut self) -> DataLinkResult<()> {
|
||||
<Self as DataLinkReceiver>::disconnect(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_data_message_creation() {
|
||||
let message = DataMessage::new(
|
||||
"TEST".to_string(),
|
||||
"123".to_string(),
|
||||
b"test payload".to_vec(),
|
||||
)
|
||||
.with_data("key1".to_string(), "value1".to_string())
|
||||
.with_signal_quality(75);
|
||||
|
||||
assert_eq!(message.message_type, "TEST");
|
||||
assert_eq!(message.source_id, "123");
|
||||
assert_eq!(message.get_data("key1"), Some(&"value1".to_string()));
|
||||
assert_eq!(message.signal_quality, Some(75));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simulation_datalink() {
|
||||
let mut datalink = SimulationDataLink::new();
|
||||
let config = DataLinkConfig::new("simulation".to_string());
|
||||
|
||||
assert_eq!(<SimulationDataLink as DataLinkReceiver>::status(&datalink), DataLinkStatus::Disconnected);
|
||||
assert!(!<SimulationDataLink as DataLinkReceiver>::is_connected(&datalink));
|
||||
|
||||
<SimulationDataLink as DataLinkReceiver>::connect(&mut datalink, &config).unwrap();
|
||||
assert_eq!(<SimulationDataLink as DataLinkReceiver>::status(&datalink), DataLinkStatus::Connected);
|
||||
assert!(<SimulationDataLink as DataLinkReceiver>::is_connected(&datalink));
|
||||
|
||||
// Should have sample messages after connecting
|
||||
let messages = <SimulationDataLink as DataLinkReceiver>::receive_all_messages(&mut datalink).unwrap();
|
||||
assert!(!messages.is_empty());
|
||||
assert!(messages.iter().any(|m| m.message_type == "AIS_POSITION"));
|
||||
|
||||
<SimulationDataLink as DataLinkReceiver>::disconnect(&mut datalink).unwrap();
|
||||
assert_eq!(<SimulationDataLink as DataLinkReceiver>::status(&datalink), DataLinkStatus::Disconnected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datalink_config() {
|
||||
let config = DataLinkConfig::new("tcp".to_string())
|
||||
.with_parameter("host".to_string(), "localhost".to_string())
|
||||
.with_parameter("port".to_string(), "4001".to_string())
|
||||
.with_timeout(Duration::from_secs(10));
|
||||
|
||||
assert_eq!(config.connection_type, "tcp");
|
||||
assert_eq!(config.parameters.get("host"), Some(&"localhost".to_string()));
|
||||
assert_eq!(config.timeout, Duration::from_secs(10));
|
||||
}
|
||||
}
|
@@ -17,3 +17,4 @@ bevy = { workspace = true, features = [
|
||||
] }
|
||||
rand = { version = "0.8.3" }
|
||||
components = { path = "../components" }
|
||||
datalink = { path = "../datalink" }
|
||||
|
@@ -1,20 +1,34 @@
|
||||
use bevy::prelude::Time;
|
||||
use components::VesselData;
|
||||
use crate::{SystemInteraction, SystemStatus, VesselSystem};
|
||||
use datalink::{DataLink, DataLinkConfig, DataLinkReceiver, DataMessage, SimulationDataLink};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// AIS (Automatic Identification System) implementation
|
||||
pub struct AisSystem {
|
||||
status: SystemStatus,
|
||||
own_mmsi: u32,
|
||||
receiving: bool,
|
||||
datalink: SimulationDataLink,
|
||||
vessel_data: HashMap<String, DataMessage>,
|
||||
}
|
||||
|
||||
impl AisSystem {
|
||||
pub fn new() -> Self {
|
||||
let mut datalink = SimulationDataLink::new();
|
||||
let config = DataLinkConfig::new("simulation".to_string());
|
||||
|
||||
// Connect to the simulation datalink
|
||||
if let Err(e) = datalink.connect(&config) {
|
||||
eprintln!("Failed to connect AIS datalink: {}", e);
|
||||
}
|
||||
|
||||
Self {
|
||||
status: SystemStatus::Active,
|
||||
own_mmsi: 123456789,
|
||||
receiving: true,
|
||||
datalink,
|
||||
vessel_data: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,38 +43,72 @@ impl VesselSystem for AisSystem {
|
||||
}
|
||||
|
||||
fn update(&mut self, _yacht_data: &VesselData, _time: &Time) {
|
||||
// AIS system is relatively static, but we could simulate
|
||||
// vessel movements or signal strength variations here
|
||||
// Receive new AIS messages from the datalink
|
||||
if self.receiving && self.datalink.is_connected() {
|
||||
if let Ok(messages) = self.datalink.receive_all_messages() {
|
||||
for message in messages {
|
||||
if message.message_type == "AIS_POSITION" {
|
||||
// Store vessel data by MMSI
|
||||
if let Some(mmsi) = message.get_data("mmsi") {
|
||||
self.vessel_data.insert(mmsi.clone(), message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_display(&self, _yacht_data: &VesselData) -> String {
|
||||
format!(
|
||||
let mut display = format!(
|
||||
"AIS - AUTOMATIC IDENTIFICATION SYSTEM\n\n\
|
||||
Status: {}\n\
|
||||
Own Ship MMSI: {}\n\
|
||||
Datalink: {}\n\
|
||||
\n\
|
||||
NEARBY VESSELS:\n\
|
||||
\n\
|
||||
🛥️ M/Y SERENITY\n\
|
||||
MMSI: 987654321\n\
|
||||
Distance: 2.1 NM @ 045°\n\
|
||||
Speed: 12.5 kts\n\
|
||||
Course: 180°\n\
|
||||
\n\
|
||||
🚢 CARGO VESSEL ATLANTIS\n\
|
||||
MMSI: 456789123\n\
|
||||
Distance: 5.8 NM @ 270°\n\
|
||||
Speed: 18.2 kts\n\
|
||||
Course: 090°\n\
|
||||
\n\
|
||||
⛵ S/Y WIND DANCER\n\
|
||||
MMSI: 789123456\n\
|
||||
Distance: 1.3 NM @ 135°\n\
|
||||
Speed: 6.8 kts\n\
|
||||
Course: 225°",
|
||||
NEARBY VESSELS:\n",
|
||||
if self.receiving { "RECEIVING" } else { "STANDBY" },
|
||||
self.own_mmsi
|
||||
)
|
||||
self.own_mmsi,
|
||||
if self.datalink.is_connected() { "CONNECTED" } else { "DISCONNECTED" }
|
||||
);
|
||||
|
||||
if self.vessel_data.is_empty() {
|
||||
display.push_str("\nNo vessels detected");
|
||||
} else {
|
||||
for (mmsi, message) in &self.vessel_data {
|
||||
let vessel_name = message.get_data("vessel_name").unwrap_or(mmsi);
|
||||
let speed = message.get_data("speed").map(|s| s.as_str()).unwrap_or("N/A");
|
||||
let course = message.get_data("course").map(|s| s.as_str()).unwrap_or("N/A");
|
||||
let lat = message.get_data("latitude").map(|s| s.as_str()).unwrap_or("N/A");
|
||||
let lon = message.get_data("longitude").map(|s| s.as_str()).unwrap_or("N/A");
|
||||
|
||||
// Determine vessel icon based on name
|
||||
let icon = if vessel_name.contains("M/Y") || vessel_name.contains("YACHT") {
|
||||
"🛥️"
|
||||
} else if vessel_name.contains("CARGO") || vessel_name.contains("SHIP") {
|
||||
"🚢"
|
||||
} else if vessel_name.contains("S/Y") || vessel_name.contains("SAIL") {
|
||||
"⛵"
|
||||
} else {
|
||||
"🚤"
|
||||
};
|
||||
|
||||
display.push_str(&format!(
|
||||
"\n{} {}\n\
|
||||
MMSI: {}\n\
|
||||
Position: {}°N, {}°W\n\
|
||||
Speed: {} kts\n\
|
||||
Course: {}°\n",
|
||||
icon, vessel_name, mmsi, lat, lon, speed, course
|
||||
));
|
||||
|
||||
if let Some(quality) = message.signal_quality {
|
||||
display.push_str(&format!("Signal: {}%\n", quality));
|
||||
}
|
||||
display.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
display
|
||||
}
|
||||
|
||||
fn handle_interaction(&mut self, interaction: SystemInteraction) -> bool {
|
||||
@@ -104,4 +152,4 @@ impl VesselSystem for AisSystem {
|
||||
fn status(&self) -> SystemStatus {
|
||||
self.status.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user