use std::path::{Path, PathBuf};
use std::time::Duration;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Parity {
#[default]
None,
Even,
Odd,
Mark,
Space,
}
impl Parity {
pub fn bits(&self) -> u32 {
match self {
Parity::None => 0,
_ => 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum StopBits {
#[default]
One,
OnePointFive,
Two,
}
impl StopBits {
pub fn bits(&self) -> f32 {
match self {
StopBits::One => 1.0,
StopBits::OnePointFive => 1.5,
StopBits::Two => 2.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DataBits {
Five = 5,
Six = 6,
Seven = 7,
Eight = 8,
}
impl Default for DataBits {
fn default() -> Self {
Self::Eight
}
}
impl DataBits {
pub fn bits(&self) -> u32 {
*self as u32
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerialConfig {
pub baud_rate: u32,
pub data_bits: DataBits,
pub parity: Parity,
pub stop_bits: StopBits,
pub flow_control: bool,
}
impl Default for SerialConfig {
fn default() -> Self {
Self {
baud_rate: 9600,
data_bits: DataBits::Eight,
parity: Parity::None,
stop_bits: StopBits::One,
flow_control: false,
}
}
}
impl SerialConfig {
pub fn new(baud_rate: u32) -> Self {
Self {
baud_rate,
..Default::default()
}
}
pub fn baud_9600() -> Self {
Self::new(9600)
}
pub fn baud_19200() -> Self {
Self::new(19200)
}
pub fn baud_38400() -> Self {
Self::new(38400)
}
pub fn baud_115200() -> Self {
Self::new(115200)
}
pub fn with_parity(mut self, parity: Parity) -> Self {
self.parity = parity;
self
}
pub fn with_stop_bits(mut self, stop_bits: StopBits) -> Self {
self.stop_bits = stop_bits;
self
}
pub fn with_data_bits(mut self, data_bits: DataBits) -> Self {
self.data_bits = data_bits;
self
}
pub fn bits_per_character(&self) -> f32 {
1.0 + self.data_bits.bits() as f32
+ self.parity.bits() as f32
+ self.stop_bits.bits()
}
pub fn char_time(&self) -> Duration {
let bits = self.bits_per_character();
let seconds = bits / self.baud_rate as f32;
Duration::from_secs_f32(seconds)
}
pub fn transmission_time(&self, bytes: usize) -> Duration {
self.char_time() * bytes as u32
}
pub fn inter_frame_timeout(&self) -> Duration {
if self.baud_rate > 19200 {
Duration::from_micros(1750)
} else {
let char_time = self.char_time();
Duration::from_secs_f32(char_time.as_secs_f32() * 3.5)
}
}
pub fn inter_char_timeout(&self) -> Duration {
if self.baud_rate > 19200 {
Duration::from_micros(750)
} else {
let char_time = self.char_time();
Duration::from_secs_f32(char_time.as_secs_f32() * 1.5)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VirtualSerialConfig {
#[serde(flatten)]
pub serial: SerialConfig,
pub slave_path: PathBuf,
pub symlink_path: Option<PathBuf>,
pub simulate_delays: bool,
pub buffer_size: usize,
}
impl Default for VirtualSerialConfig {
fn default() -> Self {
Self {
serial: SerialConfig::default(),
slave_path: PathBuf::from("/tmp/modbus_rtu_slave"),
symlink_path: None,
simulate_delays: true,
buffer_size: 4096,
}
}
}
impl VirtualSerialConfig {
pub fn new<P: AsRef<Path>>(slave_path: P) -> Self {
Self {
slave_path: slave_path.as_ref().to_path_buf(),
..Default::default()
}
}
pub fn with_baud_rate(mut self, baud_rate: u32) -> Self {
self.serial.baud_rate = baud_rate;
self
}
pub fn with_slave_path<P: AsRef<Path>>(mut self, path: P) -> Self {
self.slave_path = path.as_ref().to_path_buf();
self
}
pub fn with_symlink<P: AsRef<Path>>(mut self, path: P) -> Self {
self.symlink_path = Some(path.as_ref().to_path_buf());
self
}
pub fn with_serial_config(mut self, config: SerialConfig) -> Self {
self.serial = config;
self
}
pub fn with_delay_simulation(mut self, enabled: bool) -> Self {
self.simulate_delays = enabled;
self
}
pub fn with_buffer_size(mut self, size: usize) -> Self {
self.buffer_size = size;
self
}
}
#[derive(Debug, Error)]
pub enum VirtualSerialError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to create PTY: {0}")]
PtyCreation(String),
#[error("Failed to create symlink: {source}")]
Symlink {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Virtual serial port not available on this platform")]
NotAvailable,
#[error("Port already in use: {0}")]
InUse(PathBuf),
}
#[derive(Debug)]
pub struct VirtualSerial {
config: VirtualSerialConfig,
inner: VirtualSerialInner,
}
#[cfg(unix)]
#[derive(Debug)]
struct VirtualSerialInner {
master_fd: std::os::unix::io::RawFd,
slave_device_path: PathBuf,
has_symlink: bool,
}
#[cfg(not(unix))]
#[derive(Debug)]
struct VirtualSerialInner {
_marker: std::marker::PhantomData<()>,
}
impl VirtualSerial {
#[cfg(unix)]
pub fn create(config: VirtualSerialConfig) -> Result<Self, VirtualSerialError> {
use std::os::unix::io::AsRawFd;
let master =
nix::pty::posix_openpt(nix::fcntl::OFlag::O_RDWR | nix::fcntl::OFlag::O_NOCTTY)
.map_err(|e| VirtualSerialError::PtyCreation(e.to_string()))?;
nix::pty::grantpt(&master)
.map_err(|e| VirtualSerialError::PtyCreation(format!("grantpt failed: {}", e)))?;
nix::pty::unlockpt(&master)
.map_err(|e| VirtualSerialError::PtyCreation(format!("unlockpt failed: {}", e)))?;
let slave_name = unsafe {
nix::pty::ptsname(&master)
.map_err(|e| VirtualSerialError::PtyCreation(format!("ptsname failed: {}", e)))?
};
let slave_device_path = PathBuf::from(&slave_name);
let has_symlink = if let Some(ref symlink_path) = config.symlink_path {
let _ = std::fs::remove_file(symlink_path);
std::os::unix::fs::symlink(&slave_device_path, symlink_path).map_err(|e| {
VirtualSerialError::Symlink {
path: symlink_path.clone(),
source: e,
}
})?;
true
} else if config.slave_path != slave_device_path {
let _ = std::fs::remove_file(&config.slave_path);
std::os::unix::fs::symlink(&slave_device_path, &config.slave_path).map_err(|e| {
VirtualSerialError::Symlink {
path: config.slave_path.clone(),
source: e,
}
})?;
true
} else {
false
};
let inner = VirtualSerialInner {
master_fd: master.as_raw_fd(),
slave_device_path,
has_symlink,
};
std::mem::forget(master);
Ok(Self { config, inner })
}
#[cfg(not(unix))]
pub fn create(_config: VirtualSerialConfig) -> Result<Self, VirtualSerialError> {
Err(VirtualSerialError::NotAvailable)
}
pub fn slave_path(&self) -> &Path {
&self.config.slave_path
}
#[cfg(unix)]
pub fn device_path(&self) -> &Path {
&self.inner.slave_device_path
}
pub fn config(&self) -> &VirtualSerialConfig {
&self.config
}
pub fn serial_config(&self) -> &SerialConfig {
&self.config.serial
}
pub fn transmission_delay(&self, bytes: usize) -> Duration {
if self.config.simulate_delays {
self.config.serial.transmission_time(bytes)
} else {
Duration::ZERO
}
}
#[cfg(unix)]
pub fn master_fd(&self) -> std::os::unix::io::RawFd {
self.inner.master_fd
}
#[cfg(unix)]
pub fn into_async_io(self) -> std::io::Result<tokio::fs::File> {
use std::os::unix::io::FromRawFd;
let file = unsafe { std::fs::File::from_raw_fd(self.inner.master_fd) };
std::mem::forget(self);
Ok(tokio::fs::File::from_std(file))
}
}
#[cfg(unix)]
impl Drop for VirtualSerial {
fn drop(&mut self) {
if self.inner.has_symlink {
if let Some(ref symlink_path) = self.config.symlink_path {
let _ = std::fs::remove_file(symlink_path);
} else {
let _ = std::fs::remove_file(&self.config.slave_path);
}
}
unsafe {
libc::close(self.inner.master_fd);
}
}
}
#[derive(Debug)]
pub struct SerialPair {
#[allow(dead_code)]
server_tx: tokio::sync::mpsc::Sender<Vec<u8>>,
#[allow(dead_code)]
server_rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
#[allow(dead_code)]
client_tx: tokio::sync::mpsc::Sender<Vec<u8>>,
#[allow(dead_code)]
client_rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
#[allow(dead_code)]
config: SerialConfig,
}
impl SerialPair {
pub fn new(config: SerialConfig) -> (SerialPairServer, SerialPairClient) {
let (server_tx, client_rx) = tokio::sync::mpsc::channel(256);
let (client_tx, server_rx) = tokio::sync::mpsc::channel(256);
let server = SerialPairServer {
tx: server_tx,
rx: server_rx,
config: config.clone(),
};
let client = SerialPairClient {
tx: client_tx,
rx: client_rx,
config,
};
(server, client)
}
}
#[derive(Debug)]
pub struct SerialPairServer {
tx: tokio::sync::mpsc::Sender<Vec<u8>>,
rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
config: SerialConfig,
}
impl SerialPairServer {
pub async fn send(
&self,
data: &[u8],
) -> Result<(), tokio::sync::mpsc::error::SendError<Vec<u8>>> {
if self.config.baud_rate > 0 {
let delay = self.config.transmission_time(data.len());
tokio::time::sleep(delay).await;
}
self.tx.send(data.to_vec()).await
}
pub async fn recv(&mut self) -> Option<Vec<u8>> {
self.rx.recv().await
}
pub fn try_recv(&mut self) -> Result<Vec<u8>, tokio::sync::mpsc::error::TryRecvError> {
self.rx.try_recv()
}
}
#[derive(Debug)]
pub struct SerialPairClient {
tx: tokio::sync::mpsc::Sender<Vec<u8>>,
rx: tokio::sync::mpsc::Receiver<Vec<u8>>,
config: SerialConfig,
}
impl SerialPairClient {
pub async fn send(
&self,
data: &[u8],
) -> Result<(), tokio::sync::mpsc::error::SendError<Vec<u8>>> {
if self.config.baud_rate > 0 {
let delay = self.config.transmission_time(data.len());
tokio::time::sleep(delay).await;
}
self.tx.send(data.to_vec()).await
}
pub async fn recv(&mut self) -> Option<Vec<u8>> {
self.rx.recv().await
}
pub fn try_recv(&mut self) -> Result<Vec<u8>, tokio::sync::mpsc::error::TryRecvError> {
self.rx.try_recv()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serial_config_defaults() {
let config = SerialConfig::default();
assert_eq!(config.baud_rate, 9600);
assert_eq!(config.data_bits, DataBits::Eight);
assert_eq!(config.parity, Parity::None);
assert_eq!(config.stop_bits, StopBits::One);
}
#[test]
fn test_bits_per_character() {
let config = SerialConfig::default();
assert_eq!(config.bits_per_character(), 10.0);
let config = SerialConfig::default().with_parity(Parity::Even);
assert_eq!(config.bits_per_character(), 11.0);
let config = SerialConfig::default().with_stop_bits(StopBits::Two);
assert_eq!(config.bits_per_character(), 11.0);
}
#[test]
fn test_char_time() {
let config = SerialConfig::new(9600);
let char_time = config.char_time();
let ms = char_time.as_micros();
assert!(ms > 1000 && ms < 1100);
}
#[test]
fn test_inter_frame_timeout() {
let config = SerialConfig::new(9600);
let timeout = config.inter_frame_timeout();
let ms = timeout.as_micros();
assert!(ms > 3500 && ms < 4000);
let config = SerialConfig::new(115200);
let timeout = config.inter_frame_timeout();
assert_eq!(timeout, Duration::from_micros(1750));
}
#[test]
fn test_virtual_serial_config() {
let config = VirtualSerialConfig::default()
.with_baud_rate(19200)
.with_slave_path("/tmp/test_serial")
.with_delay_simulation(true);
assert_eq!(config.serial.baud_rate, 19200);
assert_eq!(config.slave_path, PathBuf::from("/tmp/test_serial"));
assert!(config.simulate_delays);
}
#[tokio::test]
async fn test_serial_pair() {
let (server, mut client) = SerialPair::new(SerialConfig::new(115200));
let data = vec![0x01, 0x02, 0x03];
server.send(&data).await.unwrap();
let received = client.recv().await.unwrap();
assert_eq!(received, data);
}
#[tokio::test]
async fn test_serial_pair_bidirectional() {
let (mut server, mut client) = SerialPair::new(SerialConfig::new(115200));
let client_data = vec![0x01, 0x03, 0x00, 0x00, 0x00, 0x0A];
client.send(&client_data).await.unwrap();
let received = server.recv().await.unwrap();
assert_eq!(received, client_data);
let server_data = vec![0x01, 0x03, 0x14]; server.send(&server_data).await.unwrap();
let received = client.recv().await.unwrap();
assert_eq!(received, server_data);
}
}