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 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 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 pub fn set_pixel(&mut self, x: usize, y: usize, enabled: bool) {
197 if x > 31 || y > 127 {
198 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 }
234
235 #[test]
236 fn test_to_packets() {
237 let screen = OledScreen32x128::from_device(MOCK_DEVICE).unwrap();
238 screen.to_packets();
239 }
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 }
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}