max7219_display/led_matrix/
display.rs

1//! LED matrix display implementation
2
3use embedded_hal::{delay::DelayNs, spi::SpiDevice};
4
5use crate::{
6    Error, MAX_DISPLAYS, Max7219, Register, Result,
7    led_matrix::{
8        buffer::MatrixBuffer,
9        fonts::{self, LedFont},
10        scroll::{ScrollConfig, ScrollingText},
11    },
12};
13
14/// Represents a single 8x8 LED matrix controlled by one MAX7219 device.
15pub type SingleMatrix<SPI> = LedMatrix<SPI, 64, 1>;
16
17/// Represents a 4-in-1 LED matrix module (total 8x32 pixels) using four chained MAX7219 devices.
18pub type Matrix4<SPI> = LedMatrix<SPI, 256, 4>;
19
20/// Represents an 8-in-1 LED matrix module (total 8x64 pixels) using eight chained MAX7219 devices.
21pub type Matrix8<SPI> = LedMatrix<SPI, 512, 8>;
22
23/// A high-level abstraction for controlling an LED matrix display using the MAX7219 driver.
24pub struct LedMatrix<SPI, const BUFFER_LENGTH: usize = 64, const DEVICE_COUNT: usize = 1> {
25    driver: Max7219<SPI>,
26    /// The framebuffer with one `u8` per pixel (0 = off, non-zero = on).
27    ///
28    /// Each 8x8 display has 64 pixels. For `N` daisy-chained devices,
29    /// the total framebuffer size is `N * 64` pixels.
30    ///
31    /// For example, with 4 devices: `4 * 64 = 256` pixels.
32    ///
33    /// This buffer is modified by `embedded-graphics` through the
34    /// [`DrawTarget`](https://docs.rs/embedded-graphics-core/latest/embedded_graphics_core/draw_target/trait.DrawTarget.html) trait.
35    framebuffer: [u8; BUFFER_LENGTH],
36}
37
38impl<SPI, const BUFFER_LENGTH: usize, const DEVICE_COUNT: usize>
39    LedMatrix<SPI, BUFFER_LENGTH, DEVICE_COUNT>
40where
41    SPI: SpiDevice,
42{
43    /// Simplifies initialization by creating a new `LedMatrix` instance
44    /// from the given SPI device and number of connected displays.
45    ///
46    /// Internally, this constructs and initializes the `Max7219` driver,
47    /// making setup easier for typical use cases.
48    ///
49    /// # Arguments
50    ///
51    /// * `spi` - The SPI device used for communication.
52    ///
53    /// # Returns
54    ///
55    /// Returns a `LedMatrix` instance on success, or an error if the display count is invalid
56    ///
57    /// # Example
58    ///
59    /// ```rust,ignore
60    /// let spi = /* your SPI device */;
61    /// let mut matrix = SingleMatrix::from_spi(spi, 4).unwrap();
62    /// ```
63    pub fn from_spi(spi: SPI) -> Result<Self> {
64        let mut driver = Max7219::new(spi).with_device_count(DEVICE_COUNT)?;
65        driver.init()?;
66        Ok(Self {
67            driver,
68            framebuffer: [0; BUFFER_LENGTH],
69        })
70    }
71
72    /// Creates a new `LedMatrix` instance from an existing `Max7219` driver.
73    ///
74    /// This method is useful if you have already created and configured a `Max7219` driver manually.
75    /// In most cases, it is recommended to use [`Self::from_spi`] instead, which creates the driver
76    /// and matrix together in one step.
77    ///
78    ///
79    ///
80    /// # Arguments
81    ///
82    /// * `driver` - An initialized `Max7219` driver instance.
83    ///
84    /// # Error
85    ///
86    /// Returns `Err(Error::InvalidDeviceCount)` if the driver's device count
87    /// does not match the generic `DEVICE_COUNT` parameter of this matrix type.
88    ///
89    /// # Warning
90    ///
91    /// This method is more error-prone than [`Self::from_spi`] because it is easy to configure a driver
92    /// with one device count (e.g., `.with_device_count(4)`) and then call `from_driver` on a `LedMatrix`
93    /// type instantiated with a different generic parameter (e.g., `LedMatrix<_, 1>`).
94    /// This mismatch will result in an error.
95    ///
96    /// # Example
97    ///
98    /// ```rust,ignore
99    /// let driver = Max7219::new(spi).with_device_count(1).expect("device count 1 should not panic");
100    /// let mut matrix = SingleMatrix::new(driver).unwrap();
101    /// ```
102    pub fn from_driver(driver: Max7219<SPI>) -> Result<Self> {
103        if driver.device_count() != DEVICE_COUNT {
104            return Err(Error::InvalidDeviceCount);
105        }
106        Ok(Self {
107            driver,
108            framebuffer: [0; BUFFER_LENGTH],
109        })
110    }
111
112    /// Provides mutable access to the underlying MAX7219 driver.
113    ///
114    /// This allows users to call low-level functions directly
115    pub fn driver(&mut self) -> &mut Max7219<SPI> {
116        &mut self.driver
117    }
118
119    /// Clear a specific device
120    pub fn clear(&mut self, device_index: usize) -> Result<()> {
121        self.driver.clear_display(device_index)
122    }
123
124    /// Clear all device
125    pub fn clear_all(&mut self) -> Result<()> {
126        self.driver.clear_all()
127    }
128
129    /// Write a complete buffer to a specific display
130    pub fn write_buffer(&mut self, device_index: usize, buffer: &MatrixBuffer) -> Result<()> {
131        for (row, &data) in buffer.data().iter().enumerate() {
132            self.driver.write_raw_digit(device_index, row as u8, data)?;
133        }
134        Ok(())
135    }
136
137    /// Draws a single 8x8 character on the specified display device.
138    ///
139    /// The character is converted into an 8-byte bitmap using a predefined font.
140    /// If the character is unsupported, it will be replaced with "?" char.
141    ///
142    /// Each byte in the bitmap corresponds to one row of the 8x8 LED matrix (from D0 to D7),
143    /// and is written to the digit registers of the specified `device_index`.
144    ///
145    /// # Arguments
146    /// * `device_index` - Index of the target MAX7219 device in the daisy chain.
147    /// * `c` - The character to display.
148    ///
149    /// # Errors
150    /// Returns an error if the digit conversion fails or if SPI communication fails.
151    pub fn draw_char(&mut self, device_index: usize, ch: char) -> Result<()> {
152        self.draw_char_with_font(device_index, ch, &fonts::STANDARD_LED_FONT)
153    }
154
155    /// Draws a single 8x8 character on the specified display device using a provided font.
156    ///
157    /// This function is similar to [`Self::draw_char`], but allows overriding the font used for rendering.
158    /// The character is mapped to an 8-byte bitmap. Each byte represents a row on the matrix, with
159    /// the most significant bit (bit 7) on the left and the least significant bit (bit 0) on the right.
160    ///
161    /// # Arguments
162    /// * `device_index` - Index of the MAX7219 device to write to.
163    /// * `ch` - The character to render on the display.
164    /// * `font` - The font to use for character lookup and rendering.
165    ///
166    pub fn draw_char_with_font(
167        &mut self,
168        device_index: usize,
169        ch: char,
170        font: &LedFont,
171    ) -> Result<()> {
172        let bitmap = font.get_char(ch);
173        // self.driver.draw_bitmap(bitmap, pos);
174        for (row, value) in bitmap.iter().enumerate() {
175            self.driver
176                .write_raw_digit(device_index, row as u8, *value)?;
177        }
178        Ok(())
179    }
180
181    /// Draw a string of text on the LED matrix using the default font.
182    /// Each character is displayed on one device in the daisy chain.
183    /// If the string is longer than the number of devices, the extra characters are ignored.
184    pub fn draw_text(&mut self, text: &str) -> Result<()> {
185        self.draw_text_with_font(text, &fonts::STANDARD_LED_FONT)
186    }
187
188    /// Draw a string of text on the LED matrix using a specified font.
189    /// Each character is displayed on one device in the daisy chain.
190    /// If the string is longer than the number of devices, the extra characters are ignored.
191    pub fn draw_text_with_font(&mut self, text: &str, font: &LedFont) -> Result<()> {
192        let device_count = self.driver.device_count();
193
194        let mut row_data = [[0u8; MAX_DISPLAYS]; 8];
195
196        for (i, ch) in text.chars().take(device_count).enumerate() {
197            let device_index = device_count - 1 - i;
198            let bitmap = font.get_char(ch);
199            for (row, &value) in bitmap.iter().enumerate() {
200                row_data[row][device_index] = value;
201            }
202        }
203
204        // Each digit_register targets the same row index (0 to 7) in every device.
205        // Example: if digit_register = Digit3 and device_count = 2,
206        // then ops will look like:
207        //     ops = [
208        //         (Digit3, row_data[3][0]), // device 1 (farthest), row 3
209        //         (Digit3, row_data[3][1]), // device 0 (nearest), row 3
210        //     ];
211        for (row_index, digit_register) in Register::digits().enumerate() {
212            let ops_row = row_data[row_index];
213            let mut ops = [(Register::NoOp, 0); MAX_DISPLAYS];
214
215            for (device_index, op) in ops.iter_mut().take(device_count).enumerate() {
216                *op = (digit_register, ops_row[device_index]);
217            }
218
219            self.driver.write_all_registers(&ops[..device_count])?;
220        }
221
222        Ok(())
223    }
224
225    /// Scroll the given text across the LED matrix.
226    ///
227    /// This will render `text` using the current font and step through
228    /// each frame at the delay specified by `config.step_delay_ns`. If
229    /// `config.loop_text` is true, the text will repeat with
230    /// `config.loop_padding` pixels of blank space between repetitions.
231    ///
232    ///
233    /// # Parameters
234    ///
235    /// - `delay`: delay provider implementing `embedded_hal::delay::DelayNs`.
236    /// - `text`: the string slice to scroll.
237    /// - `config`: scrolling configuration (speed, step size, looping).
238    ///
239    /// # Errors
240    ///
241    /// Returns a `MatrixError` if updating the display buffer fails.
242    pub fn scroll_text<D: DelayNs>(
243        &mut self,
244        delay: &mut D,
245        text: &str,
246        config: ScrollConfig,
247    ) -> Result<()> {
248        let mut scroller = ScrollingText::new(text, &fonts::STANDARD_LED_FONT, config);
249        scroller.reset();
250
251        let device_count = self.driver().device_count();
252
253        loop {
254            // Store the original offset
255            let base_offset = scroller.current_offset;
256
257            // Update each display device
258            for device_index in 0..device_count {
259                // Set offset for this specific device
260                // Each device shows 8 pixels, so device N shows pixels at offset + (N * 8)
261                scroller.current_offset = base_offset + (device_index as i32 * 8);
262
263                let frame = scroller.get_frame()?; // Each device shows 8 pixels width
264                self.write_buffer(device_index, &frame)?;
265            }
266
267            // Restore the original offset and step to next position
268            scroller.current_offset = base_offset;
269
270            if !scroller.step() {
271                break; // Stop if not looping and text has finished scrolling
272            }
273
274            delay.delay_ns(config.step_delay_ns);
275        }
276
277        Ok(())
278    }
279
280    /// Flush the internal display buffer to the actual LED matrix hardware.
281    ///
282    /// This function goes row by row (0 to 7), and for each row, it builds an array of
283    /// SPI operations (`ops`) to send to all devices in the daisy-chained display.
284    /// It packs each row of pixels into a single byte for each device, then sends the data
285    /// using the driver's `write_all_registers` method.
286    ///
287    /// ### Example logic (DEVICE_COUNT = 2, row = 0):
288    /// Assume self.framebuffer contains pixel bits for 2 devices (128 total):
289    ///
290    /// Device 0, row 0 pixels: [1, 0, 1, 0, 1, 0, 1, 0]  => 0b10101010 => 0xAA
291    /// Device 1, row 0 pixels: [1, 1, 1, 1, 0, 0, 0, 0]  => 0b11110000 => 0xF0
292    ///
293    /// Since SPI sends left to right, we must reverse the device order in the ops array:
294    ///     ops\[0\] = (Digit0, 0xF0)  // Device 1
295    ///     ops\[1\] = (Digit0, 0xAA)  // Device 0
296    ///
297    /// These are sent out in one SPI write for Digit0, and similarly repeated for Digit1 through Digit7.
298    pub fn flush(&mut self) -> Result<()> {
299        for (row, digit_register) in Register::digits().enumerate() {
300            let mut ops = [(Register::NoOp, 0); MAX_DISPLAYS];
301
302            for device_index in 0..DEVICE_COUNT {
303                let buffer_start = device_index * 64 + row * 8;
304                let mut packed_byte = 0;
305                for col in 0..8 {
306                    let pixel_index = buffer_start + col;
307                    if pixel_index < self.framebuffer.len() && self.framebuffer[pixel_index] != 0 {
308                        // bit 7 is leftmost pixel (Col 0) on the display
309                        packed_byte |= 1 << (7 - col);
310                    }
311                }
312
313                // Fill ops array in reverse order for SPI chain
314                let ops_index = DEVICE_COUNT - 1 - device_index;
315                ops[ops_index] = (digit_register, packed_byte);
316            }
317
318            self.driver.write_all_registers(&ops[..DEVICE_COUNT])?;
319        }
320        Ok(())
321    }
322
323    /// Clear the internal framebuffer (sets all pixels to 0).
324    pub fn clear_buffer(&mut self) {
325        self.framebuffer.fill(0);
326    }
327
328    /// Clear screen by resetting buffer and flushing
329    pub fn clear_screen(&mut self) -> Result<()> {
330        self.clear_buffer();
331        self.flush()
332    }
333}
334
335#[cfg(feature = "graphics")]
336mod eg_imports {
337    pub use embedded_graphics_core::Pixel;
338
339    pub use embedded_graphics_core::pixelcolor::BinaryColor;
340    pub use embedded_graphics_core::prelude::{DrawTarget, OriginDimensions, Size};
341}
342
343#[cfg(feature = "graphics")]
344use eg_imports::*;
345#[cfg(feature = "graphics")]
346use embedded_graphics_core::geometry::Dimensions;
347
348// Implementing embedded-graphics DrawTarget for LedMatrix
349#[cfg(feature = "graphics")]
350impl<SPI, const BUFFER_LENGTH: usize, const DEVICE_COUNT: usize> DrawTarget
351    for LedMatrix<SPI, BUFFER_LENGTH, DEVICE_COUNT>
352where
353    SPI: SpiDevice,
354{
355    type Color = BinaryColor;
356    type Error = core::convert::Infallible;
357
358    fn draw_iter<I>(&mut self, pixels: I) -> core::result::Result<(), Self::Error>
359    where
360        I: IntoIterator<Item = Pixel<Self::Color>>,
361    {
362        let bb = self.bounding_box();
363        for Pixel(pos, color) in pixels.into_iter() {
364            if bb.contains(pos) {
365                let device = (pos.x as usize) / 8;
366                let col = (pos.x as usize) % 8;
367                let row = pos.y as usize;
368
369                if device < DEVICE_COUNT && row < 8 && col < 8 {
370                    let index = device * 64 + row * 8 + col;
371                    if index < self.framebuffer.len() {
372                        self.framebuffer[index] = color.is_on() as u8;
373                    }
374                }
375            }
376        }
377        // Note: Does not call self.flush() automatically.
378        Ok(())
379    }
380}
381
382#[cfg(feature = "graphics")]
383impl<SPI, const BUFFER_LENGTH: usize, const DEVICE_COUNT: usize> OriginDimensions
384    for LedMatrix<SPI, BUFFER_LENGTH, DEVICE_COUNT>
385{
386    fn size(&self) -> Size {
387        Size::new(DEVICE_COUNT as u32 * 8, 8)
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use crate::Error;
394    use crate::led_matrix::display::{Matrix4, SingleMatrix};
395    use crate::led_matrix::fonts::STANDARD_LED_FONT;
396    use crate::led_matrix::{LedMatrix, buffer::MatrixBuffer, fonts::LedFont};
397    use crate::registers::Register;
398    use crate::{Max7219, NUM_DIGITS};
399    use embedded_hal_mock::eh1::{spi::Mock as SpiMock, spi::Transaction};
400
401    fn write_reg(addr: u8, value: u8) -> Vec<Transaction<u8>> {
402        vec![
403            Transaction::transaction_start(),
404            Transaction::write_vec(vec![addr, value]),
405            Transaction::transaction_end(),
406        ]
407    }
408
409    #[test]
410    fn test_new() {
411        let mut spi = SpiMock::new(&[]);
412        let driver = Max7219::new(&mut spi);
413        let matrix: LedMatrix<_, 64, 1> = LedMatrix::from_driver(driver).unwrap();
414        assert_eq!(matrix.framebuffer, [0u8; 64]);
415        spi.done();
416    }
417
418    #[test]
419    fn test_from_spi() {
420        let mut expected_transactions: Vec<Transaction<u8>> = vec![];
421
422        // power_on
423        expected_transactions.extend(write_reg(Register::Shutdown.addr(), 0x01));
424
425        // test_all(false)
426        expected_transactions.extend(write_reg(Register::DisplayTest.addr(), 0x00));
427
428        // set_scan_limit_all(NUM_DIGITS)
429        expected_transactions.extend(write_reg(Register::ScanLimit.addr(), NUM_DIGITS - 1));
430
431        // set_decode_mode_all(NoDecode)
432        expected_transactions.extend(write_reg(
433            Register::DecodeMode.addr(),
434            crate::registers::DecodeMode::NoDecode as u8,
435        ));
436
437        // clear_all() - 8 digits/rows
438        for digit in Register::digits() {
439            expected_transactions.extend(write_reg(digit.addr(), 0x00));
440        }
441
442        let mut spi = SpiMock::new(&expected_transactions);
443        let result: crate::Result<LedMatrix<_, 64, 1>> = LedMatrix::from_spi(&mut spi);
444
445        assert!(result.is_ok());
446        spi.done();
447    }
448
449    #[test]
450    fn test_from_spi_invalid_count() {
451        let mut spi = SpiMock::new(&[]);
452        let driver = Max7219::new(&mut spi);
453        // not valid count
454        let result = LedMatrix::<_, 1024, 256>::from_driver(driver);
455        assert!(matches!(result, Err(Error::InvalidDeviceCount)));
456
457        // Mismatched device count
458        let driver = Max7219::new(&mut spi);
459        let result = Matrix4::from_driver(driver);
460        assert!(matches!(result, Err(Error::InvalidDeviceCount)));
461
462        spi.done();
463    }
464
465    #[test]
466    fn test_clear() {
467        let mut expected_transactions = vec![];
468        for digit_register in Register::digits() {
469            expected_transactions.extend(write_reg(digit_register.addr(), 0x00));
470        }
471
472        let mut spi = SpiMock::new(&expected_transactions);
473        let driver = Max7219::new(&mut spi);
474        let mut matrix = SingleMatrix::from_driver(driver).expect("valid initialization");
475
476        let result = matrix.clear(0);
477        assert!(result.is_ok());
478        spi.done();
479    }
480
481    #[test]
482    fn test_clear_all() {
483        // Test clearing all devices (but checking with 1 device for SingleMatrix)
484        let mut expected_transactions = vec![];
485        let device_count = 4;
486
487        for digit_register in Register::digits() {
488            expected_transactions.push(Transaction::transaction_start());
489            let batched_data = (0..device_count)
490                .flat_map(|_| vec![digit_register.addr(), 0x00])
491                .collect();
492            expected_transactions.push(Transaction::write_vec(batched_data));
493            expected_transactions.push(Transaction::transaction_end());
494        }
495
496        let mut spi = SpiMock::new(&expected_transactions);
497        let driver = Max7219::new(&mut spi).with_device_count(4).unwrap();
498        let mut matrix = Matrix4::from_driver(driver).expect("valid initialization");
499
500        let result = matrix.clear_all();
501        assert!(result.is_ok());
502        spi.done();
503    }
504
505    #[test]
506    fn test_write_buffer() {
507        let device_index = 0;
508        let mut buffer = MatrixBuffer::new();
509        // Fill buffer with some test data
510        for i in 0..8 {
511            buffer.set_row(i, 0b10101010 << (i % 2)).unwrap();
512        }
513        let expected_data = buffer.data();
514
515        let mut expected_transactions = Vec::new();
516        for (row, &data) in expected_data.iter().enumerate() {
517            expected_transactions.push(Transaction::transaction_start());
518            expected_transactions.push(Transaction::write_vec(vec![
519                Register::try_digit(row as u8).unwrap().addr(),
520                data,
521            ]));
522            expected_transactions.push(Transaction::transaction_end());
523        }
524
525        let mut spi = SpiMock::new(&expected_transactions);
526        let driver = Max7219::new(&mut spi);
527        let mut matrix = SingleMatrix::from_driver(driver).expect("valid initialization");
528
529        let result = matrix.write_buffer(device_index, &buffer);
530        assert!(result.is_ok());
531        spi.done();
532    }
533
534    #[test]
535    fn test_write_buffer_invalid_index() {
536        let mut spi = SpiMock::new(&[]); // No SPI calls expected
537        let driver = Max7219::new(&mut spi).with_device_count(1).unwrap();
538        let mut matrix = SingleMatrix::from_driver(driver).expect("valid initialization");
539        let buffer = MatrixBuffer::new();
540
541        let result = matrix.write_buffer(1, &buffer); // Index 1 is invalid for device_count=1
542        // This error comes from Max7219::write_raw_digit via write_device_register
543        assert_eq!(result, Err(Error::InvalidDeviceIndex));
544        spi.done();
545    }
546
547    #[test]
548    fn test_draw_char() {
549        let device_index = 0;
550        let ch = 'A';
551        let expected_bitmap = STANDARD_LED_FONT.get_char(ch);
552
553        let mut expected_transactions = Vec::new();
554        for (row, &data) in expected_bitmap.iter().enumerate() {
555            expected_transactions.push(Transaction::transaction_start());
556            expected_transactions.push(Transaction::write_vec(vec![
557                Register::try_digit(row as u8).unwrap().addr(),
558                data,
559            ]));
560            expected_transactions.push(Transaction::transaction_end());
561        }
562
563        let mut spi = SpiMock::new(&expected_transactions);
564        let driver = Max7219::new(&mut spi);
565        let mut matrix = SingleMatrix::from_driver(driver).expect("valid initialization");
566
567        let result = matrix.draw_char(device_index, ch);
568        assert!(result.is_ok());
569        spi.done();
570    }
571
572    #[test]
573    fn test_draw_char_with_font() {
574        let device_index = 0;
575        let ch = 'Z';
576        let test_font = STANDARD_LED_FONT;
577        let expected_bitmap = test_font.get_char(ch);
578
579        let mut expected_transactions = Vec::new();
580        for (row, &data) in expected_bitmap.iter().enumerate() {
581            expected_transactions.push(Transaction::transaction_start());
582            expected_transactions.push(Transaction::write_vec(vec![
583                Register::try_digit(row as u8).unwrap().addr(),
584                data,
585            ]));
586            expected_transactions.push(Transaction::transaction_end());
587        }
588
589        let mut spi = SpiMock::new(&expected_transactions);
590        let driver = Max7219::new(&mut spi);
591        let mut matrix = SingleMatrix::from_driver(driver).expect("valid initialization");
592
593        matrix
594            .draw_char_with_font(device_index, ch, &test_font)
595            .expect("valid character");
596        spi.done();
597    }
598
599    #[test]
600    fn test_draw_char_invalid_index() {
601        let mut spi = SpiMock::new(&[]); // No SPI calls expected
602        let driver = Max7219::new(&mut spi).with_device_count(1).unwrap();
603        let mut matrix = SingleMatrix::from_driver(driver).expect("valid initialization");
604
605        let result = matrix.draw_char(1, 'A'); // Index 1 is invalid for device_count=1
606        // This error comes from Max7219::write_raw_digit via write_device_register
607        assert_eq!(result, Err(Error::InvalidDeviceIndex));
608        spi.done();
609    }
610
611    #[test]
612    fn test_draw_text_single_device() {
613        let text = "H";
614        let expected_ch = 'H';
615        let expected_bitmap = STANDARD_LED_FONT.get_char(expected_ch);
616        // For 1 device, text is written to device 0 (device_count - 1 - i = 0)
617        // Data is sent row by row using write_all_registers
618
619        let mut expected_transactions = Vec::new();
620        for (row_index, &data) in expected_bitmap.iter().enumerate() {
621            let digit_register = Register::try_digit(row_index as u8).unwrap();
622            expected_transactions.push(Transaction::transaction_start());
623            // For 1 device, ops[0] = (digit_register, data)
624            expected_transactions.push(Transaction::write_vec(vec![digit_register.addr(), data]));
625            expected_transactions.push(Transaction::transaction_end());
626        }
627
628        let mut spi = SpiMock::new(&expected_transactions);
629        let driver = Max7219::new(&mut spi);
630        let mut matrix = SingleMatrix::from_driver(driver).expect("valid initialization");
631
632        let result = matrix.draw_text(text);
633        assert!(result.is_ok());
634        spi.done();
635    }
636
637    #[test]
638    fn test_draw_text_multi_device() {
639        let device_count = 4;
640
641        let text = "Hi";
642        let expected_bitmap_h = STANDARD_LED_FONT.get_char('H');
643        let expected_bitmap_i = STANDARD_LED_FONT.get_char('i');
644        // Text chars are taken in order: 'H', 'i'
645        // They are written to devices in reverse order: 'H' -> device 1, 'i' -> device 0
646        // (because device_index = device_count - 1 - i)
647
648        let mut expected_transactions = Vec::new();
649        // Row by row transmission
650        for row_index in 0..8 {
651            let digit_register = Register::try_digit(row_index).unwrap();
652            expected_transactions.push(Transaction::transaction_start());
653            // ops[0] = (digit_register, data_for_device_1) // Sent first on SPI
654            // ops[1] = (digit_register, data_for_device_0) // Sent second, ends up in device 0
655            // Remaining devices gets filled with 0
656            let data_for_device_1 = expected_bitmap_h[row_index as usize];
657            let data_for_device_0 = expected_bitmap_i[row_index as usize];
658            expected_transactions.push(Transaction::write_vec(vec![
659                digit_register.addr(),
660                data_for_device_1, // Data for device 1
661                digit_register.addr(),
662                data_for_device_0, // Data for device 0
663                digit_register.addr(),
664                0x00,
665                digit_register.addr(),
666                0x00,
667            ]));
668            expected_transactions.push(Transaction::transaction_end());
669        }
670
671        let mut spi = SpiMock::new(&expected_transactions);
672        let driver = Max7219::new(&mut spi)
673            .with_device_count(device_count)
674            .unwrap();
675        let mut matrix = Matrix4::from_driver(driver).unwrap();
676
677        let result = matrix.draw_text(text);
678        assert!(result.is_ok());
679        spi.done();
680    }
681
682    #[test]
683    fn test_draw_text_with_font() {
684        let text = "!";
685        // Create a simple test font
686        pub const TEST_FONT: &[([u8; 8], char)] = &[([0b10101010; 8], '!')];
687        let test_font = LedFont::new(TEST_FONT);
688        let expected_bitmap = [0b10101010u8; 8];
689        let device_count = 2;
690
691        let mut expected_transactions = Vec::new();
692
693        // For each row (Digit0 to Digit7)
694        for (row_index, &data) in expected_bitmap.iter().enumerate() {
695            let digit_register = Register::try_digit(row_index as u8).unwrap();
696            expected_transactions.push(Transaction::transaction_start());
697
698            // First device gets the font data, second device gets zero
699            expected_transactions.push(Transaction::write_vec(vec![
700                digit_register.addr(),
701                data, // Device 0
702                digit_register.addr(),
703                0x00, // Device 1 (no text)
704            ]));
705
706            expected_transactions.push(Transaction::transaction_end());
707        }
708
709        let mut spi = SpiMock::new(&expected_transactions);
710        let driver = Max7219::new(&mut spi)
711            .with_device_count(device_count)
712            .unwrap();
713        let mut matrix: LedMatrix<_, 128, 2> = LedMatrix::from_driver(driver).unwrap();
714
715        let result = matrix.draw_text_with_font(text, &test_font);
716        assert!(result.is_ok());
717
718        spi.done();
719    }
720
721    #[test]
722    fn test_clear_buffer() {
723        let mut spi = SpiMock::new(&[]); // No SPI interaction
724        let driver = Max7219::new(&mut spi);
725        let mut matrix = SingleMatrix::from_driver(driver).unwrap();
726
727        // Modify the buffer
728        matrix.framebuffer[0] = 1;
729        matrix.framebuffer[10] = 1;
730        matrix.framebuffer[63] = 1;
731        assert_ne!(matrix.framebuffer, [0u8; 64]);
732
733        matrix.clear_buffer();
734
735        assert_eq!(matrix.framebuffer, [0u8; 64]);
736        spi.done();
737    }
738
739    #[test]
740    fn test_clear_screen() {
741        // All digits 0..7 will be written with 0x00 for a single device
742        let mut expected_transactions = Vec::new();
743        for row in 0..8 {
744            let digit_register = Register::try_digit(row).unwrap();
745            expected_transactions.push(Transaction::transaction_start());
746            expected_transactions.push(Transaction::write_vec(vec![digit_register.addr(), 0x00]));
747            expected_transactions.push(Transaction::transaction_end());
748        }
749
750        let mut spi = SpiMock::new(&expected_transactions);
751        let driver = Max7219::new(&mut spi);
752        let mut matrix = SingleMatrix::from_driver(driver).unwrap();
753
754        // Modify the buffer
755        matrix.framebuffer[5] = 1;
756        matrix.framebuffer[15] = 1;
757
758        assert_ne!(matrix.framebuffer, [0u8; 64]);
759
760        let result = matrix.clear_screen();
761        assert!(result.is_ok());
762        assert_eq!(matrix.framebuffer, [0u8; 64]);
763        spi.done();
764    }
765
766    #[test]
767    fn test_flush_single_device() {
768        // We expect the flush to send 8 SPI transactions, one for each row (DIGIT0 to DIGIT7)
769        // Only rows 0 and 7 have pixel data: 0b10101010 (columns 0,2,4,6 lit)
770        // All other rows should be cleared (0b00000000)
771
772        let mut expected_transactions = Vec::new();
773        for (row, digit_register) in Register::digits().enumerate() {
774            // For rows 0 and 7, the framebuffer will result in this pattern:
775            // Columns 0, 2, 4, 6 are ON => bits 7, 5, 3, 1 set => 0b10101010
776            let expected_byte = if row == 0 || row == 7 {
777                0b10101010
778            } else {
779                0b00000000
780            };
781
782            // Each transaction sends [register, data] for that row
783            expected_transactions.push(Transaction::transaction_start());
784            expected_transactions.push(Transaction::write_vec(vec![
785                digit_register.addr(),
786                expected_byte,
787            ]));
788            expected_transactions.push(Transaction::transaction_end());
789        }
790
791        // Create the SPI mock with the expected sequence of writes
792        let mut spi = SpiMock::new(&expected_transactions);
793        let driver = Max7219::new(&mut spi);
794        let mut matrix = SingleMatrix::from_driver(driver).unwrap();
795
796        // Set framebuffer values to light up alternating columns in row 0 and row 7
797        // Row 0 corresponds to framebuffer indices 0 to 7
798        matrix.framebuffer[0] = 1; // Column 0
799        matrix.framebuffer[2] = 1; // Column 2
800        matrix.framebuffer[4] = 1; // Column 4
801        matrix.framebuffer[6] = 1; // Column 6
802
803        // Each device's framebuffer is a flat array of 64 bytes: 8 rows * 8 columns
804        // The layout is row-major: [row0[0..7], row1[0..7], ..., row7[0..7]]
805        //
806        // For a single device:
807        //   framebuffer[ 0.. 7] => row 0
808        //   framebuffer[ 8..15] => row 1
809        //   framebuffer[16..23] => row 2
810        //   framebuffer[24..31] => row 3
811        //   framebuffer[32..39] => row 4
812        //   framebuffer[40..47] => row 5
813        //   framebuffer[48..55] => row 6
814        //   framebuffer[56..63] => row 7 (last row)
815        //
816        // So to update row 7, we write to indices 56 to 63.
817        matrix.framebuffer[56] = 1; // Column 0
818        matrix.framebuffer[58] = 1; // Column 2
819        matrix.framebuffer[60] = 1; // Column 4
820        matrix.framebuffer[62] = 1; // Column 6
821
822        // Call flush, which will convert framebuffer rows into bytes and send via SPI
823        let result = matrix.flush();
824        assert!(result.is_ok());
825
826        spi.done();
827    }
828
829    #[test]
830    fn test_driver_mut_access() {
831        let expected_transactions = [
832            Transaction::transaction_start(),
833            Transaction::write_vec(vec![Register::Shutdown.addr(), 0x01]),
834            Transaction::transaction_end(),
835        ];
836        let mut spi = SpiMock::new(&expected_transactions);
837        let original_driver = Max7219::new(&mut spi);
838        let mut matrix = SingleMatrix::from_driver(original_driver).unwrap();
839
840        let driver = matrix.driver();
841
842        driver.power_on().expect("Power on should succeed");
843        spi.done();
844    }
845}
846
847#[cfg(all(test, feature = "graphics"))]
848mod graphis_tests {
849    use super::*;
850    use embedded_graphics_core::geometry::Point;
851
852    use embedded_hal_mock::eh1::spi::Mock as SpiMock;
853
854    #[test]
855    fn test_draw_target_draw_iter() {
856        let mut spi = SpiMock::new(&[]);
857        let driver = Max7219::new(&mut spi);
858        let mut matrix = SingleMatrix::from_driver(driver).unwrap(); // 1 device, 64 pixels
859
860        // Define some pixels to draw
861        let pixels = [
862            Pixel(Point::new(0, 0), BinaryColor::On), // Device 0, Row 0, Col 0
863            Pixel(Point::new(1, 0), BinaryColor::Off), // Device 0, Row 0, Col 1
864            Pixel(Point::new(7, 7), BinaryColor::On), // Device 0, Row 7, Col 7
865            // out of bounds
866            Pixel(Point::new(8, 0), BinaryColor::On),
867            Pixel(Point::new(0, 8), BinaryColor::On),
868            Pixel(Point::new(20, 20), BinaryColor::On),
869        ];
870
871        // Draw the pixels
872        matrix.draw_iter(pixels.iter().cloned()).unwrap();
873
874        let mut expected = [0u8; 64];
875        expected[0] = 1; // (0, 0) ON
876        expected[1] = 0; // (1, 0) OFF
877        expected[63] = 1; // (7, 7) ON
878
879        assert_eq!(&matrix.framebuffer, &expected);
880
881        spi.done();
882    }
883
884    #[test]
885    fn test_draw_target_draw_iter_multi_device() {
886        let mut spi = SpiMock::new(&[]);
887        let driver = Max7219::new(&mut spi).with_device_count(2).unwrap(); // 2 devices
888        let mut matrix: LedMatrix<_, 128, 2> = LedMatrix::from_driver(driver).unwrap(); // 2 devices, 128 pixels
889
890        // Define some pixels to draw across devices
891        let pixels = [
892            // x=0 → device = 0/8 = 0
893            // col = 0%8 = 0
894            // row = 0
895            // index = device*64 + row*8 + col = 0*64 + 0*8 + 0 = 0
896            Pixel(Point::new(0, 0), BinaryColor::On),
897            // x=7 → device = 7/8 = 0
898            // col = 7%8 = 7
899            // row = 0
900            // index = 0*64 + 0*8 + 7 = 7
901            Pixel(Point::new(7, 0), BinaryColor::On),
902            // x=8 → device = 8/8 = 1
903            // col = 8%8 = 0
904            // row = 1
905            // index = device*64 + row*8 + col = 1*64 + 1*8 + 0 = 64 + 8 + 0 = 72
906            Pixel(Point::new(8, 1), BinaryColor::On),
907            // x=15 → device = 15/8 = 1
908            // col = 15%8 = 7
909            // row = 7
910            // index = 1*64 + 7*8 + 7 = 64 + 56 + 7 = 127
911            Pixel(Point::new(15, 7), BinaryColor::On),
912        ];
913
914        // Draw the pixels
915        matrix.draw_iter(pixels.iter().cloned()).unwrap();
916
917        // Check framebuffer state
918        let mut expected = [0u8; 128];
919        expected[0] = 1; // Device 0, Col 0, Row 0
920        expected[7] = 1; // Device 0, Col 7, Row 0
921        expected[72] = 1; // Device 1, Col 0, Row 1
922        expected[127] = 1; // Device 1, Col 7, Row 7
923
924        assert_eq!(&matrix.framebuffer, &expected);
925
926        spi.done();
927    }
928}