Skip to content

Commit

Permalink
src: device: manager: Add network discovery and upgrade AutoCreate me…
Browse files Browse the repository at this point in the history
…thod
  • Loading branch information
RaulTrombin committed Aug 30, 2024
1 parent 4a0042d commit e5c8b5d
Show file tree
Hide file tree
Showing 2 changed files with 240 additions and 15 deletions.
225 changes: 225 additions & 0 deletions src/device/manager/device_discovery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
use std::net::Ipv4Addr;

use tokio_serial::available_ports;
use tracing::warn;

use super::{SourceSelection, SourceSerialStruct, SourceUdpStruct};

#[derive(Debug, PartialEq)]
pub struct DiscoveryResponse {
pub device_name: String,
pub manufacturer: String,
pub mac_address: String,
pub ip_address: Ipv4Addr,
}

impl DiscoveryResponse {
/// Decode Ping360 ASCI NetworkDiscovery message
fn from_response(response: &str) -> Option<Self> {
let lines: Vec<&str> = response
.split("\r\n")
.filter(|line| !line.is_empty())
.collect();

if lines.len() != 4 {
warn!(
"Expected 4 lines but found {}. Response: {:?}",
lines.len(),
lines
);
return None;
}

let device_name = lines[0].trim().to_string();
let manufacturer = lines[1].trim().to_string();

let mac_address_line = lines[2].trim();
let ip_address_line = lines[3].trim();

if !mac_address_line.starts_with("MAC Address:- ")
|| !ip_address_line.starts_with("IP Address:- ")
{
warn!("auto_create: network: Unexpected format in MAC Address or IP Address lines.");
return None;
}

let mac_address = mac_address_line
.trim_start_matches("MAC Address:- ")
.to_string();
let ip_address_str = ip_address_line.trim_start_matches("IP Address:- ").trim();

let cleaned_ip_address_str = ip_address_str
.split('.')
.map(|octet| {
let trimmed = octet.trim_start_matches('0');
if trimmed.is_empty() {
"0"
} else {
trimmed
}
})
.collect::<Vec<&str>>()
.join(".");

let ip_address = match cleaned_ip_address_str.parse::<Ipv4Addr>() {
Ok(ip) => ip,
Err(err) => {
warn!(
"auto_create: network: Failed to parse IP address: {cleaned_ip_address_str}, details: {err}"
);
return None;
}
};

Some(DiscoveryResponse {
device_name,
manufacturer,
mac_address,
ip_address,
})
}
}

pub fn network_discovery() -> Option<Vec<SourceSelection>> {
// Create a UDP socket
let socket = std::net::UdpSocket::bind("0.0.0.0:0").unwrap(); // Bind to any available port
socket.set_broadcast(true).unwrap(); // Enable broadcast

let broadcast_addr = "255.255.255.255:30303";
let discovery_message = "Discovery";

socket
.send_to(discovery_message.as_bytes(), broadcast_addr)
.unwrap();

socket
.set_read_timeout(Some(std::time::Duration::from_secs(2)))
.unwrap();

let mut buf = [0; 1024];
let mut responses = Vec::new();

loop {
match socket.recv_from(&mut buf) {
Ok((size, src)) => {
let response = std::str::from_utf8(&buf[..size]).unwrap_or("[Invalid UTF-8]");

if let Some(discovery_response) = DiscoveryResponse::from_response(response) {
responses.push(discovery_response);
} else {
warn!(
"auto_create: network: Failed to parse the discovery response from: {src}"
);
}
}
Err(e) => {
warn!("auto_create: network: Timeout or error receiving response: {e}");
break;
}
}
}

if !responses.is_empty() {
let mut available_sources = Vec::new();
for device in responses {
let source = SourceSelection::UdpStream(SourceUdpStruct {
ip: device.ip_address,
port: 12345,
});

available_sources.push(source);
}
Some(available_sources)
} else {
warn!("auto_create: network: No valid discovery responses were collected.");
None
}
}

