pub const CTRL_CMD: u8 = 0x00;
pub const CTRL_DATA: u8 = 0x40;
pub const DEFAULT_I2C_ADDR: u16 = 0x3C;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Controller {
Ssd1306,
Sh1106,
}
impl Controller {
pub fn column_offset(self) -> u8 {
match self {
Controller::Ssd1306 => 0,
Controller::Sh1106 => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OledGeometry {
pub width: u8,
pub height: u8,
}
impl OledGeometry {
pub const W128_H64: OledGeometry = OledGeometry {
width: 128,
height: 64,
};
pub const W128_H32: OledGeometry = OledGeometry {
width: 128,
height: 32,
};
pub fn pages(self) -> u8 {
self.height / 8
}
pub fn frame_bytes(self) -> usize {
self.width as usize * self.pages() as usize
}
}
pub fn init_commands(geom: OledGeometry) -> Vec<u8> {
let mux = geom.height - 1;
let com_pins = if geom.height == 32 { 0x02 } else { 0x12 };
vec![
0xAE, 0xD5, 0x80, 0xA8, mux, 0xD3, 0x00, 0x40, 0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8, 0xDA, com_pins, 0x81, 0xCF, 0xD9, 0xF1, 0xDB, 0x40, 0xA4, 0xA6, 0xAF, ]
}
pub fn pack_page_buffer(canvas: &[u8], geom: OledGeometry) -> Option<Vec<u8>> {
let w = geom.width as usize;
let h = geom.height as usize;
if canvas.len() != w * h {
return None;
}
let pages = geom.pages() as usize;
let mut out = vec![0u8; w * pages];
for page in 0..pages {
for col in 0..w {
let mut byte = 0u8;
for bit in 0..8 {
let y = page * 8 + bit;
if canvas[y * w + col] != 0 {
byte |= 1 << bit;
}
}
out[page * w + col] = byte;
}
}
Some(out)
}
pub fn page_upload_plan(
packed: &[u8],
geom: OledGeometry,
controller: Controller,
) -> Vec<(Vec<u8>, Vec<u8>)> {
let w = geom.width as usize;
let pages = geom.pages() as usize;
let col0 = controller.column_offset();
let mut plan = Vec::with_capacity(pages);
for page in 0..pages {
let cmds = vec![
0xB0 | page as u8, col0 & 0x0F, 0x10 | ((col0 >> 4) & 0x0F), ];
let start = page * w;
let data = packed[start..start + w].to_vec();
plan.push((cmds, data));
}
plan
}
pub trait I2cBus {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<()>;
}
pub fn render_frame<B: I2cBus>(
bus: &mut B,
packed: &[u8],
geom: OledGeometry,
controller: Controller,
) -> std::io::Result<()> {
for (cmds, data) in page_upload_plan(packed, geom, controller) {
write_cmd(bus, &cmds)?;
write_data(bus, &data)?;
}
Ok(())
}
pub fn write_cmd<B: I2cBus>(bus: &mut B, cmds: &[u8]) -> std::io::Result<()> {
let mut frame = Vec::with_capacity(cmds.len() + 1);
frame.push(CTRL_CMD);
frame.extend_from_slice(cmds);
bus.write(&frame)
}
pub fn write_data<B: I2cBus>(bus: &mut B, data: &[u8]) -> std::io::Result<()> {
let mut frame = Vec::with_capacity(data.len() + 1);
frame.push(CTRL_DATA);
frame.extend_from_slice(data);
bus.write(&frame)
}
pub fn init<B: I2cBus>(bus: &mut B, geom: OledGeometry) -> std::io::Result<()> {
write_cmd(bus, &init_commands(geom))
}
#[cfg(target_os = "linux")]
const I2C_SLAVE: u32 = 0x0703;
#[cfg(target_os = "linux")]
pub struct I2cDev {
file: std::fs::File,
}
#[cfg(target_os = "linux")]
impl I2cDev {
pub fn open(bus: u8, addr: u16) -> std::io::Result<Self> {
use std::os::fd::AsRawFd;
let path = format!("/dev/i2c-{bus}");
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)?;
let rc = unsafe { libc::ioctl(file.as_raw_fd(), I2C_SLAVE as _, addr as libc::c_int) };
if rc < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(Self { file })
}
}
#[cfg(target_os = "linux")]
impl I2cBus for I2cDev {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write;
self.file.write_all(bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct FakeBus {
frames: Vec<Vec<u8>>,
}
impl I2cBus for FakeBus {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<()> {
self.frames.push(bytes.to_vec());
Ok(())
}
}
#[test]
fn geometry_pages_and_frame_bytes() {
assert_eq!(OledGeometry::W128_H64.pages(), 8);
assert_eq!(OledGeometry::W128_H64.frame_bytes(), 128 * 8);
assert_eq!(OledGeometry::W128_H32.pages(), 4);
assert_eq!(OledGeometry::W128_H32.frame_bytes(), 128 * 4);
}
#[test]
fn controller_column_offset() {
assert_eq!(Controller::Ssd1306.column_offset(), 0);
assert_eq!(Controller::Sh1106.column_offset(), 2);
}
#[test]
fn init_sequence_tracks_height() {
let c64 = init_commands(OledGeometry::W128_H64);
let mux_idx = c64.iter().position(|&b| b == 0xA8).unwrap();
assert_eq!(c64[mux_idx + 1], 63);
let com_idx = c64.iter().position(|&b| b == 0xDA).unwrap();
assert_eq!(c64[com_idx + 1], 0x12);
assert_eq!(*c64.last().unwrap(), 0xAF);
let c32 = init_commands(OledGeometry::W128_H32);
let mux_idx = c32.iter().position(|&b| b == 0xA8).unwrap();
assert_eq!(c32[mux_idx + 1], 31);
let com_idx = c32.iter().position(|&b| b == 0xDA).unwrap();
assert_eq!(c32[com_idx + 1], 0x02);
}
#[test]
fn pack_page_buffer_vertical_byte_layout() {
let geom = OledGeometry {
width: 8,
height: 8,
};
let mut canvas = vec![0u8; 8 * 8];
for row in 0..8 {
canvas[row * 8] = 1; }
let packed = pack_page_buffer(&canvas, geom).unwrap();
assert_eq!(packed.len(), 8); assert_eq!(packed[0], 0xFF);
assert!(packed[1..].iter().all(|&b| b == 0));
}
#[test]
fn pack_page_buffer_single_pixel_sets_one_bit() {
let geom = OledGeometry {
width: 8,
height: 8,
};
let mut canvas = vec![0u8; 8 * 8];
canvas[5 * 8 + 3] = 1;
let packed = pack_page_buffer(&canvas, geom).unwrap();
assert_eq!(packed[3], 1 << 5);
}
#[test]
fn pack_page_buffer_second_page_indexing() {
let geom = OledGeometry {
width: 8,
height: 16,
};
let mut canvas = vec![0u8; 8 * 16];
canvas[8 * 8] = 1; let packed = pack_page_buffer(&canvas, geom).unwrap();
assert_eq!(packed.len(), 16); assert_eq!(packed[8], 1 << 0);
}
#[test]
fn pack_page_buffer_rejects_wrong_size() {
let geom = OledGeometry::W128_H64;
assert!(pack_page_buffer(&[0u8; 10], geom).is_none());
}
#[test]
fn page_upload_plan_applies_sh1106_offset() {
let geom = OledGeometry {
width: 4,
height: 8,
};
let packed = vec![1, 2, 3, 4];
let plan = page_upload_plan(&packed, geom, Controller::Ssd1306);
assert_eq!(plan.len(), 1);
assert_eq!(plan[0].0, vec![0xB0, 0x00, 0x10]);
assert_eq!(plan[0].1, vec![1, 2, 3, 4]);
let plan = page_upload_plan(&packed, geom, Controller::Sh1106);
assert_eq!(plan[0].0, vec![0xB0, 0x02, 0x10]);
}
#[test]
fn page_upload_plan_one_entry_per_page() {
let geom = OledGeometry {
width: 4,
height: 16,
};
let packed = vec![0u8; 8]; let plan = page_upload_plan(&packed, geom, Controller::Ssd1306);
assert_eq!(plan.len(), 2);
assert_eq!(plan[0].0[0], 0xB0); assert_eq!(plan[1].0[0], 0xB1); }
#[test]
fn write_cmd_and_data_prefix_control_bytes() {
let mut bus = FakeBus::default();
write_cmd(&mut bus, &[0xAE]).unwrap();
write_data(&mut bus, &[0x12, 0x34]).unwrap();
assert_eq!(bus.frames[0], vec![CTRL_CMD, 0xAE]);
assert_eq!(bus.frames[1], vec![CTRL_DATA, 0x12, 0x34]);
}
#[test]
fn render_frame_emits_cmd_then_data_per_page() {
let geom = OledGeometry {
width: 4,
height: 16,
}; let packed = vec![10, 11, 12, 13, 20, 21, 22, 23];
let mut bus = FakeBus::default();
render_frame(&mut bus, &packed, geom, Controller::Ssd1306).unwrap();
assert_eq!(bus.frames.len(), 4);
assert_eq!(bus.frames[0][0], CTRL_CMD);
assert_eq!(bus.frames[1], vec![CTRL_DATA, 10, 11, 12, 13]);
assert_eq!(bus.frames[2][0], CTRL_CMD);
assert_eq!(bus.frames[3], vec![CTRL_DATA, 20, 21, 22, 23]);
}
#[test]
fn init_writes_the_command_block_once() {
let mut bus = FakeBus::default();
init(&mut bus, OledGeometry::W128_H64).unwrap();
assert_eq!(bus.frames.len(), 1);
assert_eq!(bus.frames[0][0], CTRL_CMD);
assert_eq!(*bus.frames[0].last().unwrap(), 0xAF);
}
}