use std::time::Duration;
use bytes::{Bytes};
use tracing::{debug, info, trace, warn};
use zkrust_core::{make_commkey, Command, Packet, Session};
use zkrust_transport::{TcpTransport, UdpTransport, Transport};
use zkrust_types::DeviceInfo;
use crate::error::{Error, Result};
pub struct Device {
transport: Box<dyn Transport>,
session: Session,
timeout: Duration,
password: u32, }
impl Device {
pub fn new(ip: impl Into<String>, port: u16) -> Self {
Self {
transport: Box::new(TcpTransport::new(ip, port).with_tcp_wrapper(false)),
session: Session::new(),
timeout: Duration::from_secs(5),
password: 0, }
}
pub fn new_udp(ip: impl Into<String>, port: u16) -> Self {
Self {
transport: Box::new(UdpTransport::new(ip, port)),
session: Session::new(),
timeout: Duration::from_secs(5),
password: 0, }
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_password(mut self, password: u32) -> Self {
self.password = password;
self
}
pub fn is_connected(&self) -> bool {
self.session.is_connected() && self.transport.is_connected()
}
pub async fn connect(&mut self) -> Result<()> {
info!("Connecting to {}...", self.transport.remote_addr());
self.transport.connect().await?;
let packet = Packet::new(Command::Connect, 0, 0);
self.send_packet(&packet).await?;
let response = self.receive_packet().await?;
match response.command {
Command::AckOk => {
let session_id = response.session_id;
self.session.initialize(session_id)?;
info!(
"Connected successfully (session_id={})",
session_id
);
Ok(())
}
Command::AckUnauth => {
info!("Device requires authentication, sending password...");
let session_id = response.session_id;
let auth_key = make_commkey(self.password, session_id, 50);
debug!(
"Auth key: {:02X?} (password={}, session_id={})",
auth_key, self.password, session_id
);
let auth_packet = Packet::with_payload(
Command::Auth,
session_id,
0,
auth_key,
);
self.send_packet(&auth_packet).await?;
let auth_response = self.receive_packet().await?;
match auth_response.command {
Command::AckOk => {
let session_id = auth_response.session_id;
self.session.initialize(session_id)?;
info!(
"Authenticated successfully (session_id={})",
session_id
);
Ok(())
}
Command::AckError => {
Err(Error::InvalidResponse("Authentication failed - incorrect password".into()))
}
_ => Err(Error::InvalidResponse(format!(
"Unexpected auth response: {}",
auth_response.command
))),
}
}
Command::AckError => {
Err(Error::InvalidResponse("Device returned error".into()))
}
_ => Err(Error::InvalidResponse(format!(
"Unexpected response: {}",
response.command
))),
}
}
pub async fn disconnect(&mut self) -> Result<()> {
if !self.is_connected() {
return Ok(());
}
info!("Disconnecting from {}...", self.transport.remote_addr());
let packet = self.create_packet(Command::Exit, Bytes::new());
if let Err(e) = self.send_packet(&packet).await {
warn!("Failed to send EXIT command: {}", e);
}
self.transport.disconnect().await?;
self.session.close();
info!("Disconnected");
Ok(())
}
pub async fn get_device_info(&mut self) -> Result<DeviceInfo> {
self.ensure_connected()?;
debug!("Getting device info...");
let packet = self.create_packet(Command::GetVersion, Bytes::new());
self.send_packet(&packet).await?;
let response = self.receive_packet().await?;
if !response.is_success() {
return Err(Error::InvalidResponse("Failed to get version".into()));
}
let firmware_version = String::from_utf8_lossy(&response.payload).to_string();
let serial_number = "UNKNOWN".to_string();
let info = DeviceInfo::new(serial_number, firmware_version);
debug!("Device info: {}", info);
Ok(info)
}
pub async fn enable_device(&mut self) -> Result<()> {
self.ensure_connected()?;
debug!("Enabling device...");
let packet = self.create_packet(Command::EnableDevice, Bytes::new());
self.send_packet(&packet).await?;
let response = self.receive_packet().await?;
if response.is_success() {
debug!("Device enabled");
Ok(())
} else {
Err(Error::InvalidResponse("Failed to enable device".into()))
}
}
pub async fn disable_device(&mut self) -> Result<()> {
self.ensure_connected()?;
debug!("Disabling device...");
let packet = self.create_packet(Command::DisableDevice, Bytes::new());
self.send_packet(&packet).await?;
let response = self.receive_packet().await?;
if response.is_success() {
debug!("Device disabled");
Ok(())
} else {
Err(Error::InvalidResponse("Failed to disable device".into()))
}
}
pub async fn restart(&mut self) -> Result<()> {
self.ensure_connected()?;
warn!("Restarting device...");
let packet = self.create_packet(Command::Restart, Bytes::new());
self.send_packet(&packet).await?;
self.session.close();
Ok(())
}
pub async fn power_off(&mut self) -> Result<()> {
self.ensure_connected()?;
warn!("Powering off device...");
let packet = self.create_packet(Command::PowerOff, Bytes::new());
self.send_packet(&packet).await?;
self.session.close();
Ok(())
}
fn ensure_connected(&self) -> Result<()> {
if !self.is_connected() {
return Err(Error::NotConnected);
}
Ok(())
}
fn create_packet(&self, command: Command, payload: Bytes) -> Packet {
Packet::with_payload(
command,
self.session.session_id(),
self.session.next_reply_id(),
payload,
)
}
async fn send_packet(&mut self, packet: &Packet) -> Result<()> {
trace!("Sending: {:?}", packet);
let data = packet.encode();
self.transport.send(&data).await?;
Ok(())
}
async fn receive_packet(&mut self) -> Result<Packet> {
let buf = self.transport.receive(self.timeout.as_secs()).await?;
let packet = Packet::decode(buf)?;
trace!("Received: {:?}", packet);
Ok(packet)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_device_create() {
let device = Device::new("192.168.1.201", 4370);
assert!(!device.is_connected());
}
#[tokio::test]
#[ignore] async fn test_device_connect() {
let mut device = Device::new("192.168.1.201", 4370);
device.connect().await.unwrap();
assert!(device.is_connected());
device.disconnect().await.unwrap();
assert!(!device.is_connected());
}
#[tokio::test]
#[ignore] async fn test_device_get_info() {
let mut device = Device::new("192.168.1.201", 4370);
device.connect().await.unwrap();
let info = device.get_device_info().await.unwrap();
println!("{:?}", info);
device.disconnect().await.unwrap();
}
}