use crate::{
error::{Result, TreadlyError},
types::{
AuthenticationStatus, ConnectionHealth, DeviceMode, DeviceStatus, DeviceStatusCode,
EmergencyStopState, SpeedInfo, SpeedUnit, TemperatureStatus,
},
};
use bytes::{Buf, BufMut, Bytes, BytesMut};
use std::time::SystemTime;
pub const MESSAGE_SIZE: usize = 20;
pub const PAYLOAD_SIZE: usize = 16;
pub const AUTH_SECRET_KEY: [u8; 4] = [0xA6, 0x5B, 0xF2, 0x83];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum MessageId {
Power = 0x01,
SpeedUp = 0x02,
SpeedDown = 0x03,
Status = 0x04,
SetSpeed = 0x0A,
SetUnitKilometers = 0x08,
SetUnitMiles = 0x09,
StatusEx = 0x0C,
GetDeviceInfo = 0x10,
SubscribeStatus = 0x11,
StatusEx2 = 0x54,
Authenticate = 0x13,
AuthenticateVerify = 0x14,
SecureAuthenticate = 0x26,
SecureAuthenticateVerify = 0x27,
EmergencyStopRequest = 0x18,
ResetStop = 0x37,
SetEmergHandrailEnabled = 0x17,
SetAccelerateZoneStart = 0x15,
SetDecelerateZoneEnd = 0x16,
BroadcastDeviceStatus = 0x33,
SetGameMode = 0x4E,
SetGameModeDisplay = 0x51,
UserInteractionSetEnable = 0x4A,
UserInteractionStatus = 0x49,
UserInteractionSteps = 0x48,
UserInteractionHandrail = 0x52,
GetBtAudioPassword = 0x43,
SetBtAudioPassword = 0x44,
SetDeletePairedPhones = 0x55,
BleEnableRequest = 0x1E,
MaintenanceResetRequest = 0x19,
MaintenanceStepRequest = 0x1A,
FactoryReset = 0x0B,
TestStart = 0x28,
TestNotification = 0x25,
StartBleRemoteTest = 0x4F,
BleRemoteTestResults = 0x50,
Pause = 0x42,
SetRemoteStatus = 0x36,
SetTotalStatus = 0x35,
MacAddress = 0x2B,
VerifyMacAddress = 0x2C,
ValidateDevice = 0x20,
StatusDiagnostic = 0x0D,
SetIrMode = 0x2A,
DeviceDebugLog = 0x39,
DeviceIrDebugLog = 0x3A,
}
impl MessageId {
#[must_use]
pub const fn from_u8(value: u8) -> Option<Self> {
match value {
0x01 => Some(Self::Power),
0x02 => Some(Self::SpeedUp),
0x03 => Some(Self::SpeedDown),
0x04 => Some(Self::Status),
0x08 => Some(Self::SetUnitKilometers),
0x09 => Some(Self::SetUnitMiles),
0x0A => Some(Self::SetSpeed),
0x0C => Some(Self::StatusEx),
0x10 => Some(Self::GetDeviceInfo),
0x11 => Some(Self::SubscribeStatus),
0x13 => Some(Self::Authenticate),
0x14 => Some(Self::AuthenticateVerify),
0x15 => Some(Self::SetAccelerateZoneStart),
0x16 => Some(Self::SetDecelerateZoneEnd),
0x17 => Some(Self::SetEmergHandrailEnabled),
0x18 => Some(Self::EmergencyStopRequest),
0x26 => Some(Self::SecureAuthenticate),
0x27 => Some(Self::SecureAuthenticateVerify),
0x33 => Some(Self::BroadcastDeviceStatus),
0x37 => Some(Self::ResetStop),
0x43 => Some(Self::GetBtAudioPassword),
0x44 => Some(Self::SetBtAudioPassword),
0x49 => Some(Self::UserInteractionStatus),
0x4A => Some(Self::UserInteractionSetEnable),
0x4E => Some(Self::SetGameMode),
0x51 => Some(Self::SetGameModeDisplay),
0x54 => Some(Self::StatusEx2),
0x55 => Some(Self::SetDeletePairedPhones),
0x20 => Some(Self::ValidateDevice),
0x0D => Some(Self::StatusDiagnostic),
0x2A => Some(Self::SetIrMode),
0x39 => Some(Self::DeviceDebugLog),
0x3A => Some(Self::DeviceIrDebugLog),
0x1E => Some(Self::BleEnableRequest),
0x19 => Some(Self::MaintenanceResetRequest),
0x1A => Some(Self::MaintenanceStepRequest),
0x0B => Some(Self::FactoryReset),
0x28 => Some(Self::TestStart),
0x25 => Some(Self::TestNotification),
0x4F => Some(Self::StartBleRemoteTest),
0x50 => Some(Self::BleRemoteTestResults),
0x42 => Some(Self::Pause),
0x36 => Some(Self::SetRemoteStatus),
0x35 => Some(Self::SetTotalStatus),
0x2B => Some(Self::MacAddress),
0x2C => Some(Self::VerifyMacAddress),
0x48 => Some(Self::UserInteractionSteps),
0x52 => Some(Self::UserInteractionHandrail),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum StatusCode {
Request = 0x00,
SecureAuthRequired = 0x81,
}
pub const STATUS_SUCCESS: u8 = 0x00;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub id: MessageId,
pub status: u8,
pub payload: Vec<u8>,
}
impl Message {
#[must_use]
pub const fn new(id: MessageId, payload: Vec<u8>) -> Self {
Self {
id,
status: StatusCode::Request as u8,
payload,
}
}
#[must_use]
pub fn command(id: MessageId) -> Self {
Self::new(id, vec![0; PAYLOAD_SIZE])
}
#[must_use]
pub fn set_speed(speed: f32) -> Self {
let mut payload = vec![0; PAYLOAD_SIZE];
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let speed_bytes = (speed * 10.0) as u32;
payload[0..4].copy_from_slice(&speed_bytes.to_le_bytes());
Self::new(MessageId::SetSpeed, payload)
}
#[must_use]
pub fn authenticate() -> Self {
let mut payload = vec![0; PAYLOAD_SIZE];
payload[0..4].copy_from_slice(&AUTH_SECRET_KEY);
Self::new(MessageId::Authenticate, payload)
}
#[must_use]
pub fn set_handrail_enabled(enabled: bool) -> Self {
let mut payload = vec![0; PAYLOAD_SIZE];
payload[0] = u8::from(enabled);
Self::new(MessageId::SetEmergHandrailEnabled, payload)
}
#[must_use]
pub fn secure_authenticate_verify(hash_response: [u8; 16]) -> Self {
let mut payload = vec![0; PAYLOAD_SIZE];
payload[0..16].copy_from_slice(&hash_response);
Self::new(MessageId::SecureAuthenticateVerify, payload)
}
#[must_use]
pub fn verify_mac_address(mac_bytes: [u8; 6]) -> Self {
let mut payload = vec![0; PAYLOAD_SIZE];
payload[0..6].copy_from_slice(&mac_bytes);
Self::new(MessageId::VerifyMacAddress, payload)
}
#[must_use]
pub fn to_bytes(&self) -> Bytes {
let mut buf = BytesMut::with_capacity(MESSAGE_SIZE);
buf.put_u8(self.id as u8);
buf.put_u8(self.status);
buf.put_u8(self.id as u8);
let payload_len = std::cmp::min(self.payload.len(), PAYLOAD_SIZE);
buf.extend_from_slice(&self.payload[..payload_len]);
while buf.len() < MESSAGE_SIZE - 1 {
buf.put_u8(0);
}
buf.put_u8(0);
buf.freeze()
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
if data.len() < MESSAGE_SIZE {
return Err(TreadlyError::ParseError(format!(
"Message too short: {} bytes, expected {}",
data.len(),
MESSAGE_SIZE
)));
}
let mut buf = data;
let cmd = buf.get_u8();
let status = buf.get_u8();
let id = buf.get_u8();
if cmd != id {
return Err(TreadlyError::ParseError(format!(
"Command byte ({cmd:02X}) doesn't match ID byte ({id:02X})"
)));
}
let message_id = MessageId::from_u8(id)
.ok_or_else(|| TreadlyError::ParseError(format!("Unknown message ID: {id:02X}")))?;
let payload = buf[..PAYLOAD_SIZE].to_vec();
Ok(Self {
id: message_id,
status,
payload,
})
}
}
pub fn parse_device_status(message: &Message) -> Result<DeviceStatus> {
if message.payload.len() < 13 {
return Err(TreadlyError::ParseError(
"Status payload too short".to_string(),
));
}
let mut buf = &message.payload[..];
#[allow(clippy::cast_precision_loss)]
let current_speed = buf.get_u32_le() as f32 / 10.0;
#[allow(clippy::cast_precision_loss)]
let target_speed = buf.get_u32_le() as f32 / 10.0;
let mode_byte = buf.get_u8();
let mode = DeviceMode::from(mode_byte);
let flags = buf.get_u8();
let emergency_stop = if flags & 0x01 != 0 {
EmergencyStopState::Active
} else if flags & 0x02 != 0 {
EmergencyStopState::ResetRequired
} else {
EmergencyStopState::Normal
};
let power_on = flags & 0x04 != 0;
let handrail_enabled = flags & 0x08 != 0;
let session_active = flags & 0x10 != 0;
let distance = if buf.remaining() >= 4 {
#[allow(clippy::cast_precision_loss)]
{
buf.get_u32_le() as f32 / 100.0
}
} else {
0.0
};
let steps = if buf.remaining() >= 4 {
buf.get_u32_le()
} else {
0
};
let temperature = if buf.remaining() >= 2 {
f32::from(buf.get_u16_le()) / 10.0
} else {
0.0
};
let temperature_status = if buf.remaining() >= 1 {
TemperatureStatus::from(buf.get_u8())
} else {
TemperatureStatus::Normal
};
let device_status_code = if buf.remaining() >= 1 {
DeviceStatusCode::from(buf.get_u8())
} else {
DeviceStatusCode::NoError
};
let speed = SpeedInfo::new(
current_speed,
target_speed,
0.0,
20.0, SpeedUnit::Kilometers, );
Ok(DeviceStatus {
speed,
mode,
emergency_stop,
power_on,
handrail_enabled,
distance,
steps,
temperature,
temperature_status,
device_status_code,
connection_health: ConnectionHealth::Healthy, authentication: AuthenticationStatus::NotAuthenticated, session_active,
timestamp: SystemTime::now(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_serialization() {
let msg = Message::command(MessageId::Power);
let bytes = msg.to_bytes();
assert_eq!(bytes.len(), MESSAGE_SIZE);
assert_eq!(bytes[0], MessageId::Power as u8);
assert_eq!(bytes[1], StatusCode::Request as u8);
assert_eq!(bytes[2], MessageId::Power as u8);
}
#[test]
fn test_message_deserialization() {
let original = Message::command(MessageId::Status);
let bytes = original.to_bytes();
let parsed = Message::from_bytes(&bytes).unwrap();
assert_eq!(parsed.id, original.id);
assert_eq!(parsed.status, original.status);
}
#[test]
fn test_speed_message() {
let msg = Message::set_speed(5.5);
let bytes = msg.to_bytes();
assert_eq!(&bytes[3..7], &55u32.to_le_bytes());
}
#[test]
fn test_authentication_message() {
let msg = Message::authenticate();
assert_eq!(&msg.payload[0..4], &AUTH_SECRET_KEY);
}
#[test]
fn test_secure_authentication_verify_message() {
let hash = [
0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
0x77, 0x88,
];
let msg = Message::secure_authenticate_verify(hash);
assert_eq!(msg.id, MessageId::SecureAuthenticateVerify);
assert_eq!(&msg.payload[0..16], &hash);
}
#[test]
fn test_mac_address_verification_message() {
let mac_bytes = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC];
let msg = Message::verify_mac_address(mac_bytes);
assert_eq!(msg.id, MessageId::VerifyMacAddress);
assert_eq!(&msg.payload[0..6], &mac_bytes);
}
#[test]
fn test_handrail_message() {
let enabled_msg = Message::set_handrail_enabled(true);
assert_eq!(enabled_msg.payload[0], 1);
let disabled_msg = Message::set_handrail_enabled(false);
assert_eq!(disabled_msg.payload[0], 0);
}
}