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}