flipdot_core/
page.rs

1use std::borrow::Cow;
2use std::fmt::{self, Display, Formatter};
3
4use derive_more::{Display, LowerHex, UpperHex};
5use thiserror::Error;
6
7/// Errors relating to [`Page`]s.
8#[derive(Copy, Clone, Debug, Error)]
9#[non_exhaustive]
10pub enum PageError {
11    /// Data length didn't match the width/height of the [`Page`].
12    #[error(
13        "Wrong number of data bytes for a {}x{} page: Expected {}, got {}",
14        width,
15        height,
16        expected,
17        actual
18    )]
19    WrongPageLength {
20        /// The page width.
21        width: u32,
22
23        /// The page height.
24        height: u32,
25
26        /// The expected length of the page data.
27        expected: usize,
28
29        /// The actual length of the page data that was provided.
30        actual: usize,
31    },
32}
33
34const HEADER_LEN: usize = 4;
35
36/// A page of a message for display on a sign.
37///
38/// # Examples
39///
40/// ```
41/// use flipdot_core::{Page, PageId};
42///
43/// let mut page = Page::new(PageId(1), 30, 10); // Create 30x10 page with ID 1
44/// page.set_pixel(3, 5, true); // Turn on pixel at column 3 and row 5
45/// ```
46///
47/// # Format Details
48///
49/// Data is stored in the native format, which consists of a 4-byte header and the data itself,
50/// padded to a a multiple of 16 bytes. The pixel data is column-major, with one or more bytes per
51/// column and one bit per pixel. The least significant bit is oriented toward the top of the display.
52/// The `ID` field is a "page number" used to identify individual pages in multi-page messages.
53/// The other bytes in the header are unknown, but from inspection of real ODKs seem to be most
54/// commonly `0x10 0x00 0x00`, which is what [`Page::new`] currently uses.
55///
56/// ```text
57///                   ┌─┬ ┄ ┬─┐
58///              Bits │7│...│0│
59///                   └─┴ ┄ ┴─┘
60///                    \     /
61/// ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ ┄ ┬────┬ ┄ ┬────┐
62/// │ ID │ ?? │ ?? │ ?? │  0 │  1 │  2 │  3 │  4 │  5 │...│0xFF│...│0xFF│
63/// └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴ ┄ ┴────┴ ┄ ┴────┘
64/// ┆   4-byte header   ┆            Data bytes           ┆   Padding   ┆
65/// ```
66///
67/// Depending on the intended dimensions of the sign, the same data will be interpreted differently:
68///
69/// ```text
70///                 7 height                         16 height
71///
72///                                            Bytes   0   2   4  ...
73///  Bytes   0   1   2   3   4   5  ...                1   3   5  ...
74///        ┌───┬───┬───┬───┬───┬───┬ ┄               ┌───┬───┬───┬ ┄
75///    0   │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │             0   │ 0 │ 0 │ 0 │
76///    |   ├───┼───┼───┼───┼───┼───┼ ┄           |   ├───┼───┼───┼ ┄
77///   Row  │...│...│...│...│...│...│             |   │...│...│...│
78///    |   ├───┼───┼───┼───┼───┼───┼ ┄           |   ├───┼───┼───┼ ┄
79///    7   │ 6 │ 6 │ 6 │ 6 │ 6 │ 6 │             |   │ 7 │ 7 │ 7 │
80///        └───┴───┴───┴───┴───┴───┴ ┄          Row  ╞═══╪═══╪═══╪ ┄
81///          0 - - - Column- - - 5               |   │ 0 │ 0 │ 0 │
82///                                              |   ├───┼───┼───┼ ┄
83///              (bit 7 unused)                  |   │...│...│...│
84///                                              |   ├───┼───┼───┼ ┄
85///                                             15   │ 7 │ 7 │ 7 │
86///                                                  └───┴───┴───┴ ┄
87///                                                   0 - Col - 2
88/// ```
89#[derive(Debug, Clone, PartialEq, Eq, Hash)]
90pub struct Page<'a> {
91    width: u32,
92    height: u32,
93    bytes: Cow<'a, [u8]>,
94}
95
96/// The page number of a [`Page`].
97///
98/// Used to identify a particular page in a multi-page message.
99///
100/// # Examples
101///
102/// ```
103/// use flipdot_core::{Page, PageId};
104///
105/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
106/// #
107/// let page = Page::new(PageId(1), 10, 10);
108/// assert_eq!(PageId(1), page.id());
109/// #
110/// # Ok(()) }
111/// ```
112#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Display, LowerHex, UpperHex)]
113pub struct PageId(pub u8);
114
115/// Whether the sign or controller (ODK) is in charge of flipping pages.
116#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
117pub enum PageFlipStyle {
118    /// The sign will flip pages itself.
119    Automatic,
120
121    /// The controller will notify the sign when to load/show pages.
122    Manual,
123}
124
125impl<'a> Page<'a> {
126    /// Creates a new `Page` with given ID and dimensions.
127    ///
128    /// All pixels are initially set to off. The data is owned by this `Page`.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// # use flipdot_core::{Page, PageId};
134    /// let page = Page::new(PageId(1), 90, 7); // Create 90x7 page with ID 1
135    /// assert_eq!(false, page.get_pixel(75, 3)); // All pixels initially off
136    /// ```
137    pub fn new(id: PageId, width: u32, height: u32) -> Self {
138        let mut bytes = Vec::<u8>::with_capacity(Self::total_bytes(width, height));
139
140        // 4-byte header
141        bytes.extend_from_slice(&[id.0, 0x10, 0x00, 0x00]);
142
143        // Fill remaining data bytes with 0 for a blank initial image
144        bytes.resize(Self::data_bytes(width, height), 0x00);
145
146        // Pad to multiple of 16 with 0xFF bytes
147        bytes.resize(Self::total_bytes(width, height), 0xFF);
148
149        Page {
150            width,
151            height,
152            bytes: bytes.into(),
153        }
154    }
155
156    /// Creates a new `Page` with given dimensions from the underlying byte representation.
157    ///
158    /// The data must be convertible to [`Cow`], which allows us to create efficient views of
159    /// `Page`s over existing data without making copies.
160    ///
161    /// It is the caller's responsibility to ensure that the header and padding bytes are
162    /// set appropriately as they are not validated.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`PageError::WrongPageLength`] if the data length does not match
167    /// the specified dimensions.
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// # use flipdot_core::{Page, PageId};
173    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
174    /// #
175    /// let data: Vec<u8> = vec![1, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255];
176    /// let page = Page::from_bytes(8, 8, data)?;
177    /// assert_eq!(PageId(1), page.id());
178    /// assert_eq!(true, page.get_pixel(0, 0));
179    /// assert_eq!(false, page.get_pixel(1, 0));
180    ///
181    /// let bad_data: Vec<u8> = vec![1, 0, 0, 0, 1];
182    /// let bad_page = Page::from_bytes(1, 8, bad_data);
183    /// assert!(bad_page.is_err());
184    /// #
185    /// # Ok(()) }
186    /// ```
187    pub fn from_bytes<T: Into<Cow<'a, [u8]>>>(width: u32, height: u32, bytes: T) -> Result<Self, PageError> {
188        let page = Page {
189            width,
190            height,
191            bytes: bytes.into(),
192        };
193
194        let expected_bytes = Self::total_bytes(width, height);
195        if page.bytes.len() != expected_bytes {
196            return Err(PageError::WrongPageLength {
197                width,
198                height,
199                expected: expected_bytes,
200                actual: page.bytes.len(),
201            });
202        }
203
204        Ok(page)
205    }
206
207    /// Returns the ID (page number) of this page.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// # use flipdot_core::{Page, PageId};
213    /// let page = Page::new(PageId(1), 90, 7);
214    /// println!("This is page {}", page.id().0);
215    /// ```
216    pub fn id(&self) -> PageId {
217        PageId(self.bytes[0])
218    }
219
220    /// Returns the width of this page.
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// # use flipdot_core::{Page, PageId};
226    /// let page = Page::new(PageId(1), 90, 7);
227    /// println!("Page is {} pixels wide", page.width());
228    /// ```
229    pub fn width(&self) -> u32 {
230        self.width
231    }
232
233    /// Returns the height of this page.
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// # use flipdot_core::{Page, PageId};
239    /// let page = Page::new(PageId(1), 90, 7);
240    /// println!("Page is {} pixels tall", page.height());
241    /// ```
242    pub fn height(&self) -> u32 {
243        self.height
244    }
245
246    /// Returns whether or not the pixel at the given `(x, y)` coordinate is on.
247    ///
248    /// # Panics
249    ///
250    /// Panics if `x` or `y` is out of bounds.
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// # use flipdot_core::{Page, PageId};
256    /// let page = Page::new(PageId(1), 90, 7);
257    /// let (x, y) = (45, 2);
258    /// println!("Pixel at {}, {} on? {}", x, y, page.get_pixel(x, y));
259    /// ```
260    pub fn get_pixel(&self, x: u32, y: u32) -> bool {
261        let (byte_index, bit_index) = self.byte_bit_indices(x, y);
262        let mask = 1 << bit_index;
263        let byte = &self.bytes[byte_index];
264        *byte & mask == mask
265    }
266
267    /// Turns the pixel at the given `(x, y)` coordinate on or off.
268    ///
269    /// # Panics
270    ///
271    /// Panics if `x` or `y` is out of bounds.
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// # use flipdot_core::{Page, PageId};
277    /// let mut page = Page::new(PageId(1), 90, 7);
278    /// page.set_pixel(5, 5, true); // Turn on pixel...
279    /// page.set_pixel(5, 5, false); // And turn it back off.
280    /// ```
281    pub fn set_pixel(&mut self, x: u32, y: u32, value: bool) {
282        let (byte_index, bit_index) = self.byte_bit_indices(x, y);
283        let mask = 1 << bit_index;
284        let byte = &mut self.bytes.to_mut()[byte_index];
285        if value {
286            *byte |= mask;
287        } else {
288            *byte &= !mask;
289        }
290    }
291
292    /// Turns all the pixels on the page on or off.
293    ///
294    /// # Examples
295    ///
296    /// ```
297    /// # use flipdot_core::{Page, PageId};
298    /// let mut page = Page::new(PageId(1), 90, 7);
299    /// // Turn on a couple pixels
300    /// page.set_pixel(5, 5, true);
301    /// page.set_pixel(6, 6, true);
302    ///
303    /// // And clear the page again
304    /// page.set_all_pixels(false);
305    /// ```
306    pub fn set_all_pixels(&mut self, value: bool) {
307        let byte = if value { 0xFF } else { 0x00 };
308        self.bytes.to_mut()[HEADER_LEN..Self::data_bytes(self.width, self.height)].fill(byte);
309    }
310
311    /// Returns the raw byte representation of this page.
312    ///
313    /// This is generally called on your behalf when sending a page to a sign.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// # use flipdot_core::{Page, PageId};
319    /// let mut page = Page::new(PageId(1), 8, 8);
320    /// page.set_pixel(0, 0, true);
321    /// let bytes = page.as_bytes();
322    /// assert_eq!(vec![1, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255], bytes);
323    /// ```
324    pub fn as_bytes(&self) -> &[u8] {
325        &self.bytes
326    }
327
328    /// Returns the number of bytes used to store each column.
329    fn bytes_per_column(height: u32) -> usize {
330        (height as usize + 7) / 8 // Divide by 8 rounding up
331    }
332
333    /// Returns the number of actual meaningful bytes (including header but not padding).
334    fn data_bytes(width: u32, height: u32) -> usize {
335        HEADER_LEN + width as usize * Self::bytes_per_column(height)
336    }
337
338    /// Returns the total number of bytes, including the padding.
339    fn total_bytes(width: u32, height: u32) -> usize {
340        (Self::data_bytes(width, height) + 15) / 16 * 16 // Round to multiple of 16
341    }
342
343    /// Given an x-y coordinate, returns the byte and bit at which it is stored.
344    fn byte_bit_indices(&self, x: u32, y: u32) -> (usize, u8) {
345        if x >= self.width || y >= self.height {
346            panic!(
347                "Coordinate ({}, {}) out of bounds for page of size {} x {}",
348                x, y, self.width, self.height
349            );
350        }
351
352        let byte_index = 4 + x as usize * Self::bytes_per_column(self.height) + y as usize / 8;
353        let bit_index = y % 8;
354        (byte_index, bit_index as u8)
355    }
356}
357
358impl Display for Page<'_> {
359    /// Formats the page for display using ASCII art.
360    ///
361    /// Produces a multiline string with one character per pixel and a border.
362    /// Should be displayed in a fixed-width font.
363    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
364        let border = str::repeat("-", self.width as usize);
365        writeln!(f, "+{}+", border)?;
366        for y in 0..self.height {
367            write!(f, "|")?;
368            for x in 0..self.width {
369                let dot = if self.get_pixel(x, y) { '@' } else { ' ' };
370                write!(f, "{}", dot)?;
371            }
372            writeln!(f, "|")?;
373        }
374        write!(f, "+{}+", border)?;
375        Ok(())
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use std::error::Error;
383    use test_case::test_case;
384
385    #[test]
386    fn one_byte_per_column_empty() -> Result<(), Box<dyn Error>> {
387        let page = Page::new(PageId(3), 90, 7);
388        let bytes = page.as_bytes();
389        #[rustfmt::skip]
390        const EXPECTED: &[u8] = &[
391            0x03, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
392            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
393            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
394            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
395            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
396            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
397        ];
398        assert_eq!(bytes, EXPECTED);
399
400        let page2 = Page::from_bytes(90, 7, bytes)?;
401        assert_eq!(page, page2);
402
403        Ok(())
404    }
405
406    #[test]
407    fn two_bytes_per_column_empty() -> Result<(), Box<dyn Error>> {
408        let page = Page::new(PageId(1), 40, 12);
409        let bytes = page.as_bytes();
410        #[rustfmt::skip]
411        const EXPECTED: &[u8] = &[
412            0x01, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
413            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
414            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
415            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
416            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
417            0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
418        ];
419        assert_eq!(bytes, EXPECTED);
420
421        let page2 = Page::from_bytes(40, 12, bytes)?;
422        assert_eq!(page, page2);
423
424        Ok(())
425    }
426
427    #[test]
428    fn one_byte_per_column_set_bits() -> Result<(), Box<dyn Error>> {
429        let mut page = Page::new(PageId(3), 90, 7);
430        page.set_pixel(0, 0, true);
431        page.set_pixel(89, 5, true);
432        page.set_pixel(89, 6, true);
433        page.set_pixel(4, 4, true);
434        page.set_pixel(4, 4, false);
435        let bytes = page.as_bytes();
436        #[rustfmt::skip]
437        const EXPECTED: &[u8] = &[
438            0x03, 0x10, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
439            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
440            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
441            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
442            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
443            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xFF, 0xFF,
444        ];
445        assert_eq!(bytes, EXPECTED);
446
447        let page2 = Page::from_bytes(90, 7, bytes)?;
448        assert_eq!(page, page2);
449
450        Ok(())
451    }
452
453    #[test]
454    fn two_bytes_per_column_set_bits() -> Result<(), Box<dyn Error>> {
455        let mut page = Page::new(PageId(1), 40, 12);
456        page.set_pixel(0, 0, true);
457        page.set_pixel(0, 11, true);
458        page.set_pixel(39, 5, true);
459        page.set_pixel(39, 6, true);
460        page.set_pixel(39, 8, true);
461        page.set_pixel(4, 4, true);
462        page.set_pixel(4, 4, false);
463        let bytes = page.as_bytes();
464        #[rustfmt::skip]
465        const EXPECTED: &[u8] = &[
466            0x01, 0x10, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
467            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
468            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
469            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
470            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
471            0x00, 0x00, 0x60, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
472        ];
473        assert_eq!(bytes, EXPECTED);
474
475        let page2 = Page::from_bytes(40, 12, bytes)?;
476        assert_eq!(page, page2);
477
478        Ok(())
479    }
480
481    #[test]
482    fn wrong_size_rejected() {
483        let error = Page::from_bytes(90, 7, vec![0x01, 0x01, 0x03]).unwrap_err();
484        assert!(matches!(
485            error,
486            PageError::WrongPageLength {
487                expected: 96,
488                actual: 3,
489                ..
490            }
491        ));
492    }
493
494    #[test]
495    fn set_get_pixels() {
496        let mut page = Page::new(PageId(1), 16, 16);
497
498        page.set_pixel(0, 0, true);
499        assert_eq!(true, page.get_pixel(0, 0));
500        page.set_pixel(0, 0, false);
501        assert_eq!(false, page.get_pixel(0, 0));
502
503        page.set_pixel(13, 10, true);
504        assert_eq!(true, page.get_pixel(13, 10));
505        page.set_pixel(13, 10, false);
506        assert_eq!(false, page.get_pixel(13, 10));
507    }
508
509    #[test]
510    #[should_panic]
511    fn out_of_bounds_x() {
512        let mut page = Page::new(PageId(1), 8, 8);
513        page.set_pixel(9, 0, true);
514    }
515
516    #[test]
517    #[should_panic]
518    fn out_of_bounds_y() {
519        let mut page = Page::new(PageId(1), 8, 8);
520        page.set_pixel(0, 9, true);
521    }
522
523    #[test]
524    fn display() {
525        let mut page = Page::new(PageId(1), 2, 2);
526        page.set_pixel(0, 0, true);
527        page.set_pixel(1, 1, true);
528        let display = format!("{}", page);
529        let expected = "\
530                        +--+\n\
531                        |@ |\n\
532                        | @|\n\
533                        +--+";
534        assert_eq!(expected, display);
535    }
536
537    fn verify_all_pixels(page: &Page, value: bool) {
538        for x in 0..page.width() {
539            for y in 0..page.height() {
540                assert_eq!(value, page.get_pixel(x, y));
541            }
542        }
543    }
544
545    #[test_case(Page::new(PageId(3), 90, 7) ; "one byte per column")]
546    #[test_case(Page::new(PageId(1), 40, 12) ; "two bytes per column")]
547    fn set_all_pixels(mut page: Page) {
548        let bytes_before = page.as_bytes().to_vec();
549
550        verify_all_pixels(&page, false);
551
552        page.set_all_pixels(true);
553        verify_all_pixels(&page, true);
554
555        page.set_all_pixels(false);
556        verify_all_pixels(&page, false);
557
558        assert_eq!(bytes_before, page.as_bytes());
559    }
560}