qmk_oled_api/
screen.rs

1use std::ffi::CStr;
2use std::fmt::Display;
3use std::fs;
4use std::path::Path;
5
6use fontdue::Font;
7use hidapi::{HidApi, HidError};
8use image::imageops::{dither, BiLevel, FilterType};
9use itertools::Itertools;
10
11use crate::data::{DataPacket, HidAdapter, PAYLOAD_SIZE};
12use crate::utils::{get_bit_at_index, set_bit_at_index};
13
14pub struct OledScreen32x128 {
15    data: [[u8; 128]; 4],
16    device: Box<dyn HidAdapter>,
17}
18
19impl Display for OledScreen32x128 {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        let string = self
22            .data
23            .iter()
24            .map(|row| row.map(|byte| format!("{byte:08b}")).join(""))
25            .join("\n")
26            .replace('0', "░")
27            .replace('1', "▓");
28        f.write_str(&string)
29    }
30}
31
32impl OledScreen32x128 {
33    pub fn from_path(device_path: &CStr) -> Result<Self, HidError> {
34        let api = HidApi::new()?;
35        let device = api.open_path(device_path)?;
36        Ok(Self {
37            data: [[0; 128]; 4],
38            device: Box::new(device),
39        })
40    }
41
42    pub fn from_id(vid: u16, pid: u16, usage_page: u16) -> Result<Self, HidError> {
43        let api = HidApi::new()?;
44
45        let device_info = api.device_list().find(|dev| dev.vendor_id() == vid && dev.product_id() == pid && dev.usage_page() == usage_page);
46        if let Some(device_info) = device_info {
47            let device = device_info.open_device(&api)?;
48            Ok(Self {
49                data: [[0; 128]; 4],
50                device: Box::new(device),
51            })
52        } else {
53            Err(HidError::HidApiError { message: "Could not find specified device".into() })
54        }
55    }
56
57    pub fn from_device(device: impl HidAdapter + 'static) -> Result<Self, HidError> {
58        Ok(Self {
59            data: [[0; 128]; 4],
60            device: Box::new(device),
61        })
62    }
63
64    pub(crate) fn to_packets(&self) -> Vec<DataPacket> {
65        self.data
66            .iter()
67            .flatten()
68            .chunks(PAYLOAD_SIZE - 2)
69            .into_iter()
70            .map(|chunk| {
71                let mut output_array: [u8; PAYLOAD_SIZE - 2] = [0; PAYLOAD_SIZE - 2];
72                chunk
73                    .take(PAYLOAD_SIZE - 2)
74                    .enumerate()
75                    .for_each(|(index, byte)| output_array[index] = *byte);
76                output_array
77            })
78            .enumerate()
79            .map(|(index, chunk)| DataPacket::new(index.try_into().unwrap(), chunk))
80            .collect()
81    }
82
83    pub fn draw_image<P: AsRef<Path>>(&mut self, bitmap_file: P, x: usize, y: usize, scale: bool) {
84        let mut image = image::open(bitmap_file).unwrap();
85        if scale {
86            // TODO: Find a better way of specifying canvas size
87            image = image.resize(32, 128, FilterType::Lanczos3);
88        }
89
90        let mut image = image.grayscale();
91        let image = image.as_mut_luma8().unwrap();
92        dither(image, &BiLevel);
93
94        let image_width = image.width();
95        let image_height = image.height();
96
97        for (index, pixel) in image.pixels().enumerate() {
98            let row = index / image_width as usize;
99            let col = index % image_width as usize;
100
101            let enabled = pixel.0[0] == 255;
102
103            self.set_pixel(x + col, y + image_height as usize - row, enabled)
104        }
105    }
106
107    pub fn draw_text(
108        &mut self,
109        text: &str,
110        x: usize,
111        y: usize,
112        size: f32,
113        font_path: Option<&str>,
114    ) {
115        let font = if let Some(font_path) = font_path {
116            let font_bytes = fs::read(&font_path).unwrap();
117            Font::from_bytes(font_bytes, fontdue::FontSettings::default()).unwrap()
118        } else {
119            Font::from_bytes(
120                include_bytes!("../assets/cozette.ttf") as &[u8],
121                fontdue::FontSettings::default(),
122            )
123            .unwrap()
124        };
125
126        let mut x_cursor = x;
127
128        for letter in text.chars() {
129            let width = font.metrics(letter, size).width;
130            self.draw_letter(letter, x_cursor, y, size, &font);
131
132            // FIXME: Use horizontal kerning as opposed to abstract value of "2"
133            x_cursor += width + 2
134        }
135    }
136
137    fn draw_letter(&mut self, letter: char, x: usize, y: usize, size: f32, font: &Font) {
138        let (metrics, bitmap) = font.rasterize(letter, size);
139
140        for (index, byte) in bitmap.into_iter().enumerate() {
141            let col = x + (index % metrics.width);
142            let row = y + metrics.height - (index / metrics.width);
143            let enabled = (byte as f32 / 255.0).round() as i32 == 1;
144            self.set_pixel(col, row, enabled)
145        }
146    }
147
148    pub fn send(&self) -> Result<(), HidError> {
149        let packets = self.to_packets();
150
151        for packet in packets {
152            packet.send(self.device.as_ref())?;
153        }
154
155        Ok(())
156    }
157
158    pub fn clear(&mut self) {
159        self.data = [[0; 128]; 4];
160    }
161
162    pub fn fill_all(&mut self) {
163        self.data = [[1; 128]; 4];
164    }
165
166    pub fn paint_region(
167        &mut self,
168        min_x: usize,
169        min_y: usize,
170        max_x: usize,
171        max_y: usize,
172        enabled: bool,
173    ) {
174        for x in min_x..max_x {
175            for y in min_y..max_y {
176                self.set_pixel(x, y, enabled)
177            }
178        }
179    }
180
181    pub fn get_pixel(&self, x: usize, y: usize) -> bool {
182        let byte_index = x / 8;
183        let bit_index: u8 = 7 - ((x % 8) as u8);
184
185        let byte = self.data[byte_index][y];
186        get_bit_at_index(byte, bit_index)
187    }
188
189    /// Underlying function for drawing to the canvas, if provided coordinates are out of range,
190    /// this function will fail silently
191    ///
192    /// # Arguments
193    /// * `x` - The x coordinate of the pixel to set
194    /// * `y` - The y coordinate of the pixel to set
195    /// * `enabled` - Whether to set the pixel to an enabled or disabled state (on/off)
196    pub fn set_pixel(&mut self, x: usize, y: usize, enabled: bool) {
197        if x > 31 || y > 127 {
198            // If a pixel is rendered outside of the canvas, fail silently
199            return;
200        }
201
202        let target_byte = x / 8;
203        let target_bit: u8 = 7 - ((x % 8) as u8);
204
205        self.data[target_byte][y] =
206            set_bit_at_index(self.data[target_byte][y], target_bit, enabled);
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    struct MockHidDevice;
215
216    impl HidAdapter for MockHidDevice {
217        fn write(&self, data: &[u8]) -> Result<usize, HidError> {
218            println!("Writing data {data:?}");
219            Ok(1)
220        }
221    }
222
223    const MOCK_DEVICE: MockHidDevice = MockHidDevice;
224
225    #[test]
226    fn test_display_oled_screen() {
227        let mut screen = OledScreen32x128::from_device(MOCK_DEVICE).unwrap();
228        for i in 0..128 {
229            screen.set_pixel(0, i, true);
230            screen.set_pixel(31, i, true);
231        }
232        // FIXME: ASSERT
233    }
234
235    #[test]
236    fn test_to_packets() {
237        let screen = OledScreen32x128::from_device(MOCK_DEVICE).unwrap();
238        screen.to_packets();
239        // FIXME: ASSERT
240    }
241
242    #[test]
243    fn test_draw_image() {
244        let mut screen = OledScreen32x128::from_device(MOCK_DEVICE).unwrap();
245        screen.draw_image("assets/bitmaps/test_square.bmp", 0, 0, false);
246        // FIXME: ASSERT
247    }
248
249    #[test]
250    fn test_draw_text() {
251        let mut screen = OledScreen32x128::from_device(MOCK_DEVICE).unwrap();
252        screen.draw_text("Hey", 0, 0, 8.0, None);
253
254        assert_eq!(
255            screen.data,
256            [
257                [
258                    0, 136, 8, 138, 138, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
259                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
260                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
261                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
262                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
263                ],
264                [
265                    0, 65, 128, 227, 129, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
266                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
267                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
268                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
269                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
270                    0
271                ],
272                [
273                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
274                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
275                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
276                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
277                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
278                ],
279                [
280                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
281                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
282                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
283                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
284                    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
285                ]
286            ]
287        );
288    }
289}