capsules_extra/screen/
screen_on_led.rs

1// Licensed under the Apache License, Version 2.0 or the MIT License.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// Copyright Tock Contributors 2025.
4
5//! Display virtual LEDs on a screen.
6//!
7//! This creates virtual LEDs by drawing boxes on the screen. The output looks
8//! roughly like this:
9//!
10//! ```text
11//!     ┌─┐┌─┐┌─┐┌─┐
12//! LED └─┘└─┘└─┘└─┘
13//! ```
14//! The boxes get filled in if the LED is on.
15//!
16//! ## Caveat
17//!
18//! The LED API is based on the GPIO API which is synchronous. Writing to a
19//! screen is not. This means LED events can easily get lost if the screen
20//! operation hasn't finished yet.
21
22use core::cell::Cell;
23use kernel::hil;
24use kernel::utilities::cells::MapCell;
25use kernel::utilities::leasable_buffer::SubSliceMut;
26use kernel::ErrorCode;
27
28/// How many pixels of padding on the left and right side of the graphic.
29const LEFT_RIGTH_PADDING: usize = 2;
30/// How many pixels of padding above and below the graphic.
31const TOP_BOTTOM_PADDING: usize = 2;
32/// How many pixels of padding between the text "LED" and the squares.
33const TEXT_LEDS_PADDING: usize = 2;
34/// How many pixels of padding between each letter "L", "E", "D".
35const TEXT_SPACING: usize = 2;
36/// How many pixels of padding above and below the "LED" text.
37const TEXT_TOP_BOTTOM_PADDING: usize = 4;
38
39/// Mirror of the [`kernel::hil::led::Led`] trait but that supports LED indices.
40///
41/// This is implemented by [`ScreenOnLed`]. The trait allows us to avoid having
42/// to replicate the `const` types on [`ScreenOnLed`].
43pub trait LedIndexed {
44    fn init(&self, index: usize);
45    fn on(&self, index: usize);
46    fn off(&self, index: usize);
47    fn toggle(&self, index: usize);
48    fn read(&self, index: usize) -> bool;
49}
50
51/// A simple wrapper type to contain the index of the LED.
52///
53/// Since we don't have a single GPIO pin to use, we instead store the index
54/// and call into the shared [`ScreenOnLed`].
55pub struct ScreenOnLedSingle<'a, L: LedIndexed> {
56    /// Essentially only [`ScreenOnLed`].
57    led_controller: &'a L,
58    /// Which LED this is.
59    index: usize,
60}
61
62impl<'a, L: LedIndexed> ScreenOnLedSingle<'a, L> {
63    pub fn new(led_controller: &'a L, index: usize) -> Self {
64        Self {
65            led_controller,
66            index,
67        }
68    }
69}
70
71impl<L: LedIndexed> hil::led::Led for ScreenOnLedSingle<'_, L> {
72    fn init(&self) {
73        self.led_controller.init(self.index);
74    }
75
76    fn on(&self) {
77        self.led_controller.on(self.index);
78    }
79
80    fn off(&self) {
81        self.led_controller.off(self.index);
82    }
83
84    fn toggle(&self) {
85        self.led_controller.toggle(self.index);
86    }
87
88    fn read(&self) -> bool {
89        self.led_controller.read(self.index)
90    }
91}
92
93pub struct ScreenOnLed<
94    'a,
95    S: hil::screen::Screen<'a>,
96    const NUM_LEDS: usize,
97    const SCREEN_WIDTH: usize,
98    const SCREEN_HEIGHT: usize,
99> {
100    /// Underlying screen driver to use.
101    screen: &'a S,
102
103    /// Array of the state of each LED. Needed for toggle() and read().
104    leds: Cell<[bool; NUM_LEDS]>,
105
106    /// Buffer to render the LED graphics into.
107    buffer: MapCell<&'static mut [u8]>,
108
109    /// Whether or not the buffer is initialized with the LED graphics.
110    initialized: Cell<bool>,
111
112    /// Whether LEDs were changed while a screen write was outstanding. When the
113    /// write finishes, write again with the updated LED state.
114    dirty: Cell<bool>,
115}
116
117impl<
118        'a,
119        S: hil::screen::Screen<'a>,
120        const NUM_LEDS: usize,
121        const SCREEN_WIDTH: usize,
122        const SCREEN_HEIGHT: usize,
123    > ScreenOnLed<'a, S, NUM_LEDS, SCREEN_WIDTH, SCREEN_HEIGHT>
124{
125    pub const fn new(screen: &'a S, buffer: &'static mut [u8]) -> Self {
126        Self {
127            screen,
128            leds: Cell::new([false; NUM_LEDS]),
129            buffer: MapCell::new(buffer),
130            initialized: Cell::new(false),
131            dirty: Cell::new(false),
132        }
133    }
134
135    /// Draw the main LED graphic (e.g. the text and LED boxes).
136    fn initialize_leds(&self) {
137        self.buffer.take().map(|buffer| {
138            self.render(buffer);
139            let data = SubSliceMut::new(buffer);
140            let _ = self.screen.write(data, false);
141        });
142    }
143
144    /// Draw all LEDs.
145    ///
146    /// This is a hack to help correctly show LEDs even though the screen is
147    /// async and LEDs are sync. We can get LEDs changing when the buffer is
148    /// being used by the screen, so we try to hide that by updating the status
149    /// of all LEDs each time.
150    fn show_leds(&self) {
151        if !self.initialized.get() {
152            return;
153        }
154
155        self.buffer.take().map_or_else(
156            || {
157                // We can't update the LEDs because we don't have the screen
158                // buffer. This means a screen write is in progress. We mark
159                // this and re-write when the current screen write finishes.
160                self.dirty.set(true);
161            },
162            |buffer| {
163                let leds = self.leds.get();
164                for (i, led_state) in leds.iter().enumerate() {
165                    self.render_led_state(buffer, i, *led_state);
166                }
167                let data = SubSliceMut::new(buffer);
168                let _ = self.screen.write(data, false);
169            },
170        );
171    }
172
173    fn get_led_offset(&self, led_index: usize) -> usize {
174        let led_dimension = self.get_size().1;
175
176        LEFT_RIGTH_PADDING
177            + self.get_led_width(led_dimension)
178            + TEXT_LEDS_PADDING
179            + ((led_dimension + 1) * led_index)
180    }
181
182    fn render(&self, buffer: &mut [u8]) {
183        self.render_led_text(buffer, LEFT_RIGTH_PADDING);
184        for i in 0..NUM_LEDS {
185            self.render_led(buffer, i);
186        }
187    }
188
189    fn render_led_text(&self, buffer: &mut [u8], x_offset: usize) {
190        let y_offset = self.get_size().2;
191
192        let y_top = TOP_BOTTOM_PADDING + TEXT_TOP_BOTTOM_PADDING + y_offset;
193        let y_bottom = SCREEN_HEIGHT - TOP_BOTTOM_PADDING - TEXT_TOP_BOTTOM_PADDING - y_offset;
194        let height = y_bottom - y_top;
195
196        // L
197        let l_offset = x_offset;
198        let l_width = self.get_char_width(height, 'l');
199        self.write_vertical_line(buffer, l_offset, y_top, height, 1);
200        self.write_horizontal_line(buffer, l_offset, y_bottom, l_width, 1);
201
202        // E
203        let e_offset = x_offset + l_width + TEXT_SPACING;
204        let e_width = self.get_char_width(height, 'e');
205        self.write_vertical_line(buffer, e_offset, y_top, height, 1);
206        self.write_horizontal_line(buffer, e_offset, y_top, e_width, 1);
207        self.write_horizontal_line(buffer, e_offset, y_bottom, e_width, 1);
208        self.write_horizontal_line(
209            buffer,
210            e_offset,
211            usize::midpoint(y_top, y_bottom),
212            e_width / 2,
213            1,
214        );
215
216        // D
217        let d_offset = e_offset + e_width + TEXT_SPACING;
218        let d_width = self.get_char_width(height, 'd');
219        self.write_vertical_line(buffer, d_offset, y_top, height, 1);
220        self.write_vertical_line(buffer, d_offset + d_width, y_top, height, 1);
221        self.write_horizontal_line(buffer, d_offset, y_top, d_width, 1);
222        self.write_horizontal_line(buffer, d_offset, y_bottom, d_width, 1);
223    }
224
225    fn render_led(&self, buffer: &mut [u8], led_index: usize) {
226        // Draw two squares, one on, then one inside that is off.
227
228        let (_width, led_dimension, y_offset) = self.get_size();
229        let x_offset: usize = self.get_led_offset(led_index);
230
231        // Write the outside box fully on.
232        self.write_square(
233            buffer.as_mut(),
234            x_offset,
235            TOP_BOTTOM_PADDING + y_offset,
236            led_dimension,
237            1,
238        );
239        // Clear the inside to make just the border.
240        self.write_square(
241            buffer.as_mut(),
242            x_offset + 1,
243            TOP_BOTTOM_PADDING + y_offset + 1,
244            led_dimension - 2,
245            0,
246        );
247    }
248
249    fn render_led_state(&self, buffer: &mut [u8], led_index: usize, on: bool) {
250        let (_width, led_dimension, y_offset) = self.get_size();
251        let x_offset: usize = self.get_led_offset(led_index);
252
253        // Clear the inside to make just the border.
254        self.write_square(
255            buffer.as_mut(),
256            x_offset + 1,
257            TOP_BOTTOM_PADDING + y_offset + 1,
258            led_dimension - 2,
259            0,
260        );
261
262        if on {
263            // Draw the LED as on.
264            self.write_square(
265                buffer.as_mut(),
266                x_offset + 2,
267                TOP_BOTTOM_PADDING + y_offset + 2,
268                led_dimension - 4,
269                1,
270            );
271        }
272    }
273
274    fn write_square(&self, buffer: &mut [u8], x: usize, y: usize, dimension: usize, val: usize) {
275        for i in 0..dimension {
276            for j in 0..dimension {
277                let pixel_x = i + x;
278                let pixel_y = j + y;
279                let byte = ((pixel_y / 8) * SCREEN_WIDTH) + pixel_x;
280                let bit = pixel_y % 8;
281                if val & 0x1 == 0x1 {
282                    buffer[byte] |= 1 << bit;
283                } else {
284                    buffer[byte] &= !(1 << bit);
285                }
286            }
287        }
288    }
289
290    fn write_horizontal_line(
291        &self,
292        buffer: &mut [u8],
293        x: usize,
294        y: usize,
295        length: usize,
296        val: usize,
297    ) {
298        for i in 0..length {
299            let pixel_x = i + x;
300            let byte = ((y / 8) * SCREEN_WIDTH) + pixel_x;
301            let bit = y % 8;
302            if val & 0x1 == 0x1 {
303                buffer[byte] |= 1 << bit;
304            } else {
305                buffer[byte] &= !(1 << bit);
306            }
307        }
308    }
309
310    fn write_vertical_line(
311        &self,
312        buffer: &mut [u8],
313        x: usize,
314        y: usize,
315        length: usize,
316        val: usize,
317    ) {
318        for i in 0..length {
319            let pixel_y = i + y;
320            let byte = ((pixel_y / 8) * SCREEN_WIDTH) + x;
321            let bit = pixel_y % 8;
322            if val & 0x1 == 0x1 {
323                buffer[byte] |= 1 << bit;
324            } else {
325                buffer[byte] &= !(1 << bit);
326            }
327        }
328    }
329
330    /// Find a size of the graphic that works with the screen by shrinking the
331    /// LEDs until everything fits.
332    pub const fn get_size(&self) -> (usize, usize, usize) {
333        let mut width = SCREEN_WIDTH + 1;
334        let mut led_dimension = SCREEN_HEIGHT - (TOP_BOTTOM_PADDING * 2);
335
336        while width > SCREEN_WIDTH {
337            // Shrink LEDs by 1 pixel.
338            led_dimension -= 1;
339
340            let leds_width: usize = (led_dimension * NUM_LEDS) + (NUM_LEDS - 1);
341            width = LEFT_RIGTH_PADDING
342                + self.get_led_width(led_dimension)
343                + TEXT_LEDS_PADDING
344                + leds_width
345                + LEFT_RIGTH_PADDING;
346        }
347
348        let y_offset = (SCREEN_HEIGHT - (TOP_BOTTOM_PADDING * 2) - led_dimension) / 2;
349
350        (width, led_dimension, y_offset)
351    }
352
353    pub const fn get_char_width(&self, height: usize, c: char) -> usize {
354        match c {
355            'l' => (height * 3) / 5,
356            'e' => (height * 3) / 5,
357            'd' => (height * 2) / 4,
358            _ => 0,
359        }
360    }
361
362    pub const fn get_led_width(&self, height: usize) -> usize {
363        let height = height - (TEXT_TOP_BOTTOM_PADDING * 2);
364
365        let l_width = self.get_char_width(height, 'l');
366        let e_width = self.get_char_width(height, 'e');
367        let d_width = self.get_char_width(height, 'd');
368        l_width + TEXT_SPACING + e_width + TEXT_SPACING + d_width
369    }
370}
371
372impl<
373        'a,
374        S: hil::screen::Screen<'a>,
375        const NUM_LEDS: usize,
376        const SCREEN_WIDTH: usize,
377        const SCREEN_HEIGHT: usize,
378    > LedIndexed for ScreenOnLed<'a, S, NUM_LEDS, SCREEN_WIDTH, SCREEN_HEIGHT>
379{
380    fn init(&self, _index: usize) {}
381
382    fn on(&self, index: usize) {
383        let mut leds = self.leds.get();
384        leds[index] = true;
385        self.leds.set(leds);
386        self.show_leds();
387    }
388
389    fn off(&self, index: usize) {
390        let mut leds = self.leds.get();
391        leds[index] = false;
392        self.leds.set(leds);
393        self.show_leds();
394    }
395
396    fn toggle(&self, index: usize) {
397        let mut leds = self.leds.get();
398        let updated = !leds[index];
399        leds[index] = updated;
400        self.leds.set(leds);
401        self.show_leds();
402    }
403
404    fn read(&self, index: usize) -> bool {
405        self.leds.get()[index]
406    }
407}
408
409impl<
410        'a,
411        S: hil::screen::Screen<'a>,
412        const NUM_LEDS: usize,
413        const SCREEN_WIDTH: usize,
414        const SCREEN_HEIGHT: usize,
415    > hil::screen::ScreenClient for ScreenOnLed<'a, S, NUM_LEDS, SCREEN_WIDTH, SCREEN_HEIGHT>
416{
417    fn command_complete(&self, _r: Result<(), ErrorCode>) {}
418
419    fn write_complete(&self, data: SubSliceMut<'static, u8>, _r: Result<(), ErrorCode>) {
420        self.buffer.replace(data.take());
421
422        // Check if LED state changed while we were writing. If so, do another
423        // screen write to update the LEDs.
424        if self.dirty.get() {
425            self.dirty.set(false);
426            self.show_leds();
427        }
428    }
429
430    fn screen_is_ready(&self) {
431        if !self.initialized.get() {
432            self.initialized.set(true);
433            self.initialize_leds();
434        }
435    }
436}