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}