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}