pub fn serial_discovery() -> Option<Vec<SourceSelection>> {
match available_ports() {
Ok(serial_ports) => {
let mut available_sources = Vec::new();
for port_info in serial_ports {
let source = SourceSelection::SerialStream(SourceSerialStruct {
path: port_info.port_name.clone(),
baudrate: 115200,
});
available_sources.push(source);
}
Some(available_sources)
}
Err(err) => {
warn!("Auto create: Unable to find available devices on serial ports, details: {err}");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_discovery_response_parsing() {
let response = "SONAR PING360\r\n\
Blue Robotics\r\n\
MAC Address:- 54-10-EC-79-7D-D1\r\n\
IP Address:- 192.168.000.197\r\n";

let expected = DiscoveryResponse {
device_name: "SONAR PING360".to_string(),
manufacturer: "Blue Robotics".to_string(),
mac_address: "54-10-EC-79-7D-D1".to_string(),
ip_address: Ipv4Addr::new(192, 168, 0, 197),
};

let parsed = DiscoveryResponse::from_response(response);
assert_eq!(parsed, Some(expected));
}

#[test]
fn test_invalid_response_parsing() {
let invalid_response = "INVALID RESPONSE FORMAT";

let parsed = DiscoveryResponse::from_response(invalid_response);
assert!(parsed.is_none());
}

#[test]
fn test_multiple_discovery_responses() {
let response_1 = "SONAR PING360\r\n\
Blue Robotics\r\n\
MAC Address:- 54-10-EC-79-7D-D1\r\n\
IP Address:- 192.168.000.197\r\n";

let response_2 = "SONAR PING360\r\n\
Blue Robotics\r\n\
MAC Address:- 54-10-EC-79-7D-D2\r\n\
IP Address:- 192.168.000.198\r\n";

let expected_1 = DiscoveryResponse {
device_name: "SONAR PING360".to_string(),
manufacturer: "Blue Robotics".to_string(),
mac_address: "54-10-EC-79-7D-D1".to_string(),
ip_address: Ipv4Addr::new(192, 168, 0, 197),
};

let expected_2 = DiscoveryResponse {
device_name: "SONAR PING360".to_string(),
manufacturer: "Blue Robotics".to_string(),
mac_address: "54-10-EC-79-7D-D2".to_string(),
ip_address: Ipv4Addr::new(192, 168, 0, 198),
};

let responses = vec![response_1, response_2];

let mut parsed_responses = Vec::new();
for response in responses {
if let Some(parsed) = DiscoveryResponse::from_response(response) {
parsed_responses.push(parsed);
}
}

assert_eq!(parsed_responses, vec![expected_1, expected_2]);
}
}
30 changes: 15 additions & 15 deletions src/device/manager/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// Specially for DeviceManager to retrieve checks and structures from Devices stored in it's hashmap collection
pub mod continuous_mode;
/// Specially for auto creation methods, from UDP or serial port
pub mod device_discovery;
/// Specially for continuous_mode methods, startup, shutdown, handle and errors routines for each device type
pub mod device_handle;

Expand All @@ -12,7 +14,6 @@ use std::{
ops::Deref,
};
use tokio::sync::{mpsc, oneshot};
use tokio_serial::available_ports;

use tokio_serial::{SerialPort, SerialPortBuilderExt, SerialStream};
use tracing::{error, info, trace, warn};
Expand Down Expand Up @@ -399,20 +400,22 @@ impl DeviceManager {
}

pub async fn auto_create(&mut self) -> Result<Answer, ManagerError> {
let serial_ports =
available_ports().map_err(|err| ManagerError::DeviceSourceError(err.to_string()))?;

let mut results = Vec::new();

for port_info in serial_ports {
let source = SourceSelection::SerialStream(SourceSerialStruct {
path: port_info.port_name.clone(),
baudrate: 115200,
});
let mut available_source = Vec::new();

match device_discovery::serial_discovery() {
Some(result) => available_source.extend(result),
None => warn!("Auto create: Unable to find available devices on serial ports"),
}

let device_selection = DeviceSelection::Auto;
match device_discovery::network_discovery() {
Some(result) => available_source.extend(result),
None => warn!("Auto create: Unable to find available devices on network"),
}

match self.create(source, device_selection).await {
for source in available_source {
match self.create(source.clone(), DeviceSelection::Auto).await {
Ok(answer) => match answer {
Answer::DeviceInfo(device_info) => {
results.extend(device_info);
Expand All @@ -422,10 +425,7 @@ impl DeviceManager {
}
},
Err(err) => {
error!(
"Failed to create device for port {}: {:?}",
port_info.port_name, err
);
error!("Failed to create device for source {:?}: {:?}", source, err);
}
}
}
Expand Down

0 comments on commit e5c8b5d

Please sign in to comment.