Skip to main content

escpos_vfd/
vfd.rs

1//! Синхронный низкоуровневый драйвер поверх любого [`std::io::Write`].
2//!
3//! `Vfd` полезен, когда приложение само управляет потоками и хочет немедленно выполнять
4//! команды на конкретном транспорте. Каждый метод выполняет запись до возврата из
5//! функции. Для фоновой сериализации команд из разных частей приложения используйте
6//! [`crate::VfdWorker`] или `escpos_vfd::tokio::AsyncVfdWorker`.
7
8use crate::codec::{EpsonCodec, fit_to_width, truncate_chars};
9use crate::config::{DisplaySettings, VfdConfig};
10use crate::error::{Result, VfdError};
11use serialport::SerialPort;
12use std::io::Write;
13
14/// Низкоуровневое соединение с дисплеем, владеющее транспортом записи.
15///
16/// Тип транспорта параметризован, поэтому в тестах можно использовать `Vec<u8>`, а в
17/// приложении - serial-порт. Все координаты в публичных методах задаются от единицы.
18/// Тип не синхронизирует доступ между потоками; если один дисплей используют несколько
19/// producer-ов, берите [`crate::VfdWorker`].
20pub struct Vfd<T: Write = Box<dyn SerialPort>> {
21    transport: T,
22    codec: EpsonCodec,
23}
24
25impl Vfd<Box<dyn SerialPort>> {
26    /// Открывает serial-порт и инициализирует дисплей.
27    ///
28    /// Перед открытием выполняется полная проверка [`VfdConfig`]. После успешного
29    /// открытия драйвер отправляет команды инициализации из [`DisplaySettings`]:
30    /// опциональный `ESC @` и опциональный `ESC t n`.
31    ///
32    /// # Блокировка
33    ///
34    /// Метод блокирует текущий поток на открытии serial-порта и записи init-команд.
35    ///
36    /// # Ошибки
37    ///
38    /// Возвращает [`VfdError::Config`] при неверной конфигурации,
39    /// [`VfdError::Serial`] при ошибке открытия порта и [`VfdError::Io`] при ошибке
40    /// записи init-последовательности.
41    pub fn open(cfg: VfdConfig) -> Result<Self> {
42        cfg.validate()?;
43        let serial = cfg.serial;
44        let display = cfg.display;
45
46        let mut builder = serialport::new(&serial.port_name, serial.baud_rate)
47            .data_bits(serial.data_bits)
48            .parity(serial.parity)
49            .stop_bits(serial.stop_bits)
50            .flow_control(serial.flow_control)
51            .timeout(serial.timeout);
52        #[cfg(unix)]
53        {
54            builder = builder.exclusive(serial.exclusive);
55        }
56
57        let port = builder.open()?;
58        Self::from_transport(port, display)
59    }
60}
61
62impl<T: Write> Vfd<T> {
63    /// Создаёт драйвер поверх произвольного транспорта.
64    ///
65    /// Метод сразу записывает init-последовательность в переданный транспорт. Это удобно
66    /// для mock transport в тестах и для нестандартных serial-обёрток.
67    ///
68    /// # Ошибки
69    ///
70    /// Возвращает [`VfdError::Config`], если настройки дисплея неверны, или
71    /// [`VfdError::Io`], если транспорт не принял init-байты.
72    ///
73    /// # Примеры
74    ///
75    /// ```
76    /// use escpos_vfd::{DisplaySettings, TextEncoding, Vfd};
77    ///
78    /// let display = DisplaySettings::new(4, 1, TextEncoding::Ascii);
79    /// let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display)?;
80    /// vfd.print_line(1, "OK")?;
81    /// let bytes = vfd.into_inner();
82    /// assert!(bytes.starts_with(&[0x1B, 0x40]));
83    /// # Ok::<(), escpos_vfd::VfdError>(())
84    /// ```
85    pub fn from_transport(mut transport: T, display: DisplaySettings) -> Result<Self> {
86        display.validate()?;
87        let codec = EpsonCodec::new(display)?;
88        let init = codec.init();
89        if !init.is_empty() {
90            transport.write_all(&init)?;
91        }
92        Ok(Self { transport, codec })
93    }
94
95    /// Возвращает геометрию и настройки дисплея.
96    pub fn display(&self) -> &DisplaySettings {
97        self.codec.display()
98    }
99
100    /// Количество символов в строке.
101    pub fn columns(&self) -> usize {
102        self.display().columns
103    }
104
105    /// Количество строк дисплея.
106    pub fn rows(&self) -> usize {
107        self.display().rows
108    }
109
110    /// Очищает дисплей стандартной командой очистки.
111    ///
112    /// Метод отправляет байт `0x0C`. Он не обновляет внешние кэши приложения и не
113    /// вызывает `flush`; при необходимости вызовите [`Vfd::flush`] явно.
114    ///
115    /// # Ошибки
116    ///
117    /// Возвращает [`VfdError::Io`] при ошибке записи.
118    pub fn clear(&mut self) -> Result<()> {
119        self.transport.write_all(&self.codec.clear())?;
120        Ok(())
121    }
122
123    /// Полностью перезаписывает строку текстом фиксированной ширины.
124    ///
125    /// Текст санитизируется, обрезается по числу символов и дополняется пробелами до
126    /// ширины дисплея, чтобы удалить остаток предыдущего содержимого строки.
127    ///
128    /// # Ошибки
129    ///
130    /// Возвращает [`VfdError::InvalidLine`], если `line` вне диапазона `1..=rows`, или
131    /// [`VfdError::Io`] при ошибке записи.
132    pub fn print_line(&mut self, line: u8, text: &str) -> Result<()> {
133        self.codec.validate_line(line)?;
134        self.goto_xy(1, line)?;
135        let fitted = self.codec.fit_line(text);
136        self.write_text(&fitted)
137    }
138
139    /// Выводит подготовленный кадр с начала строки без предварительной очистки.
140    ///
141    /// В отличие от [`Vfd::print_line`], метод не выполняет типографскую нормализацию до
142    /// подгонки ширины. Управляющие символы всё равно нейтрализуются на этапе кодирования.
143    ///
144    /// # Ошибки
145    ///
146    /// Возвращает [`VfdError::InvalidLine`] или [`VfdError::Io`].
147    pub fn print_frame(&mut self, line: u8, frame: &str) -> Result<()> {
148        self.codec.validate_line(line)?;
149        self.goto_xy(1, line)?;
150        let fitted = fit_to_width(frame, self.columns());
151        self.write_text(&fitted)
152    }
153
154    /// Печатает текст с координаты `(x, y)`, выполняя санацию и обрезку по правому краю.
155    ///
156    /// Координаты задаются от единицы. Если текст длиннее оставшегося места в строке,
157    /// лишние символы отбрасываются, а следующая строка не затрагивается.
158    ///
159    /// # Ошибки
160    ///
161    /// Возвращает [`VfdError::InvalidCoordinate`] или [`VfdError::Io`].
162    pub fn print_at(&mut self, x: u8, y: u8, text: &str) -> Result<()> {
163        self.print_at_prepared(x, y, text)
164    }
165
166    /// Печатает уже подготовленный текст; управляющие символы нейтрализуются codec-ом.
167    pub(crate) fn print_at_prepared(&mut self, x: u8, y: u8, text: &str) -> Result<()> {
168        self.codec.validate_xy(x, y)?;
169        let remaining = self.columns() - usize::from(x) + 1;
170        let text = truncate_chars(text, remaining).to_string();
171        self.goto_xy(x, y)?;
172        self.write_text(&text)
173    }
174
175    /// Записывает байты напрямую без кодировки и проверки содержимого.
176    ///
177    /// Используйте этот escape hatch только для команд, которых нет в типизированном API,
178    /// или для нестандартных таблиц символов конкретного устройства.
179    ///
180    /// # Ошибки
181    ///
182    /// Возвращает [`VfdError::Io`] при ошибке записи.
183    pub fn write_raw(&mut self, bytes: &[u8]) -> Result<()> {
184        self.transport.write_all(bytes)?;
185        Ok(())
186    }
187
188    /// Устанавливает яркость.
189    ///
190    /// Команда кодируется как `US X n` (`0x1F 0x58 level`). После записи выполняется
191    /// `flush`, затем sync sleep на `display.brightness_settle`, если задержка не нулевая.
192    ///
193    /// # Ошибки
194    ///
195    /// Возвращает [`VfdError::UnsupportedBrightness`], если уровень вне настроенного
196    /// диапазона или яркость отключена, и [`VfdError::Io`] при ошибке записи/flush.
197    pub fn set_brightness(&mut self, level: u8) -> Result<()> {
198        let cmd = self.codec.brightness(level)?;
199        self.transport.write_all(&cmd)?;
200        self.transport.flush()?;
201        let settle = self.display().brightness_settle;
202        if !settle.is_zero() {
203            std::thread::sleep(settle);
204        }
205        Ok(())
206    }
207
208    /// Завершает буферизированные записи транспорта.
209    ///
210    /// # Ошибки
211    ///
212    /// Возвращает [`VfdError::Io`], если внутренний транспорт не смог выполнить flush.
213    pub fn flush(&mut self) -> Result<()> {
214        self.transport.flush()?;
215        Ok(())
216    }
217
218    /// Возвращает внутренний транспорт.
219    ///
220    /// Метод потребляет драйвер. Это удобно в тестах, где внутренним транспортом служит
221    /// `Vec<u8>`, или при передаче ownership обратно вызывающему коду после shutdown.
222    pub fn into_inner(self) -> T {
223        self.transport
224    }
225
226    fn goto_xy(&mut self, x: u8, y: u8) -> Result<()> {
227        let cmd = self.codec.goto_xy(x, y)?;
228        self.transport.write_all(&cmd)?;
229        Ok(())
230    }
231
232    fn write_text(&mut self, s: &str) -> Result<()> {
233        let bytes = self.codec.encode_text(s);
234        self.transport.write_all(&bytes)?;
235        Ok(())
236    }
237}
238
239impl From<std::convert::Infallible> for VfdError {
240    fn from(value: std::convert::Infallible) -> Self {
241        match value {}
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::config::{DisplaySettings, Preset, TextEncoding, VfdConfig};
249
250    #[test]
251    fn mock_transport_gets_legacy_preset_bytes() {
252        let cfg = VfdConfig::preset("test", Preset::Epson20x2Cp866).unwrap();
253        let mut vfd = Vfd::from_transport(Vec::<u8>::new(), cfg.display).unwrap();
254
255        vfd.clear().unwrap();
256        vfd.print_line(1, "Привет").unwrap();
257        vfd.print_at(20, 2, "X!").unwrap();
258        vfd.set_brightness(4).unwrap();
259
260        let bytes = vfd.into_inner();
261        let mut expected = vec![0x1B, 0x40, 0x1B, 0x74, 6, 0x0C];
262        expected.extend_from_slice(&[0x1F, 0x24, 1, 1]);
263        expected.extend_from_slice(&encoding_rs::IBM866.encode("Привет              ").0);
264        expected.extend_from_slice(&[0x1F, 0x24, 20, 2, b'X']);
265        expected.extend_from_slice(&[0x1F, 0x58, 4]);
266
267        assert_eq!(bytes, expected);
268    }
269
270    #[test]
271    fn raw_write_bypasses_encoding() {
272        let display = DisplaySettings::new(20, 2, TextEncoding::Ascii);
273        let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
274
275        vfd.write_raw(&[0x1B, b'?', 0xFF]).unwrap();
276
277        assert_eq!(vfd.into_inner(), vec![0x1B, 0x40, 0x1B, b'?', 0xFF]);
278    }
279
280    #[test]
281    fn invalid_coordinates_and_brightness_are_errors() {
282        let display = DisplaySettings::new(20, 3, TextEncoding::Cp866);
283        let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
284
285        assert!(matches!(
286            vfd.print_line(4, "bad"),
287            Err(VfdError::InvalidLine { .. })
288        ));
289        assert!(matches!(
290            vfd.print_at(21, 1, "bad"),
291            Err(VfdError::InvalidCoordinate { .. })
292        ));
293        assert!(matches!(
294            vfd.set_brightness(5),
295            Err(VfdError::UnsupportedBrightness { .. })
296        ));
297    }
298
299    #[test]
300    fn alternate_encodings_are_selectable_manually() {
301        let display = DisplaySettings::new(4, 1, TextEncoding::Windows1251);
302        let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
303
304        vfd.print_line(1, "т").unwrap();
305
306        assert_eq!(
307            vfd.into_inner(),
308            vec![0x1B, 0x40, 0x1F, 0x24, 1, 1, 0xF2, b' ', b' ', b' ']
309        );
310    }
311
312    #[test]
313    fn high_level_text_neutralizes_protocol_controls() {
314        let display = DisplaySettings::new(8, 1, TextEncoding::Ascii);
315        let mut vfd = Vfd::from_transport(Vec::<u8>::new(), display).unwrap();
316
317        vfd.print_line(1, "A\u{1b}@B\u{0c}C").unwrap();
318
319        assert_eq!(&vfd.into_inner()[6..], b"A @B C  ");
320    }
321}