flipdot_graphics/flipdot_display.rs
1use std::{cell::RefCell, iter, rc::Rc};
2
3use embedded_graphics_core::{
4 Pixel,
5 draw_target::DrawTarget,
6 geometry::{OriginDimensions, Size},
7 pixelcolor::BinaryColor,
8};
9use flipdot::{Address, Page, PageFlipStyle, PageId, SerialSignBus, Sign, SignBus, SignError, SignType};
10use flipdot_testing::{VirtualSign, VirtualSignBus};
11
12/// A [`DrawTarget`] implementation to easily draw graphics to a Luminator sign.
13///
14/// Drawing results are buffered and only sent to the sign when [`flush`](Self::flush) is called.
15///
16/// # Examples
17///
18/// ```no_run
19/// use embedded_graphics::{
20/// mono_font::{MonoTextStyle, ascii::FONT_5X7},
21/// pixelcolor::BinaryColor,
22/// prelude::*,
23/// primitives::{Circle, PrimitiveStyle, Triangle},
24/// text::{Baseline, Text},
25/// };
26/// use flipdot_graphics::{Address, FlipdotDisplay, SignBusType, SignType};
27///
28/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
29/// #
30/// // Create a display for a sign connected over serial.
31/// let mut display = FlipdotDisplay::try_new(
32/// SignBusType::Serial("/dev/ttyUSB0"),
33/// Address(3),
34/// SignType::Max3000Side90x7
35/// )?;
36///
37/// // Draw some shapes and text to the page.
38/// Circle::new(Point::new(2, 0), 6)
39/// .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
40/// .draw(&mut display)?;
41///
42/// Triangle::new(Point::new(11, 1), Point::new(15, 5), Point::new(19, 1))
43/// .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
44/// .draw(&mut display)?;
45///
46/// let style = MonoTextStyle::new(&FONT_5X7, BinaryColor::On);
47/// Text::with_baseline("Hello, world!", Point::new(24, 0), style, Baseline::Top)
48/// .draw(&mut display)?;
49///
50/// // Send the page to the sign to be displayed.
51/// display.flush()?;
52/// #
53/// # Ok(()) }
54/// ```
55#[derive(Debug)]
56pub struct FlipdotDisplay {
57 page: Page<'static>,
58 sign: Sign,
59}
60
61/// The type of sign bus to create.
62#[derive(Debug)]
63pub enum SignBusType<'a> {
64 /// Create a [`SerialSignBus`] for communicating with a real sign over the specified serial port.
65 Serial(&'a str),
66
67 /// Create a [`VirtualSignBus`] for testing.
68 Virtual,
69}
70
71impl<'a, T: AsRef<str>> From<&'a T> for SignBusType<'a> {
72 /// Pass "virtual" to use a virtual sign bus for testing, otherwise `value` will be interpreted as a serial port.
73 fn from(value: &'a T) -> Self {
74 let port = value.as_ref();
75 if port.eq_ignore_ascii_case("virtual") {
76 Self::Virtual
77 } else {
78 Self::Serial(port)
79 }
80 }
81}
82
83impl FlipdotDisplay {
84 /// The easiest way to get started drawing to a sign in a standalone fashion.
85 ///
86 /// Creates a [`SignBus`] internally based on `bus_type` to simplify the common case.
87 /// If you do need more control, you can provide your own bus using [`new_with_bus`](Self::new_with_bus).
88 ///
89 /// # Errors
90 ///
91 /// Returns the underlying [`serial::Error`] if the serial port cannot be configured.
92 /// Virtual sign bus creation can never fail.
93 ///
94 /// # Examples
95 ///
96 /// ```no_run
97 /// use flipdot_graphics::{Address, FlipdotDisplay, SignBusType, SignType};
98 ///
99 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
100 /// #
101 /// // Create a display for a sign connected over serial.
102 /// let mut display = FlipdotDisplay::try_new(
103 /// SignBusType::Serial("COM3"),
104 /// Address(6),
105 /// SignType::Max3000Front98x16
106 /// )?;
107 /// #
108 /// # Ok(()) }
109 /// ```
110 ///
111 /// ```
112 /// use flipdot_graphics::{Address, FlipdotDisplay, SignBusType, SignType};
113 ///
114 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
115 /// #
116 /// // Create a display for a virtual sign for testing
117 /// // (set RUST_LOG=flipdot=info environment variable to see the results).
118 /// let mut display = FlipdotDisplay::try_new(
119 /// SignBusType::Virtual,
120 /// Address(4),
121 /// SignType::HorizonDash40x12
122 /// )?;
123 /// #
124 /// # Ok(()) }
125 /// ```
126 pub fn try_new(bus_type: SignBusType<'_>, address: Address, sign_type: SignType) -> Result<Self, serial::Error> {
127 let bus: Rc<RefCell<dyn SignBus>> = match bus_type {
128 SignBusType::Virtual => {
129 let bus = VirtualSignBus::new(iter::once(VirtualSign::new(address, PageFlipStyle::Manual)));
130 Rc::new(RefCell::new(bus))
131 }
132 SignBusType::Serial(port) => {
133 let port = serial::open(port)?;
134 let bus = SerialSignBus::try_new(port)?;
135 Rc::new(RefCell::new(bus))
136 }
137 };
138
139 Ok(Self::new_with_bus(bus, address, sign_type))
140 }
141
142 /// Alternative constructor if you need access to the underlying [`SignBus`], perhaps because you want to draw to
143 /// multiple signs on the same bus, want to inspect a [`VirtualSignBus`] for tests, etc.
144 ///
145 /// For the common case where you only want to draw to a single sign, [`try_new`](Self::try_new) is simpler.
146 ///
147 /// # Examples
148 ///
149 /// ```
150 /// use std::{cell::RefCell, iter, rc::Rc};
151 ///
152 /// use embedded_graphics::{
153 /// pixelcolor::BinaryColor,
154 /// prelude::*,
155 /// };
156 /// use flipdot::PageFlipStyle;
157 /// use flipdot_graphics::{Address, FlipdotDisplay, SignBusType, SignType};
158 /// use flipdot_testing::{VirtualSign, VirtualSignBus};
159 ///
160 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
161 /// #
162 /// // Set up bus
163 /// let bus = VirtualSignBus::new(iter::once(VirtualSign::new(Address(3), PageFlipStyle::Manual)));
164 /// let bus = Rc::new(RefCell::new(bus));
165 ///
166 /// let mut display = FlipdotDisplay::new_with_bus(
167 /// bus.clone(),
168 /// Address(3),
169 /// SignType::Max3000Side90x7
170 /// );
171 ///
172 /// // Draw to the display
173 /// display.draw_iter([Pixel(Point::new(0, 0), BinaryColor::On)])?;
174 /// display.flush()?;
175 ///
176 /// // Show the page sent to the sign
177 /// println!("Got page:\n{}", bus.borrow().sign(0).pages()[0]);
178 /// #
179 /// # Ok(()) }
180 /// ```
181 pub fn new_with_bus(bus: Rc<RefCell<dyn SignBus>>, address: Address, sign_type: SignType) -> Self {
182 Sign::new(bus, address, sign_type).into()
183 }
184
185 /// Sends all pending changes since the last flush to the sign.
186 pub fn flush(&self) -> Result<(), SignError> {
187 self.sign.configure_if_needed()?;
188
189 if self.sign.send_pages(iter::once(&self.page))? == PageFlipStyle::Manual {
190 self.sign.show_loaded_page()?;
191 }
192
193 Ok(())
194 }
195}
196
197impl From<Sign> for FlipdotDisplay {
198 fn from(sign: Sign) -> Self {
199 Self {
200 page: sign.create_page(PageId(0)),
201 sign,
202 }
203 }
204}
205
206impl DrawTarget for FlipdotDisplay {
207 type Color = BinaryColor;
208 type Error = core::convert::Infallible; // Drawing itself can never fail since we just write to the Page.
209
210 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
211 where
212 I: IntoIterator<Item = Pixel<Self::Color>>,
213 {
214 for Pixel(coord, color) in pixels.into_iter() {
215 // `DrawTarget` contract requires ignoring out of bounds coordinates.
216 if let Ok((x, y)) = coord.try_into() {
217 let size = self.size();
218 if x < size.width && y < size.height {
219 self.page.set_pixel(x, y, color.is_on());
220 }
221 }
222 }
223
224 Ok(())
225 }
226
227 fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
228 self.page.set_all_pixels(color.is_on());
229 Ok(())
230 }
231}
232
233impl OriginDimensions for FlipdotDisplay {
234 fn size(&self) -> Size {
235 Size::new(self.sign.width(), self.sign.height())
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use embedded_graphics::{
243 prelude::*,
244 primitives::{PrimitiveStyle, Triangle},
245 };
246 use std::error::Error;
247
248 #[test]
249 fn out_of_bounds_pixels() -> Result<(), Box<dyn Error>> {
250 let bus = VirtualSignBus::new(iter::once(VirtualSign::new(Address(3), PageFlipStyle::Manual)));
251 let bus = Rc::new(RefCell::new(bus));
252 let mut display = FlipdotDisplay::new_with_bus(bus.clone(), Address(3), SignType::Max3000Side90x7);
253
254 // Writing out of bounds shouldn't fail or panic
255 display.draw_iter([
256 Pixel(Point::new(-1, 0), BinaryColor::On),
257 Pixel(Point::new(0, -1), BinaryColor::On),
258 Pixel(Point::new(90, 0), BinaryColor::On),
259 Pixel(Point::new(0, 7), BinaryColor::On),
260 ])?;
261 display.flush()?;
262
263 // And should result in an empty page
264 let bus = bus.borrow();
265 let page = &bus.sign(0).pages()[0];
266 assert_eq!(*page, Page::new(page.id(), page.width(), page.height()));
267
268 Ok(())
269 }
270
271 #[test]
272 fn draw_and_flush() -> Result<(), Box<dyn Error>> {
273 let bus = VirtualSignBus::new(iter::once(VirtualSign::new(Address(3), PageFlipStyle::Manual)));
274 let bus = Rc::new(RefCell::new(bus));
275 let mut display = FlipdotDisplay::new_with_bus(bus.clone(), Address(3), SignType::Max3000Side90x7);
276
277 Triangle::new(Point::new(0, 0), Point::new(45, 6), Point::new(89, 0))
278 .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
279 .draw(&mut display)?;
280
281 // Ensure nothing has been sent to the sign yet.
282 assert!(bus.borrow().sign(0).pages().is_empty());
283
284 display.flush()?;
285
286 // Now verify that we have a triangle.
287 assert!(!bus.borrow().sign(0).pages().is_empty());
288 let actual = format!("{}", bus.borrow().sign(0).pages()[0]);
289 let expected = "\
290 +------------------------------------------------------------------------------------------+\n\
291 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|\n\
292 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |\n\
293 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |\n\
294 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |\n\
295 | @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |\n\
296 | @@@@@@@@@@@@@@@@@@@@@@ |\n\
297 | @@@@@@@ |\n\
298 +------------------------------------------------------------------------------------------+";
299
300 assert_eq!(actual, expected);
301
302 Ok(())
303 }
304}