rpi_led_panel/
rgb_matrix.rs

1use std::{
2    error::Error,
3    fmt::{Display, Formatter},
4    fs::{write, OpenOptions},
5    mem::replace,
6    sync::mpsc::{
7        channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TryRecvError,
8    },
9    thread::{spawn, JoinHandle},
10    time::Duration,
11};
12
13use thread_priority::{set_current_thread_priority, ThreadPriority};
14
15use crate::{
16    canvas::{Canvas, PixelDesignator, PixelDesignatorMap},
17    chip::PiChip,
18    gpio::{Gpio, GpioInitializationError},
19    pixel_mapper::PixelMapper,
20    utils::{linux_has_isol_cpu, set_thread_affinity, FrameRateMonitor},
21    RGBMatrixConfig,
22};
23
24fn initialize_update_thread(chip: PiChip) {
25    // Pin the thread to the last core to avoid the flicker resulting from context switching.
26    let last_core_id = chip.num_cores() - 1;
27    set_thread_affinity(last_core_id);
28
29    // If the user has not setup isolcpus, let them know about the performance improvement.
30    if chip.num_cores() > 1 && !linux_has_isol_cpu(last_core_id) {
31        eprintln!(
32            "Suggestion: to slightly improve display update, add\n\tisolcpus={last_core_id}\nat \
33            the end of /boot/cmdline.txt and reboot"
34        );
35    }
36
37    // Disable realtime throttling.
38    if chip.num_cores() > 1 && write("/proc/sys/kernel/sched_rt_runtime_us", "999000").is_err() {
39        eprintln!("Could not disable realtime throttling");
40    }
41
42    // Set the last core to performance mode.
43    if chip.num_cores() > 1
44        && write(
45            format!("/sys/devices/system/cpu/cpu{last_core_id}/cpufreq/scaling_governor"),
46            "performance",
47        )
48        .is_err()
49    {
50        eprintln!(
51            "Could not set core {} to performance mode.",
52            last_core_id + 1
53        );
54    }
55
56    // Set the highest thread priority.
57    if set_current_thread_priority(ThreadPriority::Max).is_err() {
58        eprintln!("Could not set thread priority. This might lead to reduced performance.",);
59    }
60}
61
62#[derive(Debug)]
63pub enum MatrixCreationError {
64    ChipDeterminationError,
65    TooManyParallelChains(usize),
66    InvalidDitherBits(usize),
67    ThreadTimedOut,
68    GpioError(GpioInitializationError),
69    MemoryAccessError,
70    PixelMapperError(String),
71}
72
73impl Error for MatrixCreationError {}
74
75impl Display for MatrixCreationError {
76    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
77        match self {
78            MatrixCreationError::ChipDeterminationError => {
79                f.write_str("Failed to automatically determine Raspberry Pi model.")
80            }
81            MatrixCreationError::TooManyParallelChains(max) => {
82                write!(f, "GPIO mapping only supports up to {max} parallel panels.")
83            }
84            MatrixCreationError::InvalidDitherBits(value) => {
85                write!(f, "Unsupported dither bits '{value}'.")
86            }
87            MatrixCreationError::ThreadTimedOut => {
88                f.write_str("The update thread did not return in time.")
89            }
90            MatrixCreationError::GpioError(error) => {
91                write!(f, "GPIO initialization error: {error}")
92            }
93            MatrixCreationError::MemoryAccessError => f.write_str(
94                "Failed to access the physical memory. Not running with root privileges?",
95            ),
96            MatrixCreationError::PixelMapperError(message) => {
97                write!(f, "Error in pixel mapper: {message}")
98            }
99        }
100    }
101}
102
103pub struct RGBMatrix {
104    /// The join handle of the update thread.
105    thread_handle: Option<JoinHandle<()>>,
106    /// Sender for the shutdown signal.
107    shutdown_sender: Sender<()>,
108    /// Receiver for GPIO inputs.
109    input_receiver: Receiver<u32>,
110    /// Channel to send canvas to update thread.
111    canvas_to_thread_sender: SyncSender<Box<Canvas>>,
112    /// Channel to receive canvas from update thread.
113    canvas_from_thread_receiver: Receiver<Box<Canvas>>,
114    /// Additional requested inputs that can be received.
115    enabled_input_bits: u32,
116    /// Frame rate measurement.
117    frame_rate_monitor: FrameRateMonitor,
118}
119
120impl RGBMatrix {
121    /// Create a new RGB matrix controller. This starts a new thread to update the matrix. Returns the
122    /// controller and a canvas for drawing.
123    ///
124    /// You can additionally request user readable GPIO bits which can later be received with
125    /// [`RGBMatrix::receive_new_inputs`]. Only bits that are not already in use for reading or writing by the
126    /// matrix are allowed. Use [`RGBMatrix::enabled_input_bits`] after calling this function to check which
127    /// bits were actually available.
128    pub fn new(
129        mut config: RGBMatrixConfig,
130        requested_inputs: u32,
131    ) -> Result<(Self, Box<Canvas>), MatrixCreationError> {
132        // Check if we can access the memory before doing anything else.
133        OpenOptions::new()
134            .read(true)
135            .write(true)
136            .open("/dev/mem")
137            .map_err(|_| MatrixCreationError::MemoryAccessError)?;
138
139        let chip = if let Some(chip) = config.pi_chip {
140            chip
141        } else {
142            PiChip::determine().ok_or(MatrixCreationError::ChipDeterminationError)?
143        };
144
145        let max_parallel = config.hardware_mapping.max_parallel_chains();
146        if config.parallel > max_parallel {
147            return Err(MatrixCreationError::TooManyParallelChains(max_parallel));
148        }
149
150        let multiplex_mapper = config.multiplexing.as_ref().map(|mapper_type| {
151            // The multiplexers might choose to have a different physical layout.
152            // We need to configure that first before setting up the hardware.
153            let mut mapper = mapper_type.create();
154            mapper.edit_rows_cols(&mut config.rows, &mut config.cols);
155            mapper
156        });
157
158        let pixel_designator = PixelDesignator::new(&config.hardware_mapping, config.led_sequence);
159        let width = config.cols * config.chain_length;
160        let height = config.rows * config.parallel;
161        let mut shared_mapper = PixelDesignatorMap::new(pixel_designator, width, height, &config);
162
163        // Apply the mapping for the panels first.
164        if let Some(mapper) = multiplex_mapper {
165            let mapper = PixelMapper::Multiplex(mapper);
166            shared_mapper =
167                Self::apply_pixel_mapper(&shared_mapper, &mapper, &config, pixel_designator)?;
168        }
169
170        // Apply higher level mappers that might arrange panels.
171        for mapper_type in config.pixelmapper.iter() {
172            let mapper = mapper_type.create(config.chain_length, config.parallel)?;
173            let mapper = PixelMapper::Named(mapper);
174            shared_mapper =
175                Self::apply_pixel_mapper(&shared_mapper, &mapper, &config, pixel_designator)?;
176        }
177
178        let dither_start_bits = match config.dither_bits {
179            0 => [0, 0, 0, 0],
180            1 => [0, 1, 0, 1],
181            2 => [0, 1, 2, 2],
182            _ => return Err(MatrixCreationError::InvalidDitherBits(config.dither_bits)),
183        };
184
185        // Create two canvases, one for the display update thread and one for the user to modify. They will be
186        // swapped out after each frame.
187        let canvas = Box::new(Canvas::new(&config, shared_mapper));
188        let mut thread_canvas = canvas.clone();
189
190        let (canvas_to_thread_sender, canvas_to_thread_receiver) = sync_channel::<Box<Canvas>>(0);
191        let (canvas_from_thread_sender, canvas_from_thread_receiver) =
192            sync_channel::<Box<Canvas>>(1);
193        let (shutdown_sender, shutdown_receiver) = channel::<()>();
194        let (input_sender, input_receiver) = channel::<u32>();
195        let (thread_start_result_sender, thread_start_result_receiver) =
196            channel::<Result<u32, MatrixCreationError>>();
197
198        let thread_handle = spawn(move || {
199            initialize_update_thread(chip);
200
201            let mut address_setter = config.row_setter.create(&config);
202
203            let mut gpio = match Gpio::new(chip, &config, address_setter.as_ref()) {
204                Ok(gpio) => gpio,
205                Err(error) => {
206                    thread_start_result_sender
207                        .send(Err(MatrixCreationError::GpioError(error)))
208                        .expect("Could not send to main thread.");
209                    return;
210                }
211            };
212
213            // Run the initialization sequence if necessary.
214            if let Some(panel_type) = config.panel_type {
215                panel_type.run_init_sequence(&mut gpio, &config);
216            }
217
218            let mut last_gpio_inputs: u32 = 0;
219
220            // Dither sequence
221            let mut dither_low_bit_sequence = 0;
222
223            let frame_time_target_us = (1_000_000.0 / config.refresh_rate as f64) as u64;
224
225            let color_clk_mask = config
226                .hardware_mapping
227                .get_color_clock_mask(config.parallel);
228
229            let enabled_input_bits = gpio.request_enabled_inputs(requested_inputs);
230            thread_start_result_sender
231                .send(Ok(enabled_input_bits))
232                .expect("Could not send to main thread.");
233
234            'thread: loop {
235                let start_time = gpio.get_time();
236                loop {
237                    // Try to receive a shutdown request.
238                    if shutdown_receiver.try_recv() != Err(TryRecvError::Empty) {
239                        break 'thread;
240                    }
241                    // Read input bits and send them if they have changed.
242                    let new_inputs = gpio.read();
243                    if new_inputs != last_gpio_inputs {
244                        match input_sender.send(new_inputs) {
245                            Ok(()) => {}
246                            Err(_) => {
247                                break 'thread;
248                            }
249                        }
250                        last_gpio_inputs = new_inputs;
251                    }
252                    // Wait for a swap canvas.
253                    match canvas_to_thread_receiver.recv_timeout(Duration::from_millis(1)) {
254                        Ok(new_canvas) => {
255                            let old_canvas = replace(&mut thread_canvas, new_canvas);
256                            match canvas_from_thread_sender.send(old_canvas) {
257                                Ok(()) => break,
258                                Err(_) => {
259                                    break 'thread;
260                                }
261                            };
262                        }
263                        Err(RecvTimeoutError::Disconnected) => {
264                            break 'thread;
265                        }
266                        Err(RecvTimeoutError::Timeout) => {}
267                    }
268                }
269
270                thread_canvas.dump_to_matrix(
271                    &mut gpio,
272                    &config.hardware_mapping,
273                    address_setter.as_mut(),
274                    dither_start_bits[dither_low_bit_sequence % dither_start_bits.len()],
275                    color_clk_mask,
276                );
277                dither_low_bit_sequence += 1;
278
279                // Sleep for the rest of the frame.
280                let now_time = gpio.get_time();
281                let end_time = start_time + frame_time_target_us;
282                if let Some(remaining_time) = end_time.checked_sub(now_time) {
283                    gpio.sleep(remaining_time);
284                }
285            }
286
287            // Turn it off.
288            thread_canvas.fill(0, 0, 0);
289            thread_canvas.dump_to_matrix(
290                &mut gpio,
291                &config.hardware_mapping,
292                address_setter.as_mut(),
293                0,
294                color_clk_mask,
295            );
296        });
297
298        let enabled_input_bits = thread_start_result_receiver
299            .recv_timeout(Duration::from_secs(10))
300            .map_err(|_| MatrixCreationError::ThreadTimedOut)??;
301
302        let rgbmatrix = Self {
303            thread_handle: Some(thread_handle),
304            input_receiver,
305            shutdown_sender,
306            canvas_to_thread_sender,
307            canvas_from_thread_receiver,
308            enabled_input_bits,
309            frame_rate_monitor: FrameRateMonitor::new(),
310        };
311
312        Ok((rgbmatrix, canvas))
313    }
314
315    fn apply_pixel_mapper(
316        shared_mapper: &PixelDesignatorMap,
317        mapper: &PixelMapper,
318        config: &RGBMatrixConfig,
319        pixel_designator: PixelDesignator,
320    ) -> Result<PixelDesignatorMap, MatrixCreationError> {
321        let old_width = shared_mapper.width();
322        let old_height = shared_mapper.height();
323        let [new_width, new_height] = mapper.get_size_mapping(old_width, old_height)?;
324        let mut new_mapper =
325            PixelDesignatorMap::new(pixel_designator, new_width, new_height, config);
326        for y in 0..new_height {
327            for x in 0..new_width {
328                let [orig_x, orig_y] = mapper.map_visible_to_matrix(old_width, old_height, x, y);
329                if orig_x >= old_width || orig_y >= old_height {
330                    return Err(MatrixCreationError::PixelMapperError(
331                        "Invalid dimensions detected. This is likely a bug.".to_string(),
332                    ));
333                }
334                let orig_designator = shared_mapper.get(orig_x, orig_y).unwrap();
335                *new_mapper.get_mut(x, y).unwrap() = *orig_designator;
336            }
337        }
338        Ok(new_mapper)
339    }
340
341    /// Updates the matrix with the new canvas. Blocks until the end of the current frame.
342    pub fn update_on_vsync(&mut self, canvas: Box<Canvas>) -> Box<Canvas> {
343        let Self {
344            canvas_to_thread_sender,
345            canvas_from_thread_receiver,
346            frame_rate_monitor,
347            ..
348        } = self;
349
350        canvas_to_thread_sender
351            .send(canvas)
352            .expect("Display update thread shut down unexpectedly.");
353
354        frame_rate_monitor.update();
355
356        canvas_from_thread_receiver
357            .recv()
358            .expect("Display update thread shut down unexpectedly.")
359    }
360
361    /// Get the bits that were available for input.
362    #[must_use]
363    pub fn enabled_input_bits(&self) -> u32 {
364        self.enabled_input_bits
365    }
366
367    /// Tries to receive a new GPIO input as specified with [`RGBMatrix::request_enabled_inputs`].
368    pub fn receive_new_inputs(&mut self, timeout: Duration) -> Option<u32> {
369        self.input_receiver.recv_timeout(timeout).ok()
370    }
371
372    /// Get the average frame rate over the last 60 frames.
373    #[must_use]
374    pub fn get_framerate(&self) -> usize {
375        self.frame_rate_monitor.get_fps().round() as usize
376    }
377}
378
379impl Drop for RGBMatrix {
380    fn drop(&mut self) {
381        let Self {
382            thread_handle,
383            shutdown_sender,
384            ..
385        } = self;
386        if let Some(handle) = thread_handle.take() {
387            shutdown_sender.send(()).ok();
388            let _result = handle.join();
389        }
390    }
391}