use crate::codec::{EpsonCodec, fit_to_width, truncate_chars};
use crate::config::{DisplaySettings, VfdConfig};
use crate::error::{Result, VfdError};
use serialport::SerialPort;
use std::io::Write;
pub struct Vfd<T: Write = Box<dyn SerialPort>> {
transport: T,
codec: EpsonCodec,
}
impl Vfd<Box<dyn SerialPort>> {
pub fn open(cfg: VfdConfig) -> Result<Self> {
cfg.validate()?;
let serial = cfg.serial;
let display = cfg.display;
let mut builder = serialport::new(&serial.port_name, serial.baud_rate)
.data_bits(serial.data_bits)
.parity(serial.parity)
.stop_bits(serial.stop_bits)
.flow_control(serial.flow_control)
.timeout(serial.timeout);
#[cfg(unix)]
{
builder = builder.exclusive(serial.exclusive);
}
let port = builder.open()?;
Self::from_transport(port, display)
}
}
impl<T: Write> Vfd<T> {
pub fn from_transport(mut transport: T, display: DisplaySettings) -> Result<Self> {
display.validate()?;
let codec = EpsonCodec::new(display)?;
let init = codec.init();
if !init.is_empty() {
transport.write_all(&init)?;
}
Ok(Self { transport, codec })
}
pub fn display(&self) -> &DisplaySettings {
self.codec.display()
}
pub fn columns(&self) -> usize {
self.display().columns
}
pub fn rows(&self) -> usize {
self.display().rows
}
pub fn clear(&mut self) -> Result<()> {
self.transport.write_all(&self.codec.clear())?;
Ok(())
}
pub fn print_line(&mut self, line: u8, text: &str) -> Result<()> {
self.codec.validate_line(line)?;
self.goto_xy(1, line)?;
let fitted = self.codec.fit_line(text);
self.write_text(&fitted)
}
pub fn print_frame(&mut self, line: u8, frame: &str) -> Result<()> {
self.codec.validate_line(line)?;
self.goto_xy(1, line)?;
let fitted = fit_to_width(frame, self.columns());
self.write_text(&fitted)
}
pub fn print_at(&mut self, x: u8, y: u8, text: &str) -> Result<()> {
self.print_at_prepared(x, y, text)
}
pub(crate) fn print_at_prepared(&mut self, x: u8, y: u8, text: &str) -> Result<()> {
self.codec.validate_xy(x, y)?;
let remaining = self.columns() - usize::from(x) + 1;
let text = truncate_chars(text, remaining).to_string();
self.goto_xy(x, y)?;
self.write_text(&text)
}
pub fn write_raw(&mut self, bytes: &[u8]) -> Result<()> {
self.transport.write_all(bytes)?;
Ok(())
}
pub fn set_brightness(&mut self, level: u8) -> Result<()> {
let cmd = self.codec.brightness(level)?;
self.transport.write_all(&cmd)?;
self.transport.flush()?;
let settle = self.display().brightness_settle;
if !settle.is_zero() {
std::thread::sleep(settle);
}
Ok(())
}
pub fn flush(&mut self) -> Result<()> {
self.transport.flush()?;
Ok(())
}
pub fn into_inner(self) -> T {
self.transport
}
fn goto_xy(&mut self, x: u8, y: u8) -> Result<()> {
let cmd = self.codec.goto_xy(x, y)?;
self.transport.write_all(&cmd)?;
Ok(())
}
fn write_text(&mut self, s: &str) -> Result<()> {
let bytes = self.codec.encode_text(s);
self.transport.write_all(&bytes)?;
Ok(())
}
}
impl From<std::convert::Infallible> for VfdError {
fn from(value: std::convert::Infallible) -> Self {
match value {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{DisplaySettings, Preset, TextEncoding, VfdConfig};
#[test]
fn mock_transport_gets_legacy_preset_bytes() {
let cfg = VfdConfig::preset("test", Preset::Epson20x2Cp866).unwrap();
let mut vfd = Vfd::from_transport(Vec::<u8>::new(), cfg.display).unwrap();
vfd.clear().unwrap();
vfd.print_line(1, "Привет").unwrap();
vfd.print_at(20, 2, "X!").unwrap();
vfd.set_brightness(4).unwrap();
let bytes = vfd.into_inner();
let mut expected = vec![0x1B, 0x40, 0x1B, 0x74, 6, 0x0C];
expected.extend_from_slice(&[0x1F, 0x24, 1, 1]);
expected.extend_from_slice(&encoding_rs::IBM866.encode("Привет ").0);
expected.extend_from_slice(&[0x1F, 0x24, 20, 2, b'X']);
expected.extend_from_slice(&[0x1F, 0x58, 4]);
assert_eq!(bytes, expected);
}
#[test]
fn raw_write_bypasses_encoding() {
let display = DisplaySettings::new(20, 2, TextEncoding::Ascii);
let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
vfd.write_raw(&[0x1B, b'?', 0xFF]).unwrap();
assert_eq!(vfd.into_inner(), vec![0x1B, 0x40, 0x1B, b'?', 0xFF]);
}
#[test]
fn invalid_coordinates_and_brightness_are_errors() {
let display = DisplaySettings::new(20, 3, TextEncoding::Cp866);
let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
assert!(matches!(
vfd.print_line(4, "bad"),
Err(VfdError::InvalidLine { .. })
));
assert!(matches!(
vfd.print_at(21, 1, "bad"),
Err(VfdError::InvalidCoordinate { .. })
));
assert!(matches!(
vfd.set_brightness(5),
Err(VfdError::UnsupportedBrightness { .. })
));
}
#[test]
fn alternate_encodings_are_selectable_manually() {
let display = DisplaySettings::new(4, 1, TextEncoding::Windows1251);
let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
vfd.print_line(1, "т").unwrap();
assert_eq!(
vfd.into_inner(),
vec![0x1B, 0x40, 0x1F, 0x24, 1, 1, 0xF2, b' ', b' ', b' ']
);
}
#[test]
fn high_level_text_neutralizes_protocol_controls() {
let display = DisplaySettings::new(8, 1, TextEncoding::Ascii);
let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
vfd.print_line(1, "A\u{1b}@B\u{0c}C").unwrap();
assert_eq!(&vfd.into_inner()[6..], b"A @B C ");
}
}