use std::ffi::CStr;
use std::fmt::Display;
use std::fs;
use std::path::Path;
use fontdue::Font;
use hidapi::{HidApi, HidError};
use image::imageops::{dither, BiLevel, FilterType};
use itertools::Itertools;
use crate::data::{DataPacket, HidAdapter, PAYLOAD_SIZE};
use crate::utils::{get_bit_at_index, set_bit_at_index};
pub struct OledScreen32x128 {
data: [[u8; 128]; 4],
device: Box<dyn HidAdapter>,
}
impl Display for OledScreen32x128 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = self
.data
.iter()
.map(|row| row.map(|byte| format!("{byte:08b}")).join(""))
.join("\n")
.replace('0', "░")
.replace('1', "▓");
f.write_str(&string)
}
}
impl OledScreen32x128 {
pub fn from_path(device_path: &CStr) -> Result<Self, HidError> {
let api = HidApi::new()?;
let device = api.open_path(device_path)?;
Ok(Self {
data: [[0; 128]; 4],
device: Box::new(device),
})
}
pub fn from_id(vid: u16, pid: u16, usage_page: u16) -> Result<Self, HidError> {
let api = HidApi::new()?;
let device_info = api.device_list().find(|dev| dev.vendor_id() == vid && dev.product_id() == pid && dev.usage_page() == usage_page);
if let Some(device_info) = device_info {
let device = device_info.open_device(&api)?;
Ok(Self {
data: [[0; 128]; 4],
device: Box::new(device),
})
} else {
Err(HidError::HidApiError { message: "Could not find specified device".into() })
}
}
pub fn from_device(device: impl HidAdapter + 'static) -> Result<Self, HidError> {
Ok(Self {
data: [[0; 128]; 4],
device: Box::new(device),
})
}
pub(crate) fn to_packets(&self) -> Vec<DataPacket> {
self.data
.iter()
.flatten()
.chunks(PAYLOAD_SIZE - 2)
.into_iter()
.map(|chunk| {
let mut output_array: [u8; PAYLOAD_SIZE - 2] = [0; PAYLOAD_SIZE - 2];
chunk
.take(PAYLOAD_SIZE - 2)
.enumerate()
.for_each(|(index, byte)| output_array[index] = *byte);
output_array
})
.enumerate()
.map(|(index, chunk)| DataPacket::new(index.try_into().unwrap(), chunk))
.collect()
}
pub fn draw_image<P: AsRef<Path>>(&mut self, bitmap_file: P, x: usize, y: usize, scale: bool) {
let mut image = image::open(bitmap_file).unwrap();
if scale {
image = image.resize(32, 128, FilterType::Lanczos3);
}
let mut image = image.grayscale();
let image = image.as_mut_luma8().unwrap();
dither(image, &BiLevel);
let image_width = image.width();
let image_height = image.height();
for (index, pixel) in image.pixels().enumerate() {
let row = index / image_width as usize;
let col = index % image_width as usize;
let enabled = pixel.0[0] == 255;
self.set_pixel(x + col, y + image_height as usize - row, enabled)
}
}
pub fn draw_text(
&mut self,
text: &str,
x: usize,
y: usize,
size: f32,
font_path: Option<&str>,
) {
let font = if let Some(font_path) = font_path {
let font_bytes = fs::read(&font_path).unwrap();
Font::from_bytes(font_bytes, fontdue::FontSettings::default()).unwrap()
} else {
Font::from_bytes(
include_bytes!("../assets/cozette.ttf") as &[u8],
fontdue::FontSettings::default(),
)
.unwrap()
};
let mut x_cursor = x;
for letter in text.chars() {
let width = font.metrics(letter, size).width;
self.draw_letter(letter, x_cursor, y, size, &font);
x_cursor += width + 2
}
}
fn draw_letter(&mut self, letter: char, x: usize, y: usize, size: f32, font: &Font) {
let (metrics, bitmap) = font.rasterize(letter, size);
for (index, byte) in bitmap.into_iter().enumerate() {
let col = x + (index % metrics.width);
let row = y + metrics.height - (index / metrics.width);
let enabled = (byte as f32 / 255.0).round() as i32 == 1;
self.set_pixel(col, row, enabled)
}
}
pub fn send(&self) -> Result<(), HidError> {
let packets = self.to_packets();
for packet in packets {
packet.send(self.device.as_ref())?;
}
Ok(())
}
pub fn clear(&mut self) {
self.data = [[0; 128]; 4];
}
pub fn fill_all(&mut self) {
self.data = [[1; 128]; 4];
}
pub fn paint_region(
&mut self,
min_x: usize,
min_y: usize,
max_x: usize,
max_y: usize,
enabled: bool,
) {
for x in min_x..max_x {
for y in min_y..max_y {
self.set_pixel(x, y, enabled)
}
}
}
pub fn get_pixel(&self, x: usize, y: usize) -> bool {
let byte_index = x / 8;
let bit_index: u8 = 7 - ((x % 8) as u8);
let byte = self.data[byte_index][y];
get_bit_at_index(byte, bit_index)
}
pub fn set_pixel(&mut self, x: usize, y: usize, enabled: bool) {
if x > 31 || y > 127 {
return;
}
let target_byte = x / 8;
let target_bit: u8 = 7 - ((x % 8) as u8);
self.data[target_byte][y] =
set_bit_at_index(self.data[target_byte][y], target_bit, enabled);
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockHidDevice;
impl HidAdapter for MockHidDevice {
fn write(&self, data: &[u8]) -> Result<usize, HidError> {
println!("Writing data {data:?}");
Ok(1)
}
}
const MOCK_DEVICE: MockHidDevice = MockHidDevice;
#[test]
fn test_display_oled_screen() {
let mut screen = OledScreen32x128::from_device(MOCK_DEVICE).unwrap();
for i in 0..128 {
screen.set_pixel(0, i, true);
screen.set_pixel(31, i, true);
}
}
#[test]
fn test_to_packets() {
let screen = OledScreen32x128::from_device(MOCK_DEVICE).unwrap();
screen.to_packets();
}
#[test]
fn test_draw_image() {
let mut screen = OledScreen32x128::from_device(MOCK_DEVICE).unwrap();
screen.draw_image("assets/bitmaps/test_square.bmp", 0, 0, false);
}
#[test]
fn test_draw_text() {
let mut screen = OledScreen32x128::from_device(MOCK_DEVICE).unwrap();
screen.draw_text("Hey", 0, 0, 8.0, None);
assert_eq!(
screen.data,
[
[
0, 136, 8, 138, 138, 8, 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, 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, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
],
[
0, 65, 128, 227, 129, 128, 128, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
]
);
}
}