use crate::error::ConfigError;
use serialport::{DataBits, FlowControl, Parity, StopBits};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Preset {
Epson20x2Cp866,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SerialSettings {
pub port_name: String,
pub baud_rate: u32,
pub data_bits: DataBits,
pub parity: Parity,
pub stop_bits: StopBits,
pub flow_control: FlowControl,
pub timeout: Duration,
#[cfg(unix)]
pub exclusive: bool,
}
impl SerialSettings {
pub fn new(port_name: impl Into<String>, baud_rate: u32) -> Self {
Self {
port_name: port_name.into(),
baud_rate,
data_bits: DataBits::Eight,
parity: Parity::None,
stop_bits: StopBits::One,
flow_control: FlowControl::None,
timeout: Duration::from_millis(100),
#[cfg(unix)]
exclusive: true,
}
}
pub fn validate(&self) -> std::result::Result<(), ConfigError> {
if self.port_name.trim().is_empty() {
return Err(ConfigError::EmptyPortName);
}
if self.baud_rate == 0 {
return Err(ConfigError::ZeroBaudRate);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextEncoding {
Cp866,
Windows1251,
Ascii,
Utf8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DisplaySettings {
pub columns: usize,
pub rows: usize,
pub encoding: TextEncoding,
pub code_table: Option<u8>,
pub reset_on_open: bool,
pub brightness: Option<std::ops::RangeInclusive<u8>>,
pub brightness_settle: Duration,
}
impl DisplaySettings {
pub fn new(columns: usize, rows: usize, encoding: TextEncoding) -> Self {
Self {
columns,
rows,
encoding,
code_table: None,
reset_on_open: true,
brightness: Some(1..=4),
brightness_settle: Duration::from_millis(2),
}
}
pub fn validate(&self) -> std::result::Result<(), ConfigError> {
if !(1..=u8::MAX as usize).contains(&self.columns) {
return Err(ConfigError::InvalidColumns(self.columns));
}
if !(1..=u8::MAX as usize).contains(&self.rows) {
return Err(ConfigError::InvalidRows(self.rows));
}
if let Some(range) = &self.brightness {
let min = *range.start();
let max = *range.end();
if min == 0 || min > max {
return Err(ConfigError::InvalidBrightnessRange { min, max });
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VfdConfig {
pub serial: SerialSettings,
pub display: DisplaySettings,
pub queue_capacity: usize,
}
impl VfdConfig {
pub fn new(
serial: SerialSettings,
display: DisplaySettings,
) -> std::result::Result<Self, ConfigError> {
let cfg = Self {
serial,
display,
queue_capacity: 32,
};
cfg.validate()?;
Ok(cfg)
}
pub fn preset(
port_name: impl Into<String>,
preset: Preset,
) -> std::result::Result<Self, ConfigError> {
match preset {
Preset::Epson20x2Cp866 => {
let serial = SerialSettings::new(port_name, 9600);
let mut display = DisplaySettings::new(20, 2, TextEncoding::Cp866);
display.code_table = Some(6);
display.reset_on_open = true;
display.brightness = Some(1..=4);
display.brightness_settle = Duration::from_millis(2);
Self::new(serial, display)
}
}
}
pub fn with_queue_capacity(
mut self,
capacity: usize,
) -> std::result::Result<Self, ConfigError> {
self.queue_capacity = capacity;
self.validate()?;
Ok(self)
}
pub fn validate(&self) -> std::result::Result<(), ConfigError> {
self.serial.validate()?;
self.display.validate()?;
if self.queue_capacity == 0 {
return Err(ConfigError::ZeroQueueCapacity);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn epson_preset_matches_legacy_settings() {
let cfg = VfdConfig::preset("COM1", Preset::Epson20x2Cp866).unwrap();
assert_eq!(cfg.serial.port_name, "COM1");
assert_eq!(cfg.serial.baud_rate, 9600);
assert_eq!(cfg.serial.data_bits, DataBits::Eight);
assert_eq!(cfg.serial.parity, Parity::None);
assert_eq!(cfg.serial.stop_bits, StopBits::One);
assert_eq!(cfg.serial.flow_control, FlowControl::None);
assert_eq!(cfg.serial.timeout, Duration::from_millis(100));
assert_eq!(cfg.display.columns, 20);
assert_eq!(cfg.display.rows, 2);
assert_eq!(cfg.display.encoding, TextEncoding::Cp866);
assert_eq!(cfg.display.code_table, Some(6));
assert!(cfg.display.reset_on_open);
assert_eq!(cfg.display.brightness, Some(1..=4));
}
#[test]
fn manual_config_has_no_hidden_code_table_or_baud() {
let serial = SerialSettings::new("/tmp/tty", 19_200);
let display = DisplaySettings::new(16, 4, TextEncoding::Windows1251);
let cfg = VfdConfig::new(serial, display).unwrap();
assert_eq!(cfg.serial.baud_rate, 19_200);
assert_eq!(cfg.display.columns, 16);
assert_eq!(cfg.display.rows, 4);
assert_eq!(cfg.display.code_table, None);
}
#[test]
fn invalid_config_values_are_rejected() {
assert!(matches!(
SerialSettings::new("", 9600).validate(),
Err(ConfigError::EmptyPortName)
));
assert!(matches!(
SerialSettings::new("p", 0).validate(),
Err(ConfigError::ZeroBaudRate)
));
assert!(matches!(
DisplaySettings::new(0, 2, TextEncoding::Cp866).validate(),
Err(ConfigError::InvalidColumns(0))
));
assert!(matches!(
DisplaySettings::new(20, 256, TextEncoding::Cp866).validate(),
Err(ConfigError::InvalidRows(256))
));
let mut display = DisplaySettings::new(20, 2, TextEncoding::Cp866);
let min = 4;
let max = 1;
display.brightness = Some(min..=max);
assert!(matches!(
display.validate(),
Err(ConfigError::InvalidBrightnessRange { min: 4, max: 1 })
));
let cfg = VfdConfig::preset("COM1", Preset::Epson20x2Cp866).unwrap();
assert!(matches!(
cfg.with_queue_capacity(0),
Err(ConfigError::ZeroQueueCapacity)
));
}
}