use crate::{
ble::{BleManager, TreadlyConnection},
error::{Result, TreadlyError},
protocol::{parse_device_status, Message, MessageId},
types::{
AuthenticationStatus, ConnectionParams, DeviceInfo, DeviceStatus, DeviceStatusCode,
EmergencyStopState, SpeedUnit, TemperatureStatus, TimeoutConfig,
},
};
use std::{
sync::Arc,
time::{Duration, Instant},
};
use tokio::sync::{Mutex, RwLock};
use tracing::{error, info, warn};
pub struct TreadlyDevice {
connection: Arc<Mutex<Option<TreadlyConnection>>>,
device_info: DeviceInfo,
status: Arc<RwLock<DeviceStatus>>,
#[allow(dead_code)]
ble_manager: BleManager,
authenticated: Arc<RwLock<bool>>,
last_message_time: Arc<RwLock<Instant>>,
connection_monitoring_active: Arc<RwLock<bool>>,
timeout_config: TimeoutConfig,
}
impl TreadlyDevice {
pub async fn connect_first() -> Result<Self> {
Self::connect_first_with_params(ConnectionParams::default()).await
}
pub async fn connect_first_with_params(params: ConnectionParams) -> Result<Self> {
Self::connect_first_with_params_and_timeout(params, TimeoutConfig::default()).await
}
pub async fn connect_first_with_params_and_timeout(
params: ConnectionParams,
timeout_config: TimeoutConfig,
) -> Result<Self> {
let ble_manager = BleManager::new().await?;
let devices = ble_manager.scan_for_devices(¶ms).await?;
if devices.is_empty() {
return Err(TreadlyError::DeviceNotFound);
}
let mut sorted_devices = devices;
sorted_devices.sort_by(|a, b| b.priority.cmp(&a.priority).then(b.rssi.cmp(&a.rssi)));
let device_info = sorted_devices.into_iter().next().unwrap();
Self::connect_to_device_with_timeout(device_info, params, timeout_config).await
}
pub async fn connect_to_device(
device_info: DeviceInfo,
params: ConnectionParams,
) -> Result<Self> {
Self::connect_to_device_with_timeout(device_info, params, TimeoutConfig::default()).await
}
pub async fn connect_to_device_with_timeout(
device_info: DeviceInfo,
params: ConnectionParams,
timeout_config: TimeoutConfig,
) -> Result<Self> {
let mut ble_manager = BleManager::new().await?;
let connection = ble_manager.connect_to_device(&device_info, ¶ms).await?;
let device = Self {
connection: Arc::new(Mutex::new(Some(connection))),
device_info,
status: Arc::new(RwLock::new(DeviceStatus::default())),
ble_manager,
authenticated: Arc::new(RwLock::new(false)),
last_message_time: Arc::new(RwLock::new(Instant::now())),
connection_monitoring_active: Arc::new(RwLock::new(false)),
timeout_config,
};
if params.authenticate {
device.authenticate_with_fallback().await?;
}
device.subscribe_to_status().await?;
device.refresh_status().await?;
device.start_connection_monitoring().await?;
Ok(device)
}
#[must_use]
pub const fn device_info(&self) -> &DeviceInfo {
&self.device_info
}
#[must_use]
pub const fn timeout_config(&self) -> &TimeoutConfig {
&self.timeout_config
}
#[must_use]
pub const fn get_command_timeout(&self, message_id: MessageId) -> u64 {
match message_id {
MessageId::Authenticate => self.timeout_config.auth_timeout_ms,
MessageId::SecureAuthenticate | MessageId::SecureAuthenticateVerify => {
self.timeout_config.secure_auth_timeout_ms
}
MessageId::EmergencyStopRequest => self.timeout_config.emergency_stop_timeout_ms,
MessageId::Status
| MessageId::StatusEx
| MessageId::StatusEx2
| MessageId::BroadcastDeviceStatus => self.timeout_config.status_timeout_ms,
MessageId::SetSpeed | MessageId::SpeedUp | MessageId::SpeedDown => {
self.timeout_config.speed_command_timeout_ms
}
MessageId::Power => self.timeout_config.power_command_timeout_ms,
_ => self.timeout_config.default_timeout_ms,
}
}
pub async fn get_status(&self) -> DeviceStatus {
self.status.read().await.clone()
}
pub async fn is_connected(&self) -> bool {
if let Some(conn) = self.connection.lock().await.as_ref() {
conn.is_connected().await
} else {
false
}
}
pub async fn is_authenticated(&self) -> bool {
*self.authenticated.read().await
}
pub async fn authenticate(&self) -> Result<()> {
info!("Authenticating with device (basic authentication)");
let message = Message::authenticate();
let timeout = self.get_command_timeout(MessageId::Authenticate);
self.send_command_with_response(message, timeout).await?;
*self.authenticated.write().await = true;
{
let mut status = self.status.write().await;
status.authentication = AuthenticationStatus::Authenticated;
}
info!("Basic authentication successful");
Ok(())
}
pub async fn authenticate_secure(&self) -> Result<()> {
info!("Starting secure MD5 challenge-response authentication");
{
let mut status = self.status.write().await;
status.authentication = AuthenticationStatus::InProgress;
}
let challenge_message = Message::command(MessageId::SecureAuthenticate);
let challenge_timeout = self.get_command_timeout(MessageId::SecureAuthenticate);
let challenge_response = self
.send_command_with_response(challenge_message, challenge_timeout)
.await?;
if challenge_response.payload.len() < 16 {
return Err(TreadlyError::AuthenticationFailed(
"Invalid challenge response - payload too short".to_string(),
));
}
let challenge_data = &challenge_response.payload[0..16];
info!("Received authentication challenge from device");
let hash_response = Self::compute_md5_challenge_response(challenge_data);
let verify_message = Message::secure_authenticate_verify(hash_response);
let verify_timeout = self.get_command_timeout(MessageId::SecureAuthenticateVerify);
let verify_response = self
.send_command_with_response(verify_message, verify_timeout)
.await?;
if verify_response.status != crate::protocol::STATUS_SUCCESS {
{
let mut status = self.status.write().await;
status.authentication = AuthenticationStatus::Failed;
}
return Err(TreadlyError::AuthenticationFailed(format!(
"Secure authentication failed - device returned status: {:02X}",
verify_response.status
)));
}
*self.authenticated.write().await = true;
{
let mut status = self.status.write().await;
status.authentication = AuthenticationStatus::Authenticated;
}
info!("Secure MD5 challenge-response authentication successful");
Ok(())
}
fn compute_md5_challenge_response(challenge_data: &[u8]) -> [u8; 16] {
let mut hasher = md5::Context::new();
hasher.consume(challenge_data);
hasher.consume(crate::protocol::AUTH_SECRET_KEY);
let result = hasher.finalize();
let hash_bytes = result.0;
info!("Computed MD5 challenge response");
hash_bytes
}
pub async fn authenticate_with_fallback(&self) -> Result<()> {
info!("Attempting authentication with secure->basic fallback");
match self.authenticate_secure().await {
Ok(()) => {
info!("Secure authentication successful");
Ok(())
}
Err(e) => {
warn!("Secure authentication failed: {}, falling back to basic", e);
{
let mut status = self.status.write().await;
status.authentication = AuthenticationStatus::NotAuthenticated;
}
*self.authenticated.write().await = false;
match self.authenticate().await {
Ok(()) => {
info!("Basic authentication successful after secure fallback");
Ok(())
}
Err(basic_err) => {
error!("Both secure and basic authentication failed");
Err(TreadlyError::AuthenticationFailed(format!(
"Secure auth failed: {e}, Basic auth failed: {basic_err}"
)))
}
}
}
}
}
pub async fn power_on(&self) -> Result<()> {
info!("Powering on treadmill");
self.ensure_ready().await?;
let message = Message::command(MessageId::Power);
let timeout = self.get_command_timeout(MessageId::Power);
self.send_command_with_retry(message, timeout, self.timeout_config.max_retry_attempts)
.await?;
{
let mut status = self.status.write().await;
status.power_on = true;
}
Ok(())
}
pub async fn set_speed(&self, speed: f32, unit: SpeedUnit) -> Result<()> {
info!("Setting speed to {:.1} {}", speed, unit);
self.ensure_ready().await?;
self.ensure_powered_on().await?;
let speed_kmh = match unit {
SpeedUnit::Kilometers => speed,
SpeedUnit::Miles => speed * 1.6093,
};
if !(0.0..=20.0).contains(&speed_kmh) {
return Err(TreadlyError::InvalidParameters(format!(
"Speed {speed_kmh:.1} km/h is out of range (0.0 - 20.0)"
)));
}
let message = Message::set_speed(speed_kmh);
let timeout = self.get_command_timeout(MessageId::SetSpeed);
self.send_command_with_retry(message, timeout, self.timeout_config.max_retry_attempts)
.await?;
Ok(())
}
pub async fn speed_up(&self) -> Result<()> {
info!("Increasing speed");
self.ensure_ready().await?;
self.ensure_powered_on().await?;
let message = Message::command(MessageId::SpeedUp);
self.send_command_with_response(message, 3000).await?;
Ok(())
}
pub async fn speed_down(&self) -> Result<()> {
info!("Decreasing speed");
self.ensure_ready().await?;
self.ensure_powered_on().await?;
let message = Message::command(MessageId::SpeedDown);
self.send_command_with_response(message, 3000).await?;
Ok(())
}
pub async fn emergency_stop(&self) -> Result<()> {
warn!("Emergency stop activated - sending command with acknowledgment verification");
let message = Message::command(MessageId::EmergencyStopRequest);
let timeout = self.get_command_timeout(MessageId::EmergencyStopRequest);
match self
.send_command_with_response_no_safety(message, timeout)
.await
{
Ok(response) => {
if response.id == MessageId::EmergencyStopRequest
|| response.id == MessageId::Status
|| response.id == MessageId::BroadcastDeviceStatus
{
if let Ok(status) = crate::protocol::parse_device_status(&response) {
if status.emergency_stop == EmergencyStopState::Active
|| status.speed.current == 0.0
{
info!("Emergency stop acknowledged by device");
{
let mut local_status = self.status.write().await;
local_status.emergency_stop = EmergencyStopState::Active;
local_status.speed.current = 0.0;
local_status.speed.target = 0.0;
}
return Ok(());
}
}
}
error!("Emergency stop command sent but device response doesn't confirm stop");
Err(TreadlyError::EmergencyStopNotAcknowledged)
}
Err(e) => {
error!("Emergency stop command failed: {}", e);
{
let mut status = self.status.write().await;
status.emergency_stop = EmergencyStopState::Active;
status.speed.current = 0.0;
status.speed.target = 0.0;
}
Err(e)
}
}
}
pub async fn emergency_stop_immediate(&self) -> Result<()> {
warn!("Immediate emergency stop activated");
let message = Message::command(MessageId::EmergencyStopRequest);
self.send_command(message).await?;
{
let mut status = self.status.write().await;
status.emergency_stop = EmergencyStopState::Active;
status.speed.current = 0.0;
status.speed.target = 0.0;
}
Ok(())
}
pub async fn reset_emergency_stop(&self) -> Result<()> {
info!("Resetting emergency stop");
self.ensure_ready().await?;
let message = Message::command(MessageId::ResetStop);
self.send_command_with_response(message, 3000).await?;
{
let mut status = self.status.write().await;
status.emergency_stop = EmergencyStopState::Normal;
}
Ok(())
}
pub async fn set_speed_unit(&self, unit: SpeedUnit) -> Result<()> {
info!("Setting speed unit to {}", unit);
self.ensure_ready().await?;
let message_id = match unit {
SpeedUnit::Kilometers => MessageId::SetUnitKilometers,
SpeedUnit::Miles => MessageId::SetUnitMiles,
};
let message = Message::command(message_id);
self.send_command_with_response(message, 3000).await?;
{
let mut status = self.status.write().await;
status.speed.unit = unit;
}
Ok(())
}
pub async fn set_handrail_enabled(&self, enabled: bool) -> Result<()> {
info!("Setting handrail emergency stop: {}", enabled);
self.ensure_ready().await?;
let message = Message::set_handrail_enabled(enabled);
self.send_command_with_response(message, 3000).await?;
{
let mut status = self.status.write().await;
status.handrail_enabled = enabled;
}
Ok(())
}
pub async fn pause(&self) -> Result<()> {
info!("Pausing treadmill");
self.ensure_ready().await?;
let message = Message::command(MessageId::Pause);
self.send_command_with_response(message, 3000).await?;
Ok(())
}
pub async fn refresh_status(&self) -> Result<()> {
let message = Message::command(MessageId::Status);
let timeout = self.get_command_timeout(MessageId::Status);
self.send_command_with_response(message, timeout).await?;
Ok(())
}
pub async fn subscribe_to_status(&self) -> Result<()> {
let message = Message::command(MessageId::SubscribeStatus);
self.send_command(message).await?;
Ok(())
}
pub async fn disconnect(&self) -> Result<()> {
info!("Disconnecting from device");
let conn = self.connection.lock().await.take();
if let Some(conn) = conn {
conn.disconnect().await?;
}
*self.authenticated.write().await = false;
Ok(())
}
async fn send_command_with_response(
&self,
message: Message,
timeout_ms: u64,
) -> Result<Message> {
self.send_command_with_response_internal(message, timeout_ms, true)
.await
}
pub async fn send_command_with_retry(
&self,
message: Message,
timeout_ms: u64,
max_retries: u32,
) -> Result<Message> {
let mut last_error = None;
let mut current_timeout = timeout_ms;
for attempt in 0..=max_retries {
match self
.send_command_with_response(message.clone(), current_timeout)
.await
{
Ok(response) => return Ok(response),
Err(e) => {
last_error = Some(e);
if attempt < max_retries {
warn!(
"Command {:?} failed on attempt {}/{}, retrying in {}ms: {}",
message.id,
attempt + 1,
max_retries + 1,
self.timeout_config.retry_delay_ms,
last_error.as_ref().unwrap()
);
current_timeout = current_timeout.saturating_mul(3).saturating_div(2);
tokio::time::sleep(Duration::from_millis(
self.timeout_config.retry_delay_ms,
))
.await;
}
}
}
}
error!(
"Command {:?} failed after {} attempts",
message.id,
max_retries + 1
);
Err(last_error.unwrap())
}
async fn send_command_with_response_no_safety(
&self,
message: Message,
timeout_ms: u64,
) -> Result<Message> {
self.send_command(message).await?;
let mut connection = self.connection.lock().await;
if let Some(conn) = connection.as_mut() {
let response = conn.receive_notification(timeout_ms).await?;
*self.last_message_time.write().await = Instant::now();
if matches!(
response.id,
MessageId::Status | MessageId::StatusEx | MessageId::BroadcastDeviceStatus
) {
if let Ok(new_status) = parse_device_status(&response) {
*self.status.write().await = new_status;
}
}
Ok(response)
} else {
Err(TreadlyError::Disconnected)
}
}
async fn send_command_with_response_internal(
&self,
message: Message,
timeout_ms: u64,
perform_safety_monitoring: bool,
) -> Result<Message> {
self.send_command(message).await?;
let mut connection = self.connection.lock().await;
if let Some(conn) = connection.as_mut() {
let response = conn.receive_notification(timeout_ms).await?;
*self.last_message_time.write().await = Instant::now();
if matches!(
response.id,
MessageId::Status | MessageId::StatusEx | MessageId::BroadcastDeviceStatus
) {
if let Ok(new_status) = parse_device_status(&response) {
if perform_safety_monitoring {
if let Err(safety_error) = self.perform_safety_monitoring(&new_status).await
{
warn!("Safety monitoring detected issue: {}", safety_error);
}
}
*self.status.write().await = new_status;
}
}
Ok(response)
} else {
Err(TreadlyError::Disconnected)
}
}
async fn send_command(&self, message: Message) -> Result<()> {
let connection = self.connection.lock().await;
if let Some(conn) = connection.as_ref() {
conn.send_command(&message).await
} else {
Err(TreadlyError::Disconnected)
}
}
async fn ensure_ready(&self) -> Result<()> {
if !self.is_connected().await {
return Err(TreadlyError::NotReady {
reason: "Device not connected".to_string(),
});
}
{
let status = self.status.read().await;
match status.emergency_stop {
EmergencyStopState::Active => {
return Err(TreadlyError::EmergencyStop);
}
EmergencyStopState::ResetRequired => {
return Err(TreadlyError::NotReady {
reason: "Emergency stop reset required".to_string(),
});
}
EmergencyStopState::Normal => {}
}
}
Ok(())
}
async fn ensure_powered_on(&self) -> Result<()> {
{
let status = self.status.read().await;
if !status.power_on {
return Err(TreadlyError::NotReady {
reason: "Device not powered on".to_string(),
});
}
}
Ok(())
}
pub async fn monitor_temperature(&self, status: &DeviceStatus) -> Result<()> {
match status.temperature_status {
TemperatureStatus::Stop => {
warn!("Critical temperature detected - forcing emergency stop");
self.emergency_stop().await?;
Err(TreadlyError::TemperatureSafetyStop)
}
TemperatureStatus::ReduceSpeed => {
warn!("High temperature detected - reducing speed for safety");
self.reduce_speed_for_temperature().await?;
Err(TreadlyError::HighTemperatureSpeedReduction)
}
TemperatureStatus::Unknown => {
warn!("Temperature sensor error - safety monitoring compromised");
Err(TreadlyError::TemperatureSensorError)
}
TemperatureStatus::Normal => Ok(()),
}
}
async fn reduce_speed_for_temperature(&self) -> Result<()> {
let current_status = self.status.read().await;
let current_speed = current_status.speed.current;
let reduced_speed = current_speed * 0.7;
let min_safe_speed = 1.0;
let target_speed = if reduced_speed < min_safe_speed {
min_safe_speed
} else {
reduced_speed
};
drop(current_status);
info!(
"Reducing speed from {:.1} to {:.1} km/h due to high temperature",
current_speed, target_speed
);
let speed_kmh = match SpeedUnit::Kilometers {
SpeedUnit::Kilometers => target_speed,
SpeedUnit::Miles => target_speed * 1.6093,
};
if !(0.0..=20.0).contains(&speed_kmh) {
return Err(TreadlyError::InvalidParameters(format!(
"Speed {speed_kmh:.1} km/h is outside safe range (0.0-20.0 km/h)"
)));
}
let message = Message::set_speed(target_speed);
self.send_command_with_response_no_safety(message, 3000)
.await?;
Ok(())
}
pub fn monitor_device_status(&self, status: &DeviceStatus) -> Result<()> {
match status.device_status_code {
DeviceStatusCode::HighTemperature => {
warn!("Device reporting high temperature status");
Ok(())
}
DeviceStatusCode::PowerCycleRequired => {
warn!("Device requires power cycle - safety condition");
Err(TreadlyError::PowerCycleRequired)
}
DeviceStatusCode::RequestError => {
warn!("Device reporting request error");
Ok(())
}
DeviceStatusCode::WifiNotConnected | DeviceStatusCode::WifiError => {
info!("Device WiFi status: {}", status.device_status_code);
Ok(())
}
DeviceStatusCode::WifiScanning => {
info!("Device WiFi scanning in progress");
Ok(())
}
DeviceStatusCode::NoError => Ok(()),
}
}
pub async fn perform_safety_monitoring(&self, status: &DeviceStatus) -> Result<()> {
self.monitor_temperature(status).await?;
self.monitor_device_status(status)?;
self.monitor_connection_health().await?;
if status.emergency_stop == EmergencyStopState::Active {
warn!("Emergency stop is active - device stopped for safety");
return Err(TreadlyError::EmergencyStop);
}
if status.emergency_stop == EmergencyStopState::ResetRequired {
warn!("Emergency stop reset required before operation");
return Err(TreadlyError::EmergencyStop);
}
Ok(())
}
pub async fn start_connection_monitoring(&self) -> Result<()> {
let monitoring_active = self.connection_monitoring_active.clone();
let last_message_time = self.last_message_time.clone();
let connection = self.connection.clone();
let status = self.status.clone();
*monitoring_active.write().await = true;
let monitoring_task = tokio::spawn(async move {
const CONNECTION_TIMEOUT: Duration = Duration::from_secs(30);
const MESSAGE_TIMEOUT: Duration = Duration::from_secs(10);
const MONITOR_INTERVAL: Duration = Duration::from_millis(500);
info!("Connection monitoring started");
loop {
if !*monitoring_active.read().await {
info!("Connection monitoring stopped");
break;
}
let is_connected = if let Some(conn) = connection.lock().await.as_ref() {
conn.is_connected().await
} else {
false
};
if !is_connected {
error!("Connection lost - triggering emergency stop for safety");
{
let mut device_status = status.write().await;
device_status.connection_health = crate::types::ConnectionHealth::Lost;
}
break;
}
let last_message = *last_message_time.read().await;
if last_message.elapsed() > MESSAGE_TIMEOUT {
warn!("Message timeout detected - connection may be unstable");
{
let mut device_status = status.write().await;
device_status.connection_health = crate::types::ConnectionHealth::Unstable;
if last_message.elapsed() > CONNECTION_TIMEOUT {
error!("Connection timeout exceeded - triggering emergency stop");
device_status.connection_health = crate::types::ConnectionHealth::Lost;
drop(device_status);
break;
}
}
} else {
{
let mut device_status = status.write().await;
device_status.connection_health = crate::types::ConnectionHealth::Healthy;
}
}
tokio::time::sleep(MONITOR_INTERVAL).await;
}
*monitoring_active.write().await = false;
});
tokio::spawn(monitoring_task);
Ok(())
}
pub async fn stop_connection_monitoring(&self) {
*self.connection_monitoring_active.write().await = false;
info!("Connection monitoring stop requested");
}
pub async fn is_connection_monitoring_active(&self) -> bool {
*self.connection_monitoring_active.read().await
}
pub async fn get_connection_health(&self) -> crate::types::ConnectionHealth {
self.status.read().await.connection_health
}
pub async fn get_mac_address(&self) -> Result<String> {
info!("Requesting MAC address from device");
let message = Message::command(MessageId::MacAddress);
let timeout = self.get_command_timeout(MessageId::MacAddress);
let response = self.send_command_with_response(message, timeout).await?;
if response.payload.len() < 6 {
return Err(TreadlyError::Protocol(
"MAC address response payload too short".to_string(),
));
}
let mac_bytes = &response.payload[0..6];
let mac_address = format!(
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
mac_bytes[0], mac_bytes[1], mac_bytes[2], mac_bytes[3], mac_bytes[4], mac_bytes[5]
);
info!("Device MAC address: {}", mac_address);
Ok(mac_address)
}
pub async fn verify_mac_address(&self, expected_mac: &str) -> Result<()> {
info!("Verifying MAC address: {}", expected_mac);
let actual_mac = self.get_mac_address().await?;
if actual_mac.to_lowercase() != expected_mac.to_lowercase() {
return Err(TreadlyError::AuthenticationFailed(format!(
"MAC address mismatch: expected {expected_mac}, got {actual_mac}"
)));
}
let mac_bytes = Self::parse_mac_address(expected_mac)?;
let message = Message::verify_mac_address(mac_bytes);
let timeout = self.get_command_timeout(MessageId::VerifyMacAddress);
let response = self.send_command_with_response(message, timeout).await?;
if response.status != crate::protocol::STATUS_SUCCESS {
return Err(TreadlyError::AuthenticationFailed(format!(
"MAC address verification failed - device returned status: {:02X}",
response.status
)));
}
info!("MAC address verification successful");
Ok(())
}
fn parse_mac_address(mac_address: &str) -> Result<[u8; 6]> {
let parts: Vec<&str> = mac_address.split(':').collect();
if parts.len() != 6 {
return Err(TreadlyError::InvalidParameters(format!(
"Invalid MAC address format: {mac_address}. Expected format: XX:XX:XX:XX:XX:XX"
)));
}
let mut mac_bytes = [0u8; 6];
for (i, part) in parts.iter().enumerate() {
mac_bytes[i] = u8::from_str_radix(part, 16).map_err(|_| {
TreadlyError::InvalidParameters(format!("Invalid MAC address byte: {part}"))
})?;
}
Ok(mac_bytes)
}
pub async fn validate_device(&self, expected_mac: Option<&str>) -> Result<()> {
info!("Starting comprehensive device validation");
let message = Message::command(MessageId::ValidateDevice);
let timeout = self.get_command_timeout(MessageId::ValidateDevice);
let response = self.send_command_with_response(message, timeout).await?;
if response.status != crate::protocol::STATUS_SUCCESS {
return Err(TreadlyError::AuthenticationFailed(format!(
"Device validation failed - device returned status: {:02X}",
response.status
)));
}
if let Some(mac) = expected_mac {
self.verify_mac_address(mac).await?;
}
let status_message = Message::command(MessageId::Status);
let status_timeout = self.get_command_timeout(MessageId::Status);
let status_response = self
.send_command_with_response(status_message, status_timeout)
.await?;
match crate::protocol::parse_device_status(&status_response) {
Ok(_device_status) => {
info!("Device validation successful - device is authentic Treadly");
if let Some(mac) = expected_mac {
info!("Device MAC address validated: {}", mac);
}
Ok(())
}
Err(e) => Err(TreadlyError::AuthenticationFailed(format!(
"Device validation failed - invalid device status response: {e}"
))),
}
}
pub async fn validate_device_on_connection(&self, expected_mac: Option<&str>) -> Result<()> {
info!("Validating device during connection");
let validation_result = self.validate_device(expected_mac).await;
match validation_result {
Ok(()) => {
info!("Device validation successful during connection");
Ok(())
}
Err(e) => {
error!("Device validation failed during connection: {}", e);
Err(e)
}
}
}
pub async fn emergency_stop_connection_loss(&self) -> Result<()> {
error!("Emergency stop triggered due to connection loss");
if self.is_connected().await {
if let Err(e) = self.emergency_stop_immediate().await {
error!("Failed to send emergency stop command: {}", e);
}
}
let mut device_status = self.status.write().await;
device_status.emergency_stop = EmergencyStopState::Active;
device_status.connection_health = crate::types::ConnectionHealth::Lost;
drop(device_status);
Err(TreadlyError::ConnectionLostEmergencyStop)
}
pub async fn monitor_connection_health(&self) -> Result<()> {
let connection_health = self.get_connection_health().await;
match connection_health {
crate::types::ConnectionHealth::Lost => self.emergency_stop_connection_loss().await,
crate::types::ConnectionHealth::Unstable => {
warn!("Connection unstable - monitoring for safety");
Err(TreadlyError::ConnectionHealthDegraded)
}
crate::types::ConnectionHealth::Degraded => {
warn!("Connection degraded - monitoring for safety");
Ok(())
}
crate::types::ConnectionHealth::Healthy => Ok(()),
}
}
}
impl Drop for TreadlyDevice {
fn drop(&mut self) {
let connection = self.connection.clone();
let monitoring_active = self.connection_monitoring_active.clone();
tokio::spawn(async move {
*monitoring_active.write().await = false;
let value = connection.lock().await.take();
if let Some(conn) = value {
let _ = conn.disconnect().await;
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_speed_validation() {
let invalid_speeds = vec![-1.0, 25.0, 100.0];
for speed in invalid_speeds {
let speed_kmh = match SpeedUnit::Kilometers {
SpeedUnit::Kilometers => speed,
SpeedUnit::Miles => speed * 1.6093,
};
assert!(!(0.0..=20.0).contains(&speed_kmh));
}
}
#[test]
fn test_unit_conversion() {
let speed_mph = 10.0f32;
let speed_kmh = speed_mph * 1.6093f32;
assert!((speed_kmh - 16.093f32).abs() < 0.01f32);
}
#[test]
fn test_mac_address_parsing_logic() {
let test_cases = vec![
(
"12:34:56:78:9A:BC",
true,
[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC],
),
(
"00:11:22:33:44:55",
true,
[0x00, 0x11, 0x22, 0x33, 0x44, 0x55],
),
(
"FF:EE:DD:CC:BB:AA",
true,
[0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA],
),
];
for (mac_str, should_succeed, expected_bytes) in test_cases {
let result = parse_mac_address_for_test(mac_str);
if should_succeed {
assert!(result.is_ok(), "MAC address parsing failed for: {mac_str}");
assert_eq!(result.unwrap(), expected_bytes);
} else {
assert!(
result.is_err(),
"MAC address parsing should have failed for: {mac_str}"
);
}
}
let invalid_macs = vec![
"12:34:56:78:9A", "12:34:56:78:9A:BC:DE", "12:34:56:78:9A:XY", "12-34-56-78-9A-BC", "", "12:34:56:78:9A:BC::", ];
for invalid_mac in invalid_macs {
let result = parse_mac_address_for_test(invalid_mac);
assert!(
result.is_err(),
"MAC address parsing should have failed for: {invalid_mac}"
);
}
}
fn parse_mac_address_for_test(mac_address: &str) -> Result<[u8; 6]> {
let parts: Vec<&str> = mac_address.split(':').collect();
if parts.len() != 6 {
return Err(TreadlyError::InvalidParameters(format!(
"Invalid MAC address format: {mac_address}. Expected format: XX:XX:XX:XX:XX:XX"
)));
}
let mut mac_bytes = [0u8; 6];
for (i, part) in parts.iter().enumerate() {
mac_bytes[i] = u8::from_str_radix(part, 16).map_err(|_| {
TreadlyError::InvalidParameters(format!("Invalid MAC address byte: {part}"))
})?;
}
Ok(mac_bytes)
}
#[test]
fn test_speed_validation_logic() {
let test_cases = vec![
(-1.0, false), (0.0, true), (10.0, true), (20.0, true), (25.0, false), ];
for (speed, should_be_valid) in test_cases {
let is_valid = (0.0..=20.0).contains(&speed);
assert_eq!(is_valid, should_be_valid, "Speed {speed} validation failed");
}
}
#[test]
fn test_unit_conversion_logic() {
let mph_speeds = vec![5.0, 10.0, 15.0];
for mph_speed in mph_speeds {
let kmh_speed = mph_speed * 1.6093f32;
let back_to_mph = kmh_speed * 0.6214f32;
assert!((back_to_mph - mph_speed).abs() < 0.01f32);
}
}
}