refactor: project structure (#9)
Split up project into multiple crates and use a Cargo workspace.
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025 Markus Zehnder
|
||||
|
||||
use crate::FakeSerialPort;
|
||||
use crate::ToRgb565;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use log::{debug, error, info, warn};
|
||||
use serialport::{SerialPort, SerialPortType};
|
||||
use std::io::{Read, Write};
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub const DISPLAY_SIZE: (u32, u32) = (960, 376);
|
||||
|
||||
const SERIAL_RETRY: u8 = 3;
|
||||
const UART_BAUDRATE: u32 = 1_500_000;
|
||||
|
||||
const USB_UART_VID: u16 = 0x416;
|
||||
const USB_UART_PID: u16 = 0x90A1;
|
||||
|
||||
const IMG_CHUNK_SIZE: usize = 47;
|
||||
|
||||
static DISPLAY_OFF: [u8; 8] = [0xAA, 0x55, 0xAA, 0x55, 0x0A, 0x00, 0x00, 0x00];
|
||||
static DISPLAY_ON: [u8; 8] = [0xAA, 0x55, 0xAA, 0x55, 0x0B, 0x00, 0x00, 0x00];
|
||||
|
||||
static HEADER_START: [u8; 16] = [
|
||||
0xAA, 0x55, 0xAA, 0x55, 0x05, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0F, 0x2F, 0x00, 0x04, 0x0B, 0x00,
|
||||
];
|
||||
static HEADER_END: [u8; 8] = [0xAA, 0x55, 0xAA, 0x55, 0x06, 0x00, 0x00, 0x00];
|
||||
static HEADER: [u8; 8] = [0xAA, 0x55, 0xAA, 0x55, 0x08, 0x00, 0x00, 0x00];
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AooScreenBuilder {
|
||||
timeout: Option<Duration>,
|
||||
enable_cache: Option<bool>,
|
||||
no_init_check: Option<bool>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl AooScreenBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
/// Set the amount of time to wait to receive data before timing out. Defaults to 1 sec.
|
||||
pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
|
||||
self.timeout = Some(timeout);
|
||||
self
|
||||
}
|
||||
|
||||
/// Cache previous frame sent to display for future diff updates. Enabled by default.
|
||||
pub fn enable_cache(&mut self, enable: bool) -> &mut Self {
|
||||
self.enable_cache = Some(enable);
|
||||
self
|
||||
}
|
||||
|
||||
/// Disable LCD initialization check and only write data to the display. Defaults to false.
|
||||
pub fn no_init_check(&mut self, no_check: bool) -> &mut Self {
|
||||
self.no_init_check = Some(no_check);
|
||||
self
|
||||
}
|
||||
|
||||
/// Open the default AOOSTAR LCD USB UART device 416:90A1.
|
||||
pub fn open_default(self) -> anyhow::Result<AooScreen> {
|
||||
self.open_usb(USB_UART_VID, USB_UART_PID)
|
||||
}
|
||||
|
||||
/// Simulate the LCD device. No real device or serial port is required.
|
||||
pub fn simulate(self) -> anyhow::Result<AooScreen> {
|
||||
Ok(AooScreen {
|
||||
port: Some(Box::new(FakeSerialPort::new())),
|
||||
enable_cache: self.enable_cache.unwrap_or(true),
|
||||
prev_frame: None,
|
||||
no_init_check: self.no_init_check.unwrap_or(false),
|
||||
})
|
||||
}
|
||||
|
||||
/// Open the specified USB UART device id. Format: vid:pid
|
||||
pub fn open_usb_id(self, id: &str) -> anyhow::Result<AooScreen> {
|
||||
let (vid, pid) = id
|
||||
.split_once(':')
|
||||
.with_context(|| "Error parsing serial port ID. Expected `vid:pid` format.")?;
|
||||
self.open_usb(u16::from_str_radix(vid, 16)?, u16::from_str_radix(pid, 16)?)
|
||||
}
|
||||
|
||||
/// Open the specified USB UART
|
||||
pub fn open_usb(self, vid: u16, pid: u16) -> anyhow::Result<AooScreen> {
|
||||
let serial_dev = find_usb_serial_port(vid, pid)?;
|
||||
self.open_device(&serial_dev)
|
||||
}
|
||||
|
||||
/// Open the specified serial device
|
||||
pub fn open_device(self, device: &str) -> anyhow::Result<AooScreen> {
|
||||
let port = serialport::new(device, UART_BAUDRATE)
|
||||
.timeout(self.timeout.unwrap_or(Duration::from_millis(1000)))
|
||||
.open()
|
||||
.with_context(|| format!("Error opening serial port: {device}"))?;
|
||||
|
||||
info!(
|
||||
"Opened serial port {device}: baud={}, {}:{}:{}",
|
||||
port.baud_rate()?,
|
||||
port.data_bits()?,
|
||||
port.parity()?,
|
||||
port.stop_bits()?
|
||||
);
|
||||
|
||||
Ok(AooScreen {
|
||||
port: Some(port),
|
||||
enable_cache: self.enable_cache.unwrap_or(true),
|
||||
prev_frame: None,
|
||||
no_init_check: self.no_init_check.unwrap_or(false),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AooScreen {
|
||||
port: Option<Box<dyn SerialPort>>,
|
||||
enable_cache: bool,
|
||||
prev_frame: Option<BytesMut>,
|
||||
no_init_check: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl AooScreen {
|
||||
pub fn init(&mut self) -> anyhow::Result<()> {
|
||||
let port = self.port.as_mut().ok_or(anyhow!("LCD port not open"))?;
|
||||
|
||||
port.write(&DISPLAY_ON)
|
||||
.with_context(|| "Error sending display on command")?;
|
||||
|
||||
if self.no_init_check {
|
||||
warn!("Test mode: only writing to the display");
|
||||
} else {
|
||||
// quick and dirty response check as in the original app
|
||||
sleep(Duration::from_secs(1));
|
||||
|
||||
let available = port
|
||||
.bytes_to_read()
|
||||
.with_context(|| "Failed to get available bytes from serial port")?;
|
||||
if available == 0 {
|
||||
return Err(anyhow!("Initialization failed, no response received"));
|
||||
}
|
||||
let mut serial_buf: Vec<u8> = vec![0; available as usize];
|
||||
port.read(serial_buf.as_mut_slice())
|
||||
.with_context(|| "Failed to read from serial port")?;
|
||||
|
||||
let marker = b'A';
|
||||
if !serial_buf.contains(&marker) {
|
||||
return Err(anyhow!(
|
||||
"Initialization failed, received: {}",
|
||||
String::from_utf8_lossy(&serial_buf)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
info!("Display initialized!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn close(&mut self) {
|
||||
if self.port.is_some() {
|
||||
if let Err(e) = self.off() {
|
||||
warn!("Failed to close display: {e}");
|
||||
}
|
||||
self.port = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on(&mut self) -> anyhow::Result<()> {
|
||||
self.send(&DISPLAY_ON)
|
||||
.with_context(|| "Failed to send display on")
|
||||
}
|
||||
|
||||
pub fn off(&mut self) -> anyhow::Result<()> {
|
||||
self.send(&DISPLAY_OFF)
|
||||
.with_context(|| "Failed to send display off")
|
||||
}
|
||||
|
||||
pub fn send_image(&mut self, image: impl ToRgb565) -> anyhow::Result<()> {
|
||||
let img_rgb565 = image.to_rgb565_le();
|
||||
debug!(
|
||||
"Start sending image (size {}) {} cache... ",
|
||||
img_rgb565.len(),
|
||||
if self.enable_cache && self.prev_frame.is_some() {
|
||||
"with"
|
||||
} else {
|
||||
"without"
|
||||
}
|
||||
);
|
||||
|
||||
let start_time = Instant::now();
|
||||
self.send(&HEADER_START)
|
||||
.with_context(|| "Failed to send header start")?;
|
||||
|
||||
let mut buf = BytesMut::with_capacity(HEADER.len() + 4 + IMG_CHUNK_SIZE);
|
||||
let mut sent_chunks = 0;
|
||||
for (idx, chunk) in img_rgb565.chunks(IMG_CHUNK_SIZE).enumerate() {
|
||||
let offset = idx * IMG_CHUNK_SIZE;
|
||||
|
||||
if self.enable_cache
|
||||
&& let Some(cache) = self.prev_frame.as_mut()
|
||||
{
|
||||
let offset = idx * IMG_CHUNK_SIZE;
|
||||
if offset + IMG_CHUNK_SIZE <= cache.len()
|
||||
&& cache[offset..offset + IMG_CHUNK_SIZE].eq(chunk)
|
||||
{
|
||||
// Block is unchanged from the previous frame; skip sending
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
buf.clear();
|
||||
buf.extend(&HEADER);
|
||||
buf.put_u32_le(offset as u32);
|
||||
buf.extend(chunk);
|
||||
|
||||
self.send(&buf)
|
||||
.with_context(|| format!("Failed to send image data chunk {idx}"))?;
|
||||
sent_chunks += 1;
|
||||
}
|
||||
|
||||
self.send(&HEADER_END)
|
||||
.with_context(|| "Failed to send header end")?;
|
||||
|
||||
if self.enable_cache {
|
||||
self.prev_frame.replace(img_rgb565);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Image sent: {}ms, {sent_chunks} chunks",
|
||||
start_time.elapsed().as_millis()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn enable_cache(&mut self, enable: bool) {
|
||||
self.enable_cache = enable;
|
||||
if !enable {
|
||||
self.clear_cache();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_cache_enabled(&self) -> bool {
|
||||
self.enable_cache
|
||||
}
|
||||
|
||||
pub fn clear_cache(&mut self) {
|
||||
self.prev_frame = None;
|
||||
}
|
||||
|
||||
fn send(&mut self, data: &[u8]) -> anyhow::Result<()> {
|
||||
// TODO not sure if retry logic is required. Need a real device to test...
|
||||
let mut retry = 0;
|
||||
|
||||
let port = self.port.as_mut().ok_or(anyhow!("LCD port not open"))?;
|
||||
|
||||
loop {
|
||||
return match port.write_all(data) {
|
||||
Ok(()) => {
|
||||
port.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Bytes queued to send: {}",
|
||||
port.bytes_to_write()
|
||||
.with_context(|| "Error calling bytes_to_write")?
|
||||
);
|
||||
if retry < SERIAL_RETRY {
|
||||
warn!("Failed to write to display, retrying! Error: {e}");
|
||||
retry += 1;
|
||||
continue;
|
||||
}
|
||||
error!("Failed to write to display: {e}");
|
||||
Err(e.into())
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_usb_serial_port(vid: u16, pid: u16) -> serialport::Result<String> {
|
||||
info!("Looking for USB serial port {vid:x}:{pid:x}");
|
||||
let ports = serialport::available_ports()?;
|
||||
for p in ports {
|
||||
debug!("Found serial port: {}", p.port_name);
|
||||
if let SerialPortType::UsbPort(info) = p.port_type
|
||||
&& info.pid == pid
|
||||
&& info.vid == vid
|
||||
{
|
||||
return Ok(p.port_name);
|
||||
}
|
||||
}
|
||||
|
||||
Err(serialport::Error::new(
|
||||
serialport::ErrorKind::NoDevice,
|
||||
format!("USB serial port {vid:x}:{pid:x} not found"),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025 Markus Zehnder
|
||||
|
||||
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPort, StopBits};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct FakeSerialPort {
|
||||
baud_rate: u32,
|
||||
data_bits: DataBits,
|
||||
flow_control: FlowControl,
|
||||
parity: Parity,
|
||||
stop_bits: StopBits,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl Default for FakeSerialPort {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeSerialPort {
|
||||
pub fn new() -> FakeSerialPort {
|
||||
Self {
|
||||
baud_rate: 1_500_000,
|
||||
data_bits: DataBits::Eight,
|
||||
flow_control: FlowControl::None,
|
||||
parity: Parity::None,
|
||||
stop_bits: StopBits::One,
|
||||
timeout: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Read for FakeSerialPort {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
buf[0] = b'A';
|
||||
Ok(1)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for FakeSerialPort {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
// just some approximation, additional overhead like flushing etc is not considered
|
||||
let byte_rate =
|
||||
self.baud_rate / (1 + u8::from(self.data_bits) + u8::from(self.stop_bits)) as u32;
|
||||
let delay = Duration::from_micros((buf.len() * 1000 * 1000 / byte_rate as usize) as u64);
|
||||
sleep(delay);
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SerialPort for FakeSerialPort {
|
||||
fn name(&self) -> Option<String> {
|
||||
Some("Dummy Serial".into())
|
||||
}
|
||||
|
||||
fn baud_rate(&self) -> serialport::Result<u32> {
|
||||
Ok(self.baud_rate)
|
||||
}
|
||||
|
||||
fn data_bits(&self) -> serialport::Result<DataBits> {
|
||||
Ok(self.data_bits)
|
||||
}
|
||||
|
||||
fn flow_control(&self) -> serialport::Result<FlowControl> {
|
||||
Ok(self.flow_control)
|
||||
}
|
||||
|
||||
fn parity(&self) -> serialport::Result<Parity> {
|
||||
Ok(self.parity)
|
||||
}
|
||||
|
||||
fn stop_bits(&self) -> serialport::Result<StopBits> {
|
||||
Ok(self.stop_bits)
|
||||
}
|
||||
|
||||
fn timeout(&self) -> Duration {
|
||||
self.timeout
|
||||
}
|
||||
|
||||
fn set_baud_rate(&mut self, baud_rate: u32) -> serialport::Result<()> {
|
||||
self.baud_rate = baud_rate;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_data_bits(&mut self, data_bits: DataBits) -> serialport::Result<()> {
|
||||
self.data_bits = data_bits;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_flow_control(&mut self, flow_control: FlowControl) -> serialport::Result<()> {
|
||||
self.flow_control = flow_control;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_parity(&mut self, parity: Parity) -> serialport::Result<()> {
|
||||
self.parity = parity;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_stop_bits(&mut self, stop_bits: StopBits) -> serialport::Result<()> {
|
||||
self.stop_bits = stop_bits;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_timeout(&mut self, timeout: Duration) -> serialport::Result<()> {
|
||||
self.timeout = timeout;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_request_to_send(&mut self, _level: bool) -> serialport::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_data_terminal_ready(&mut self, _level: bool) -> serialport::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_clear_to_send(&mut self) -> serialport::Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn read_data_set_ready(&mut self) -> serialport::Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn read_ring_indicator(&mut self) -> serialport::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn read_carrier_detect(&mut self) -> serialport::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn bytes_to_read(&self) -> serialport::Result<u32> {
|
||||
Ok(1)
|
||||
}
|
||||
|
||||
fn bytes_to_write(&self) -> serialport::Result<u32> {
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn clear(&self, _buffer_to_clear: ClearBuffer) -> serialport::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_clone(&self) -> serialport::Result<Box<dyn SerialPort>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn set_break(&self) -> serialport::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_break(&self) -> serialport::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025 Markus Zehnder
|
||||
|
||||
#![forbid(non_ascii_idents)]
|
||||
#![deny(unsafe_code)]
|
||||
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use image::{RgbImage, RgbaImage};
|
||||
|
||||
mod aoo_screen;
|
||||
mod fake_serialport;
|
||||
|
||||
pub use aoo_screen::{AooScreen, AooScreenBuilder, DISPLAY_SIZE};
|
||||
pub use fake_serialport::FakeSerialPort;
|
||||
|
||||
/// Trait definition to get a RGB 565 representation from a source image.
|
||||
pub trait ToRgb565 {
|
||||
/// Get an RGB 565 representation of the image in little endian format.
|
||||
fn to_rgb565_le(&self) -> BytesMut;
|
||||
|
||||
/// Convert a single RGB 888 pixel to 16 bit RGB 565 format.
|
||||
fn convert_rgb(&self, r: u8, g: u8, b: u8) -> u16 {
|
||||
((r & 248) as u16) << 8 | ((g & 252) as u16) << 3 | ((b as u16) >> 3)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO quick & dirty approach for converting RgbImage & RgbaImage to RGB 565.
|
||||
// There should be a more generic way, maybe with PixelEnumerator...
|
||||
impl ToRgb565 for &RgbImage {
|
||||
fn to_rgb565_le(&self) -> BytesMut {
|
||||
let mut img_rgb565 =
|
||||
BytesMut::with_capacity(self.width() as usize * self.height() as usize * 2);
|
||||
|
||||
for (_x, _y, pixel) in self.enumerate_pixels() {
|
||||
img_rgb565.put_u16_le(self.convert_rgb(pixel.0[0], pixel.0[1], pixel.0[2]));
|
||||
}
|
||||
|
||||
img_rgb565
|
||||
}
|
||||
}
|
||||
|
||||
impl ToRgb565 for &RgbaImage {
|
||||
fn to_rgb565_le(&self) -> BytesMut {
|
||||
let mut img_rgb565 =
|
||||
BytesMut::with_capacity(self.width() as usize * self.height() as usize * 2);
|
||||
|
||||
for (_x, _y, pixel) in self.enumerate_pixels() {
|
||||
img_rgb565.put_u16_le(self.convert_rgb(pixel.0[0], pixel.0[1], pixel.0[2]));
|
||||
}
|
||||
|
||||
img_rgb565
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user