use log::debug;
use serde::{Deserialize, Serialize};
use std::io::Write;
use std::time::Duration;
use std::{cmp::min, fmt};
use thiserror::Error;
use crate::{OpenError, PortListing, WriteError};
use super::DmxPort;
use serialport::{SerialPort, SerialPortInfo, SerialPortType, UsbPortInfo};
const START_VAL: u8 = 0x7E;
const END_VAL: u8 = 0xE7;
const MIN_UNIVERSE_SIZE: usize = 24;
const MAX_UNIVERSE_SIZE: usize = 512;
const SET_PARAMETERS: u8 = 4;
const SEND_DMX_PACKET: u8 = 6;
fn write_packet<W: Write>(
message_type: u8,
payload: &[u8],
add_payload_pad_byte: bool,
mut w: W,
) -> Result<(), WriteError> {
let payload_size = payload.len() + add_payload_pad_byte as usize;
let (len_lsb, len_msb) = (payload_size as u8, (payload_size >> 8) as u8);
let header = [START_VAL, message_type, len_lsb, len_msb];
let mut write_all = |buf| -> Result<(), EnttecWriteError> {
w.write_all(buf)?;
Ok(())
};
write_all(&header)?;
if add_payload_pad_byte {
write_all(&[0][..])?;
}
write_all(payload)?;
write_all(&[END_VAL][..])?;
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EnttecParams {
break_time: u8,
mark_after_break_time: u8,
output_rate: u8,
}
impl Default for EnttecParams {
fn default() -> Self {
EnttecParams {
break_time: 9,
mark_after_break_time: 1,
output_rate: 40,
}
}
}
impl EnttecParams {
fn write_into<W: Write>(&self, w: W) -> Result<(), WriteError> {
let payload = [
0, 0, self.break_time,
self.mark_after_break_time,
self.output_rate,
];
write_packet(SET_PARAMETERS, &payload, false, w)
}
}
#[derive(Serialize, Deserialize)]
pub struct EnttecDmxPort {
params: EnttecParams,
#[serde(skip)]
port: Option<Box<dyn SerialPort>>,
#[serde(with = "SerialPortInfoDef")]
info: SerialPortInfo,
}
impl EnttecDmxPort {
pub fn available_ports() -> anyhow::Result<PortListing> {
Ok(serialport::available_ports()?
.into_iter()
.filter(is_enttec)
.map(|info| Box::new(EnttecDmxPort::new(info)) as Box<dyn DmxPort>)
.collect())
}
pub fn new(info: SerialPortInfo) -> Self {
let params = EnttecParams::default();
Self {
params,
port: None,
info,
}
}
pub fn opened(info: SerialPortInfo) -> anyhow::Result<Self> {
let mut port = Self::new(info);
port.open()?;
Ok(port)
}
fn write_params(&mut self) -> Result<(), WriteError> {
self.params
.write_into(self.port.as_mut().ok_or(WriteError::Disconnected)?)?;
Ok(())
}
}
#[typetag::serde]
impl DmxPort for EnttecDmxPort {
fn open(&mut self) -> Result<(), OpenError> {
if self.port.is_some() {
return Ok(());
}
let port = match serialport::new(&self.info.port_name, 57600)
.timeout(Duration::from_millis(1))
.open()
{
Ok(port) => port,
Err(err) => {
if let serialport::ErrorKind::Io(std::io::ErrorKind::NotFound) = err.kind() {
return Err(OpenError::NotConnected);
} else {
return Err(OpenError::Other(err.into()));
}
}
};
self.port = Some(port);
if let Err(e) = self.write_params() {
self.port = None;
return Err(OpenError::Other(e.into()));
}
Ok(())
}
fn close(&mut self) {
self.port = None;
}
fn write(&mut self, frame: &[u8]) -> Result<(), WriteError> {
if self.port.is_none()
&& let Err(err) = self.open()
{
debug!("Failed to reopen DMX port {}: {:#?}.", self, err);
return Err(WriteError::Disconnected);
}
let port = self.port.as_mut().ok_or(WriteError::Disconnected)?;
let size = frame.len();
let write_result = if size < MIN_UNIVERSE_SIZE {
let mut padded_frame = Vec::with_capacity(MIN_UNIVERSE_SIZE);
padded_frame.extend_from_slice(frame);
padded_frame.resize(MIN_UNIVERSE_SIZE, 0);
write_packet(SEND_DMX_PACKET, &padded_frame, true, port)
} else {
write_packet(
SEND_DMX_PACKET,
&frame[0..min(size, MAX_UNIVERSE_SIZE)],
true,
port,
)
};
if let Err(WriteError::Disconnected) = write_result {
self.port = None;
}
write_result
}
}
impl fmt::Display for EnttecDmxPort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let SerialPortType::UsbPort(p) = &self.info.port_type
&& let Some(sn) = &p.serial_number
{
return write!(f, "Enttec DMX USB PRO {}", sn);
}
write!(f, "Enttec DMX USB PRO {}", self.info.port_name)
}
}
#[cfg(unix)]
fn is_enttec(info: &SerialPortInfo) -> bool {
let SerialPortType::UsbPort(details) = &info.port_type else {
return false;
};
let Some(product) = &details.product else {
return false;
};
product == "DMX USB PRO" && info.port_name.contains("tty")
}
#[cfg(windows)]
fn is_enttec(info: &SerialPortInfo) -> bool {
let SerialPortType::UsbPort(details) = &info.port_type else {
return false;
};
let Some(manufacturer) = &details.manufacturer else {
return false;
};
manufacturer == "FTDI"
}
#[derive(Error, Debug)]
#[error(transparent)]
pub struct EnttecWriteError(#[from] std::io::Error);
impl From<EnttecWriteError> for WriteError {
fn from(value: EnttecWriteError) -> Self {
if value.0.kind() == std::io::ErrorKind::BrokenPipe {
Self::Disconnected
} else {
Self::Other(value.0.into())
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(remote = "SerialPortInfo")]
struct SerialPortInfoDef {
pub port_name: String,
#[serde(with = "SerialPortTypeDef")]
pub port_type: SerialPortType,
}
#[derive(Serialize, Deserialize)]
#[serde(remote = "SerialPortType")]
pub enum SerialPortTypeDef {
#[serde(with = "UsbPortInfoDef")]
UsbPort(UsbPortInfo),
PciPort,
BluetoothPort,
Unknown,
}
#[derive(Serialize, Deserialize)]
#[serde(remote = "UsbPortInfo")]
pub struct UsbPortInfoDef {
pub vid: u16,
pub pid: u16,
pub serial_number: Option<String>,
pub manufacturer: Option<String>,
pub product: Option<String>,
}