use crate::error::{ProblemJson, ViiperError};
use crate::types::*;
use std::io::{Read, Write};
use std::net::TcpStream;
pub struct ViiperClient {
addr: String,
}
impl ViiperClient {
pub fn new(host: impl Into<String>, port: u16) -> Self {
Self {
addr: format!("{}:{}", host.into(), port),
}
}
fn do_request<T: for<'de> serde::Deserialize<'de>>(
&self,
path: &str,
payload: Option<&str>,
) -> Result<T, ViiperError> {
let mut stream = TcpStream::connect(&self.addr)?;
stream.write_all(path.as_bytes())?;
if let Some(p) = payload {
stream.write_all(b" ")?;
stream.write_all(p.as_bytes())?;
}
stream.write_all(b"\0")?;
let mut buf = Vec::new();
stream.read_to_end(&mut buf)?;
let response = String::from_utf8(buf)
.map_err(|_| ViiperError::UnexpectedResponse("invalid UTF-8".into()))?
.trim_end_matches('\n')
.to_string();
if response.starts_with("{\"status\":") {
let problem: ProblemJson = serde_json::from_str(&response)?;
return Err(ViiperError::Protocol(problem));
}
serde_json::from_str(&response).map_err(Into::into)
}
pub fn bus_list(&self) -> Result<BusListResponse, ViiperError> {
let path = "bus/list";
let payload: Option<String> = None;
self.do_request(&path, payload.as_deref())
}
pub fn bus_create(&self, uint32: Option<u32>) -> Result<BusCreateResponse, ViiperError> {
let path = "bus/create";
let payload = uint32.map(|v| v.to_string());
self.do_request(&path, payload.as_deref())
}
pub fn bus_remove(&self, uint32: Option<u32>) -> Result<BusRemoveResponse, ViiperError> {
let path = "bus/remove";
let payload = uint32.map(|v| v.to_string());
self.do_request(&path, payload.as_deref())
}
pub fn bus_devices_list(&self, id: u32) -> Result<DevicesListResponse, ViiperError> {
let path = format!("bus/{}/list", id);
let payload: Option<String> = None;
self.do_request(&path, payload.as_deref())
}
pub fn bus_device_add(&self, id: u32, device_create_request: &DeviceCreateRequest) -> Result<Device, ViiperError> {
let path = format!("bus/{}/add", id);
let payload = Some(serde_json::to_string(&device_create_request)?);
self.do_request(&path, payload.as_deref())
}
pub fn bus_device_remove(&self, id: u32, string: Option<&str>) -> Result<DeviceRemoveResponse, ViiperError> {
let path = format!("bus/{}/remove", id);
let payload = string.map(|s| s.to_string());
self.do_request(&path, payload.as_deref())
}
pub fn connect_device(&self, bus_id: u32, dev_id: &str) -> Result<DeviceStream, ViiperError> {
DeviceStream::connect(&self.addr, bus_id, dev_id)
}
}
pub struct DeviceStream {
stream: TcpStream,
output_thread: Option<std::thread::JoinHandle<()>>,
}
impl DeviceStream {
pub fn connect(addr: &str, bus_id: u32, dev_id: &str) -> Result<Self, ViiperError> {
let mut stream = TcpStream::connect(addr)?;
let handshake = format!("bus/{}/{}\0", bus_id, dev_id);
stream.write_all(handshake.as_bytes())?;
Ok(Self {
stream,
output_thread: None,
})
}
pub fn send<T: crate::wire::DeviceInput>(&mut self, input: &T) -> Result<(), ViiperError> {
let bytes = input.to_bytes();
self.stream.write_all(&bytes)?;
Ok(())
}
pub fn on_output<F>(&mut self, mut callback: F) -> Result<(), ViiperError>
where
F: FnMut(&mut dyn std::io::BufRead) -> std::io::Result<()> + Send + 'static,
{
if self.output_thread.is_some() {
return Err(ViiperError::UnexpectedResponse("Output callback already registered".into()));
}
let stream = self.stream.try_clone()?;
let handle = std::thread::spawn(move || {
let mut reader = std::io::BufReader::new(stream);
loop {
match callback(&mut reader) {
Ok(()) => continue,
Err(_) => break,
}
}
});
self.output_thread = Some(handle);
Ok(())
}
pub fn send_raw(&mut self, data: &[u8]) -> Result<(), ViiperError> {
self.stream.write_all(data)?;
Ok(())
}
pub fn read_raw(&mut self, buf: &mut [u8]) -> Result<usize, ViiperError> {
self.stream.read(buf).map_err(Into::into)
}
pub fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), ViiperError> {
self.stream.read_exact(buf).map_err(Into::into)
}
}
impl Drop for DeviceStream {
fn drop(&mut self) {
let _ = self.stream.shutdown(std::net::Shutdown::Both);
if let Some(handle) = self.output_thread.take() {
let _ = handle.join();
}
}
}