From 652246c77a87a8d934194f7c5ef1dbf6121383e3 Mon Sep 17 00:00:00 2001 From: filipton Date: Mon, 9 Mar 2026 18:40:47 +0100 Subject: [PATCH 01/31] wip: v4 firmware [SKIP CI] --- Cargo.lock | 5 +- Cargo.toml | 5 +- src/board.rs | 136 +++++++++++++++++++++++++++++-------------- src/buttons.rs | 8 ++- src/main.rs | 22 ++++--- src/rfid.rs | 19 ++++-- src/stackmat.rs | 8 +-- src/utils/buttons.rs | 16 ++++- 8 files changed, 147 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0decc63..771a9b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -959,10 +959,9 @@ dependencies = [ [[package]] name = "esp-hal-mfrc522" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60cb9b21a888cca9d719cde04541e6f8a0a383097e2986cf2ca99931e6f136d1" +version = "0.3.4" dependencies = [ + "embassy-sync 0.7.2", "embassy-time", "embedded-hal-async", "heapless 0.9.2", diff --git a/Cargo.toml b/Cargo.toml index f5aab4a..bb38dfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,8 @@ embedded-hal-bus = { version = "0.3.0", features = ["async"] } embedded-tls = { git = "https://github.com/filipton/embedded-tls", default-features = false, features = ["alloc", "embedded-io-adapters", "log"] } rand_core = { version = "0.6.4", features = ["getrandom"] } ag-lcd-async = { git = "https://github.com/filipton/ag-lcd-async", features = [] } -esp-hal-mfrc522 = { version = "0.3.3", features = ["embassy-time"] } +#esp-hal-mfrc522 = { version = "0.3.4", features = ["embassy-time"] } +esp-hal-mfrc522 = { path = "../esp-hal-mfrc522", features = ["alloc", "embassy-time"] } esp-bootloader-esp-idf = { version = "0.4.0", features = ["log-04", "esp32c3"] } trouble-host = { version = "0.5.1", features = ["scan", "security"] } @@ -61,6 +62,8 @@ e2e = [] qa = [] sleep = [] auto_add = [] +v3 = [] +v4 = [] [profile.dev] opt-level = 3 diff --git a/src/board.rs b/src/board.rs index 29e88bf..e6d0288 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1,15 +1,23 @@ use crate::utils::stackmat::{DEC_DIGITS, DOT_MOD}; use adv_shift_registers::wrappers::ShifterValueRange; +use alloc::rc::Rc; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; use embedded_hal::digital::OutputPin; use esp_hal::{ + Async, gpio::{AnyPin, Input, InputConfig, Level, Output, Pin, Pull}, + i2c::master::I2c, peripherals::{ - ADC1, AES, BT, FLASH, Peripherals, SPI2, SW_INTERRUPT, TIMG0, TIMG1, UART1, WIFI, + self, ADC1, AES, BT, FLASH, I2C0, Peripherals, SPI2, SW_INTERRUPT, TIMG0, TIMG1, UART1, + WIFI, }, rng::Rng, + time::Rate, timer::timg::TimerGroup, }; +pub type MutexI2C = Rc>>; + #[allow(dead_code)] pub struct Board { // peripherals @@ -27,19 +35,24 @@ pub struct Board { pub sw_interrupt: SW_INTERRUPT<'static>, // spi - pub miso: AnyPin<'static>, - pub mosi: AnyPin<'static>, - pub sck: AnyPin<'static>, - pub cs: adv_shift_registers::wrappers::ShifterPin, + //pub miso: AnyPin<'static>, + //pub mosi: AnyPin<'static>, + //pub sck: AnyPin<'static>, + //pub cs: adv_shift_registers::wrappers::ShifterPin, + + // i2c + pub i2c: MutexI2C, pub stackmat_rx: AnyPin<'static>, - pub battery: esp_hal::peripherals::GPIO2<'static>, - pub button_input: Input<'static>, - pub digits_shifters: ShifterValueRange, + pub buttons: [Input<'static>; 4], + + // pub battery: esp_hal::peripherals::GPIO2<'static>, + //pub button_input: Input<'static>, + //pub digits_shifters: ShifterValueRange, - pub buttons_shifter: adv_shift_registers::wrappers::ShifterValue, - pub lcd: adv_shift_registers::wrappers::ShifterValue, + //pub buttons_shifter: adv_shift_registers::wrappers::ShifterValue, + // pub lcd: adv_shift_registers::wrappers::ShifterValue, // usb pins pub usb_dp: AnyPin<'static>, @@ -61,42 +74,75 @@ impl Board { let flash = peripherals.FLASH; let sw_interrupt = peripherals.SW_INTERRUPT; - let sck = peripherals.GPIO4.degrade(); - let miso = peripherals.GPIO5.degrade(); - let mosi = peripherals.GPIO6.degrade(); - let battery = peripherals.GPIO2; + //let sck = peripherals.GPIO4.degrade(); + //let miso = peripherals.GPIO5.degrade(); + //let mosi = peripherals.GPIO6.degrade(); + //let battery = peripherals.GPIO2; let stackmat_rx = peripherals.GPIO20.degrade(); let usb_dp = peripherals.GPIO19.degrade(); let usb_dm = peripherals.GPIO18.degrade(); - let button_input = Input::new( - peripherals.GPIO3, + let b1 = Input::new( + peripherals.GPIO0, InputConfig::default().with_pull(Pull::Down), ); - let shifter_data_pin = Output::new(peripherals.GPIO10, Level::Low, Default::default()); - let shifter_latch_pin = Output::new(peripherals.GPIO1, Level::Low, Default::default()); - let shifter_clk_pin = Output::new(peripherals.GPIO21, Level::Low, Default::default()); - - let adv_shift_reg = adv_shift_registers::AdvancedShiftRegister::<8, _>::new( - shifter_data_pin, - shifter_clk_pin, - shifter_latch_pin, - 0, + let b2 = Input::new( + peripherals.GPIO1, + InputConfig::default().with_pull(Pull::Down), ); - let adv_shift_reg = alloc::boxed::Box::new(adv_shift_reg); - let adv_shift_reg = alloc::boxed::Box::leak(adv_shift_reg); - let mut backlight = adv_shift_reg.get_pin_mut(1, 1, false); - _ = backlight.set_high(); + let b3 = Input::new( + peripherals.GPIO2, + InputConfig::default().with_pull(Pull::Down), + ); - let buttons_shifter = adv_shift_reg.get_shifter_mut(0); - let lcd = adv_shift_reg.get_shifter_mut(1); - let digits_shifters = adv_shift_reg.get_shifter_range_mut(2..8); - digits_shifters.set_data(&[!DEC_DIGITS[8] ^ DOT_MOD; 6]); + let b4 = Input::new( + peripherals.GPIO3, + InputConfig::default().with_pull(Pull::Down), + ); - let mut cs = adv_shift_reg.get_pin_mut(1, 0, true); - _ = cs.set_high(); + let Ok(i2c) = I2c::new( + peripherals.I2C0, + esp_hal::i2c::master::Config::default().with_frequency(Rate::from_khz(400)), + ) else { + log::error!("Rfid task error while creating Spi instance!"); + panic!() + }; + let i2c = i2c + .with_sda(peripherals.GPIO8) + .with_scl(peripherals.GPIO9) + .into_async(); + let i2c: Rc> = Rc::new(Mutex::new(i2c)); + + //let button_input = Input::new( + // peripherals.GPIO3, + // InputConfig::default().with_pull(Pull::Down), + //); + + //let shifter_data_pin = Output::new(peripherals.GPIO10, Level::Low, Default::default()); + //let shifter_latch_pin = Output::new(peripherals.GPIO5, Level::Low, Default::default()); + //let shifter_clk_pin = Output::new(peripherals.GPIO21, Level::Low, Default::default()); + + //let adv_shift_reg = adv_shift_registers::AdvancedShiftRegister::<8, _>::new( + // shifter_data_pin, + // shifter_clk_pin, + // shifter_latch_pin, + // 0, + //); + //let adv_shift_reg = alloc::boxed::Box::new(adv_shift_reg); + //let adv_shift_reg = alloc::boxed::Box::leak(adv_shift_reg); + + //let mut backlight = adv_shift_reg.get_pin_mut(1, 1, false); + //_ = backlight.set_high(); + + //let buttons_shifter = adv_shift_reg.get_shifter_mut(0); + //let lcd = adv_shift_reg.get_shifter_mut(1); + //let digits_shifters = adv_shift_reg.get_shifter_range_mut(2..8); + //digits_shifters.set_data(&[!DEC_DIGITS[8] ^ DOT_MOD; 6]); + + //let mut cs = adv_shift_reg.get_pin_mut(1, 0, true); + //_ = cs.set_high(); Board { timg0, @@ -112,18 +158,20 @@ impl Board { flash, sw_interrupt, - miso, - mosi, - sck, - cs, + //miso, + //mosi, + //sck, + //cs, + i2c, - battery, stackmat_rx, - button_input, + buttons: [b1, b2, b3, b4], + // battery, + //button_input, - buttons_shifter, - digits_shifters, - lcd, + //buttons_shifter, + //digits_shifters, + //lcd, usb_dp, usb_dm, } diff --git a/src/buttons.rs b/src/buttons.rs index efb4616..19bc2fb 100644 --- a/src/buttons.rs +++ b/src/buttons.rs @@ -15,8 +15,9 @@ macros::generate_button_handler_enum!(triggered: &ButtonTrigger, hold_time: u64, #[embassy_executor::task] pub async fn buttons_task( state: GlobalState, - button_input: Input<'static>, - button_reg: adv_shift_registers::wrappers::ShifterValue, + button_inputs: [Input<'static>; 4], + //button_input: Input<'static>, + //button_reg: adv_shift_registers::wrappers::ShifterValue, ) { let mut handler = ButtonsHandler::new(Some(wakeup_button())); handler.add_handler(Button::Third, ButtonTrigger::Up, submit_up()); @@ -55,7 +56,8 @@ pub async fn buttons_task( ); handler.add_handler(Button::Second, ButtonTrigger::Up, delegate_hold()); - handler.run(&state, &button_input, &button_reg).await; + //handler.run(&state, &button_input, &button_reg).await; + handler.run(&state, &button_inputs).await; } #[macros::button_handler] diff --git a/src/main.rs b/src/main.rs index d696a05..77b78fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -114,6 +114,7 @@ async fn main(spawner: Spawner) { unsafe { crate::state::SIGN_KEY = sign_key }; } + /* spawner.must_spawn(lcd::lcd_task( board.lcd, global_state.clone(), @@ -126,24 +127,27 @@ async fn main(spawner: Spawner) { board.adc1, global_state.clone(), )); + */ spawner.must_spawn(buttons::buttons_task( global_state.clone(), - board.button_input, - board.buttons_shifter, + board.buttons, + //board.button_input, + //board.buttons_shifter, )); spawner.must_spawn(stackmat::stackmat_task( board.uart1, board.stackmat_rx, - board.digits_shifters, + // board.digits_shifters, global_state.clone(), )); spawner.must_spawn(rfid::rfid_task( - board.miso, - board.mosi, - board.sck, - board.cs, - board.spi2, - board.spi_dma, + board.i2c, + //board.miso, + //board.mosi, + //board.sck, + //board.cs, + //board.spi2, + //board.spi_dma, global_state.clone(), )); diff --git a/src/rfid.rs b/src/rfid.rs index 072e6d9..c6e355d 100644 --- a/src/rfid.rs +++ b/src/rfid.rs @@ -5,6 +5,7 @@ use crate::translations::{TranslationKey, get_translation}; use alloc::string::ToString; use anyhow::{Result, anyhow}; use embassy_time::{Duration, Instant, Timer}; +use esp_hal::i2c::master::I2c; use esp_hal::time::Rate; use esp_hal::{ dma::{DmaRxBuf, DmaTxBuf}, @@ -16,14 +17,19 @@ use esp_hal_mfrc522::consts::UidSize; #[embassy_executor::task] pub async fn rfid_task( - miso: AnyPin<'static>, - mosi: AnyPin<'static>, - sck: AnyPin<'static>, - cs_pin: adv_shift_registers::wrappers::ShifterPin, - spi: esp_hal::peripherals::SPI2<'static>, - dma_chan: esp_hal::peripherals::DMA_CH0<'static>, + i2c: crate::board::MutexI2C, + //miso: AnyPin<'static>, + //mosi: AnyPin<'static>, + //sck: AnyPin<'static>, + //cs_pin: adv_shift_registers::wrappers::ShifterPin, + //spi: esp_hal::peripherals::SPI2<'static>, + //dma_chan: esp_hal::peripherals::DMA_CH0<'static>, global_state: GlobalState, ) { + let driver = esp_hal_mfrc522::drivers::I2CLockDriver::new(i2c, 0x28); + let mut mfrc522 = esp_hal_mfrc522::MFRC522::new(driver); + + /* #[allow(clippy::manual_div_ceil)] let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(512); let Ok(dma_tx_buf) = DmaTxBuf::new(tx_descriptors, tx_buffer) else { @@ -62,6 +68,7 @@ pub async fn rfid_task( esp_hal_mfrc522::MFRC522::new(esp_hal_mfrc522::drivers::SpiDriver::new(spi)) }; + */ #[cfg(not(feature = "e2e"))] loop { diff --git a/src/stackmat.rs b/src/stackmat.rs index e88ba68..baba252 100644 --- a/src/stackmat.rs +++ b/src/stackmat.rs @@ -16,7 +16,7 @@ pub static mut CURRENT_TIME: u64 = 0; pub async fn stackmat_task( uart: UART1<'static>, uart_pin: AnyPin<'static>, - display: ShifterValueRange, + // display: ShifterValueRange, global_state: GlobalState, ) { let serial_config = esp_hal::uart::Config::default().with_baudrate(1200); @@ -50,7 +50,7 @@ pub async fn stackmat_task( && last_state != Some(false) { last_state = Some(false); - display.set_data(&[255; 6]); + // display.set_data(&[255; 6]); let mut state = global_state.state.lock().await; state.stackmat_connected = Some(false); @@ -203,7 +203,7 @@ pub async fn stackmat_task( } let time_str = ms_to_time_str(parsed.1); - display.set_data(&time_str_to_display(&time_str)); + // display.set_data(&time_str_to_display(&time_str)); #[cfg(feature = "qa")] crate::qa::send_qa_resp(crate::qa::QaSignal::Stackmat(parsed.1)); @@ -237,7 +237,7 @@ pub async fn stackmat_task( state.time_confirmed = true; } - display.set_data(&[255; 6]); + // display.set_data(&[255; 6]); } last_stackmat_state = parsed.0; diff --git a/src/utils/buttons.rs b/src/utils/buttons.rs index 7203acd..20bd1c8 100644 --- a/src/utils/buttons.rs +++ b/src/utils/buttons.rs @@ -68,8 +68,9 @@ impl ButtonsHandler { pub async fn run( &mut self, state: &GlobalState, - button_input: &Input<'static>, - button_reg: &adv_shift_registers::wrappers::ShifterValue, + //button_input: &Input<'static>, + button_inputs: &[Input<'static>], + //button_reg: &adv_shift_registers::wrappers::ShifterValue, ) { let mut debounce_time = esp_hal::time::Instant::now(); let mut old_debounced = i32::MAX; @@ -104,6 +105,16 @@ impl ButtonsHandler { } } + { + for i in 0..4 { + if button_inputs[i].is_high() { + // if button_input.is_high() { + out_val |= 1 << i; + } + } + } + + /* { let mut val = 0b10000000; for i in 0..4 { @@ -115,6 +126,7 @@ impl ButtonsHandler { val >>= 1; } } + */ if old_val != out_val { old_val = out_val; From 6e811d1999a68052ea629b88266cf487960221db Mon Sep 17 00:00:00 2001 From: filipton Date: Mon, 9 Mar 2026 19:26:45 +0100 Subject: [PATCH 02/31] wip: shared i2c struct with embedded_hal_async [SKIP CI] --- Cargo.lock | 100 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 9 +++- src/board.rs | 11 +++-- src/rfid.rs | 5 +- src/utils/mod.rs | 1 + src/utils/shared_i2c.rs | 37 +++++++++++++++ 6 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 src/utils/shared_i2c.rs diff --git a/Cargo.lock b/Cargo.lock index 771a9b7..3ccd0ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "base16ct" version = "0.2.0" @@ -413,6 +419,23 @@ dependencies = [ "subtle", ] +[[package]] +name = "display-interface" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba2aab1ef3793e6f7804162debb5ac5edb93b3d650fbcc5aeb72fcd0e6c03a0" + +[[package]] +name = "display-interface-i2c" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d964fa85bbbb5a6ecd06e58699407ac5dc3e3ad72dac0ab7e6b0d00a1cd262d" +dependencies = [ + "display-interface", + "embedded-hal 1.0.0", + "embedded-hal-async", +] + [[package]] name = "dns-protocol" version = "0.1.2" @@ -655,6 +678,48 @@ dependencies = [ "nb 1.1.0", ] +[[package]] +name = "embedded-dma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "embedded-graphics" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8da660bb0c829b34a56a965490597f82a55e767b91f9543be80ce8ccb416fe" +dependencies = [ + "az", + "byteorder", + "embedded-graphics-core", + "float-cmp", + "micromath", +] + +[[package]] +name = "embedded-graphics-core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95743bef3ff70fcba3930246c4e6872882bbea0dcc6da2ca860112e0cd4bd09f" +dependencies = [ + "az", + "byteorder", +] + +[[package]] +name = "embedded-graphics-framebuf" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22354420f68727fa24d1e2741dae1e9a041065e80fb63b35a8d19c647a85be76" +dependencies = [ + "embedded-dma", + "embedded-graphics", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -959,9 +1024,10 @@ dependencies = [ [[package]] name = "esp-hal-mfrc522" -version = "0.3.4" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60cb9b21a888cca9d719cde04541e6f8a0a383097e2986cf2ca99931e6f136d1" dependencies = [ - "embassy-sync 0.7.2", "embassy-time", "embedded-hal-async", "heapless 0.9.2", @@ -1282,6 +1348,8 @@ dependencies = [ "ag-lcd-async", "anyhow", "critical-section", + "display-interface", + "display-interface-i2c", "dotenvy", "dyn-smooth", "embassy-executor", @@ -1289,6 +1357,8 @@ dependencies = [ "embassy-net", "embassy-sync 0.7.2", "embassy-time", + "embedded-graphics", + "embedded-graphics-framebuf", "embedded-hal 1.0.0", "embedded-hal-async", "embedded-hal-bus", @@ -1317,12 +1387,22 @@ dependencies = [ "rand_core 0.6.4", "serde", "serde_json", + "ssd1309", "static_cell", "trouble-host", "uuid", "ws-framer", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1704,6 +1784,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "micromath" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" + [[package]] name = "nb" version = "0.1.3" @@ -2135,6 +2221,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0f368519fc6c85fc1afdb769fb5a51123f6158013e143656e25a3485a0d401c" +[[package]] +name = "ssd1309" +version = "0.4.0" +source = "git+https://github.com/oakamil/ssd1309.git#6aa09574af957714eaedaf55073cc1f06a216304" +dependencies = [ + "display-interface", + "embedded-graphics-core", + "embedded-hal 1.0.0", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index bb38dfd..43ae524 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,11 +45,16 @@ embedded-hal-bus = { version = "0.3.0", features = ["async"] } embedded-tls = { git = "https://github.com/filipton/embedded-tls", default-features = false, features = ["alloc", "embedded-io-adapters", "log"] } rand_core = { version = "0.6.4", features = ["getrandom"] } ag-lcd-async = { git = "https://github.com/filipton/ag-lcd-async", features = [] } -#esp-hal-mfrc522 = { version = "0.3.4", features = ["embassy-time"] } -esp-hal-mfrc522 = { path = "../esp-hal-mfrc522", features = ["alloc", "embassy-time"] } +esp-hal-mfrc522 = { version = "0.3.3", features = ["embassy-time"] } esp-bootloader-esp-idf = { version = "0.4.0", features = ["log-04", "esp32c3"] } trouble-host = { version = "0.5.1", features = ["scan", "security"] } +display-interface = "0.5.0" +display-interface-i2c = "0.5.0" +ssd1309 = {git="https://github.com/oakamil/ssd1309.git"} +embedded-graphics = "0.8.2" +embedded-graphics-framebuf = "0.5.0" + [patch.crates-io] elliptic-curve = { git = "https://github.com/filipton/rust-crypto-traits.git" } diff --git a/src/board.rs b/src/board.rs index e6d0288..48bf13f 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1,4 +1,7 @@ -use crate::utils::stackmat::{DEC_DIGITS, DOT_MOD}; +use crate::utils::{ + shared_i2c::SharedI2C, + stackmat::{DEC_DIGITS, DOT_MOD}, +}; use adv_shift_registers::wrappers::ShifterValueRange; use alloc::rc::Rc; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; @@ -16,8 +19,6 @@ use esp_hal::{ timer::timg::TimerGroup, }; -pub type MutexI2C = Rc>>; - #[allow(dead_code)] pub struct Board { // peripherals @@ -41,7 +42,7 @@ pub struct Board { //pub cs: adv_shift_registers::wrappers::ShifterPin, // i2c - pub i2c: MutexI2C, + pub i2c: SharedI2C, pub stackmat_rx: AnyPin<'static>, @@ -113,7 +114,7 @@ impl Board { .with_sda(peripherals.GPIO8) .with_scl(peripherals.GPIO9) .into_async(); - let i2c: Rc> = Rc::new(Mutex::new(i2c)); + let i2c = SharedI2C::new(i2c); //let button_input = Input::new( // peripherals.GPIO3, diff --git a/src/rfid.rs b/src/rfid.rs index c6e355d..d37fabd 100644 --- a/src/rfid.rs +++ b/src/rfid.rs @@ -2,6 +2,7 @@ use crate::consts::RFID_RETRY_INIT_MS; use crate::state::{GlobalState, MenuScene, current_epoch, sleep_state}; use crate::structs::{CardInfoResponsePacket, SolveConfirmPacket}; use crate::translations::{TranslationKey, get_translation}; +use crate::utils::shared_i2c::SharedI2C; use alloc::string::ToString; use anyhow::{Result, anyhow}; use embassy_time::{Duration, Instant, Timer}; @@ -17,7 +18,7 @@ use esp_hal_mfrc522::consts::UidSize; #[embassy_executor::task] pub async fn rfid_task( - i2c: crate::board::MutexI2C, + i2c: SharedI2C, //miso: AnyPin<'static>, //mosi: AnyPin<'static>, //sck: AnyPin<'static>, @@ -26,7 +27,7 @@ pub async fn rfid_task( //dma_chan: esp_hal::peripherals::DMA_CH0<'static>, global_state: GlobalState, ) { - let driver = esp_hal_mfrc522::drivers::I2CLockDriver::new(i2c, 0x28); + let driver = esp_hal_mfrc522::drivers::I2CDriver::new(i2c, 0x28); let mut mfrc522 = esp_hal_mfrc522::MFRC522::new(driver); /* diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 671ddb1..2231d1a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,6 +4,7 @@ pub mod buttons; pub mod lcd_abstract; pub mod logger; pub mod rolling_average; +pub mod shared_i2c; pub mod signaled_mutex; pub mod stackmat; diff --git a/src/utils/shared_i2c.rs b/src/utils/shared_i2c.rs new file mode 100644 index 0000000..61fdcb3 --- /dev/null +++ b/src/utils/shared_i2c.rs @@ -0,0 +1,37 @@ +use alloc::{rc::Rc, vec::Vec}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; +use esp_hal::{Async, i2c::master::I2c}; + +#[derive(Clone)] +pub struct SharedI2C { + inner: Rc>>, +} + +impl embedded_hal::i2c::ErrorType for SharedI2C { + type Error = esp_hal::i2c::master::Error; +} + +impl embedded_hal_async::i2c::I2c for SharedI2C { + async fn transaction( + &mut self, + address: u8, + operations: &mut [embedded_hal::i2c::Operation<'_>], + ) -> Result<(), Self::Error> { + self.inner.lock().await.transaction( + address, + operations + .iter_mut() + .map(esp_hal::i2c::master::Operation::from) + .collect::>() + .iter_mut(), + ) + } +} + +impl SharedI2C { + pub fn new(i2c: I2c<'static, Async>) -> Self { + SharedI2C { + inner: Rc::new(Mutex::new(i2c)), + } + } +} From 0f904bf8bad3bff1d7ade55c3551a5daae10ed7a Mon Sep 17 00:00:00 2001 From: filipton Date: Mon, 9 Mar 2026 19:26:59 +0100 Subject: [PATCH 03/31] wip: async oled driver [SKIP CI] --- Cargo.lock | 87 +++++++++++++++++++++++++++++++++++++++++++++------- Cargo.toml | 2 +- src/board.rs | 10 ++++-- src/main.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 166 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ccd0ab..37ec1ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,6 +386,47 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + +[[package]] +name = "defmt" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + [[package]] name = "delegate" version = "0.13.5" @@ -1383,11 +1424,11 @@ dependencies = [ "log", "macros", "nb 1.1.0", + "oled_async", "portable-atomic", "rand_core 0.6.4", "serde", "serde_json", - "ssd1309", "static_cell", "trouble-host", "uuid", @@ -1846,6 +1887,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "oled_async" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a1a30da0097d4c0e9d7ba0757d62f2db86df3254f59d1e15c3988eb0bb24c4" +dependencies = [ + "defmt 0.3.100", + "display-interface", + "embedded-graphics-core", + "embedded-hal 1.0.0", +] + [[package]] name = "opaque-debug" version = "0.3.1" @@ -1951,6 +2004,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2221,16 +2296,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0f368519fc6c85fc1afdb769fb5a51123f6158013e143656e25a3485a0d401c" -[[package]] -name = "ssd1309" -version = "0.4.0" -source = "git+https://github.com/oakamil/ssd1309.git#6aa09574af957714eaedaf55073cc1f06a216304" -dependencies = [ - "display-interface", - "embedded-graphics-core", - "embedded-hal 1.0.0", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index 43ae524..243c9f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,9 +51,9 @@ trouble-host = { version = "0.5.1", features = ["scan", "security"] } display-interface = "0.5.0" display-interface-i2c = "0.5.0" -ssd1309 = {git="https://github.com/oakamil/ssd1309.git"} embedded-graphics = "0.8.2" embedded-graphics-framebuf = "0.5.0" +oled_async = { version = "0.2.0", features = ["i2c"] } [patch.crates-io] elliptic-curve = { git = "https://github.com/filipton/rust-crypto-traits.git" } diff --git a/src/board.rs b/src/board.rs index 48bf13f..abca3c9 100644 --- a/src/board.rs +++ b/src/board.rs @@ -40,8 +40,7 @@ pub struct Board { //pub mosi: AnyPin<'static>, //pub sck: AnyPin<'static>, //pub cs: adv_shift_registers::wrappers::ShifterPin, - - // i2c + pub display_rst: Output<'static>, pub i2c: SharedI2C, pub stackmat_rx: AnyPin<'static>, @@ -83,6 +82,12 @@ impl Board { let usb_dp = peripherals.GPIO19.degrade(); let usb_dm = peripherals.GPIO18.degrade(); + let display_rst = Output::new( + peripherals.GPIO7, + esp_hal::gpio::Level::Low, + Default::default(), + ); + let b1 = Input::new( peripherals.GPIO0, InputConfig::default().with_pull(Pull::Down), @@ -165,6 +170,7 @@ impl Board { //cs, i2c, + display_rst, stackmat_rx, buttons: [b1, b2, b3, b4], // battery, diff --git a/src/main.rs b/src/main.rs index 77b78fe..b7747e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use board::Board; use consts::LOG_SEND_INTERVAL_MS; use embassy_executor::Spawner; use embassy_sync::signal::Signal; -use embassy_time::{Instant, Timer}; +use embassy_time::{Delay, Instant, Timer}; use esp_backtrace as _; use esp_hal::interrupt::software::SoftwareInterruptControl; use esp_hal_wifimanager::{Nvs, WIFI_NVS_KEY}; @@ -114,6 +114,85 @@ async fn main(spawner: Spawner) { unsafe { crate::state::SIGN_KEY = sign_key }; } + let di = display_interface_i2c::I2CInterface::new(board.i2c.clone(), 0x3C, 0x40); + let raw_disp = + oled_async::builder::Builder::new(oled_async::displays::ssd1309::Ssd1309_128_64 {}) + .with_rotation(oled_async::prelude::DisplayRotation::Rotate180) + .connect(di); + + let mut disp: oled_async::mode::GraphicsMode<_, _> = raw_disp.into(); + let mut display_rst = board.display_rst; + disp.reset(&mut display_rst, &mut Delay); + + disp.init().await.unwrap(); + disp.clear(); + disp.flush().await.unwrap(); + + let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() + .font(&embedded_graphics::mono_font::ascii::FONT_6X10) + .text_color(embedded_graphics::pixelcolor::BinaryColor::On) + .build(); + + embedded_graphics::Drawable::draw( + &embedded_graphics::text::Text::with_baseline( + "test 123", + embedded_graphics::prelude::Point::zero(), + text_style, + embedded_graphics::text::Baseline::Top, + ), + &mut disp, + ) + .unwrap(); + + disp.flush().await.unwrap(); + + let mut data = [embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 64) as usize]; + let mut fbuf = embedded_graphics_framebuf::FrameBuf::new(&mut data, 128, 64); + + let start = Instant::now(); + loop { + embedded_graphics::prelude::DrawTarget::clear( + &mut fbuf, + embedded_graphics::pixelcolor::BinaryColor::Off, + ); + Timer::after(embassy_time::Duration::from_millis(1000 / 60)).await; + + let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() + .font(&embedded_graphics::mono_font::ascii::FONT_6X10) + .text_color(embedded_graphics::pixelcolor::BinaryColor::On) + .build(); + + let time_str = utils::stackmat::ms_to_time_str(start.elapsed().as_millis()); + embedded_graphics::Drawable::draw( + &embedded_graphics::text::Text::with_baseline( + &alloc::format!("Hello world! {time_str}"), + embedded_graphics::prelude::Point::zero(), + text_style, + embedded_graphics::text::Baseline::Top, + ), + &mut fbuf, + ) + .unwrap(); + + embedded_graphics::Drawable::draw( + &embedded_graphics::text::Text::with_baseline( + "Hello Rust!", + embedded_graphics::prelude::Point::new(0, 16), + text_style, + embedded_graphics::text::Baseline::Top, + ), + &mut fbuf, + ) + .unwrap(); + + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); + + if start.elapsed().as_secs() > 120 { + break; + } + } + /* spawner.must_spawn(lcd::lcd_task( board.lcd, @@ -141,7 +220,7 @@ async fn main(spawner: Spawner) { global_state.clone(), )); spawner.must_spawn(rfid::rfid_task( - board.i2c, + board.i2c.clone(), //board.miso, //board.mosi, //board.sck, From c10ffe79ca0c0994d03bee14600afa358ad1887a Mon Sep 17 00:00:00 2001 From: filipton Date: Mon, 9 Mar 2026 19:31:42 +0100 Subject: [PATCH 04/31] wip: lcd in own task --- src/lcd.rs | 306 +++++++++++++++++++++++++++++++++------------------- src/main.rs | 87 +-------------- 2 files changed, 200 insertions(+), 193 deletions(-) diff --git a/src/lcd.rs b/src/lcd.rs index d742682..5c087a0 100644 --- a/src/lcd.rs +++ b/src/lcd.rs @@ -5,6 +5,7 @@ use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; use embassy_time::{Delay, Duration, Instant, Timer}; use embedded_hal::digital::OutputPin; use embedded_hal_async::delay::DelayNs; +use esp_hal::gpio::Output; use crate::{ consts::{ @@ -17,143 +18,226 @@ use crate::{ translations::{TranslationKey, get_translation, get_translation_params}, utils::{ lcd_abstract::{LcdAbstract, PrintAlign}, + shared_i2c::SharedI2C, stackmat::ms_to_time_str, }, }; #[embassy_executor::task] pub async fn lcd_task( - lcd_shifter: adv_shift_registers::wrappers::ShifterValue, + i2c: SharedI2C, + mut display_rst: Output<'static>, + //lcd_shifter: adv_shift_registers::wrappers::ShifterValue, global_state: GlobalState, wifi_setup_sig: Rc>, - display: ShifterValueRange, + //display: ShifterValueRange, ) { - let mut lcd = { - let bl_pin = lcd_shifter.get_pin_mut(1, true); - let rs_pin = lcd_shifter.get_pin_mut(2, true); - let en_pin = lcd_shifter.get_pin_mut(3, true); - let d4_pin = lcd_shifter.get_pin_mut(4, false); - let d5_pin = lcd_shifter.get_pin_mut(5, false); - let d6_pin = lcd_shifter.get_pin_mut(6, false); - let d7_pin = lcd_shifter.get_pin_mut(7, false); - LcdDisplay::new(rs_pin, en_pin, Delay) - .with_display(ag_lcd_async::Display::On) - .with_blink(ag_lcd_async::Blink::Off) - .with_cursor(ag_lcd_async::Cursor::Off) - .with_size(ag_lcd_async::Size::Dots5x8) - .with_cols(16) - .with_lines(ag_lcd_async::Lines::TwoLines) - .with_half_bus(d4_pin, d5_pin, d6_pin, d7_pin) - .with_backlight(bl_pin) - .build() - .await - }; - - lcd.clear().await; - lcd.backlight_on(); - - let mut lcd_driver: LcdAbstract<80, 16, 2, 3> = LcdAbstract::new(); - - _ = lcd_driver.print( - 0, - &alloc::format!("{:X}", crate::utils::get_efuse_u32()), - PrintAlign::Left, - true, - ); - _ = lcd_driver.print(1, crate::version::VERSION, PrintAlign::Center, true); - lcd_driver.display_on_lcd(&mut lcd).await; - - _ = lcd_driver.print( - 0, - &alloc::format!("{}%", global_state.show_battery.wait().await), - PrintAlign::Right, - false, - ); - lcd_driver.display_on_lcd(&mut lcd).await; - - #[cfg(not(feature = "bat_dev_lcd"))] - Timer::after_millis(2500).await; - - _ = lcd_driver.clear_all(); - let mut last_update; + let di = display_interface_i2c::I2CInterface::new(i2c, 0x3C, 0x40); + let raw_disp = + oled_async::builder::Builder::new(oled_async::displays::ssd1309::Ssd1309_128_64 {}) + .with_rotation(oled_async::prelude::DisplayRotation::Rotate180) + .connect(di); + + let mut disp: oled_async::mode::GraphicsMode<_, _> = raw_disp.into(); + disp.reset(&mut display_rst, &mut Delay); + + disp.init().await.unwrap(); + disp.clear(); + disp.flush().await.unwrap(); + + let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() + .font(&embedded_graphics::mono_font::ascii::FONT_6X10) + .text_color(embedded_graphics::pixelcolor::BinaryColor::On) + .build(); + + embedded_graphics::Drawable::draw( + &embedded_graphics::text::Text::with_baseline( + "test 123", + embedded_graphics::prelude::Point::zero(), + text_style, + embedded_graphics::text::Baseline::Top, + ), + &mut disp, + ) + .unwrap(); + + disp.flush().await.unwrap(); + + let mut data = [embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 64) as usize]; + let mut fbuf = embedded_graphics_framebuf::FrameBuf::new(&mut data, 128, 64); + + let start = Instant::now(); loop { - let current_state = global_state.state.value().await.clone(); - log::debug!("lcd current_state: {current_state:?}"); - last_update = Instant::now(); + embedded_graphics::prelude::DrawTarget::clear( + &mut fbuf, + embedded_graphics::pixelcolor::BinaryColor::Off, + ); + Timer::after(embassy_time::Duration::from_millis(1000 / 60)).await; + + let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() + .font(&embedded_graphics::mono_font::ascii::FONT_6X10) + .text_color(embedded_graphics::pixelcolor::BinaryColor::On) + .build(); + + let time_str = crate::utils::stackmat::ms_to_time_str(start.elapsed().as_millis()); + embedded_graphics::Drawable::draw( + &embedded_graphics::text::Text::with_baseline( + &alloc::format!("Hello world! {time_str}"), + embedded_graphics::prelude::Point::zero(), + text_style, + embedded_graphics::text::Baseline::Top, + ), + &mut fbuf, + ) + .unwrap(); + + embedded_graphics::Drawable::draw( + &embedded_graphics::text::Text::with_baseline( + "Hello Rust!", + embedded_graphics::prelude::Point::new(0, 16), + text_style, + embedded_graphics::text::Baseline::Top, + ), + &mut fbuf, + ) + .unwrap(); + + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); + + if start.elapsed().as_secs() > 120 { + break; + } + } - if sleep_state() { - lcd.backlight_on(); + /* + let mut lcd = { + let bl_pin = lcd_shifter.get_pin_mut(1, true); + let rs_pin = lcd_shifter.get_pin_mut(2, true); + let en_pin = lcd_shifter.get_pin_mut(3, true); + let d4_pin = lcd_shifter.get_pin_mut(4, false); + let d5_pin = lcd_shifter.get_pin_mut(5, false); + let d6_pin = lcd_shifter.get_pin_mut(6, false); + let d7_pin = lcd_shifter.get_pin_mut(7, false); + LcdDisplay::new(rs_pin, en_pin, Delay) + .with_display(ag_lcd_async::Display::On) + .with_blink(ag_lcd_async::Blink::Off) + .with_cursor(ag_lcd_async::Cursor::Off) + .with_size(ag_lcd_async::Size::Dots5x8) + .with_cols(16) + .with_lines(ag_lcd_async::Lines::TwoLines) + .with_half_bus(d4_pin, d5_pin, d6_pin, d7_pin) + .with_backlight(bl_pin) + .build() + .await + }; - unsafe { - crate::state::SLEEP_STATE = false; - } - } + lcd.clear().await; + lcd.backlight_on(); - let current_scene = current_state.scene.clone(); - let fut = async { - let _ = process_lcd( - current_state, - &global_state, - &mut lcd_driver, - &mut lcd, - &wifi_setup_sig, - &display, - ) - .await; - lcd_driver.display_on_lcd(&mut lcd).await; + let mut lcd_driver: LcdAbstract<80, 16, 2, 3> = LcdAbstract::new(); - let mut scroll_ticker = - embassy_time::Ticker::every(Duration::from_millis(SCROLL_TICKER_INVERVAL_MS)); - loop { - scroll_ticker.next().await; - let changed = lcd_driver.scroll_step(); - if changed.is_ok_and(|c| c) { - lcd_driver.display_on_lcd(&mut lcd).await; + _ = lcd_driver.print( + 0, + &alloc::format!("{:X}", crate::utils::get_efuse_u32()), + PrintAlign::Left, + true, + ); + _ = lcd_driver.print(1, crate::version::VERSION, PrintAlign::Center, true); + lcd_driver.display_on_lcd(&mut lcd).await; + + _ = lcd_driver.print( + 0, + &alloc::format!("{}%", global_state.show_battery.wait().await), + PrintAlign::Right, + false, + ); + lcd_driver.display_on_lcd(&mut lcd).await; + + #[cfg(not(feature = "bat_dev_lcd"))] + Timer::after_millis(2500).await; + + _ = lcd_driver.clear_all(); + let mut last_update; + loop { + let current_state = global_state.state.value().await.clone(); + log::debug!("lcd current_state: {current_state:?}"); + last_update = Instant::now(); + + if sleep_state() { + lcd.backlight_on(); + + unsafe { + crate::state::SLEEP_STATE = false; } + } - #[cfg(not(any(feature = "e2e", feature = "qa")))] - if !sleep_state() - && (Instant::now() - last_update).as_millis() > SLEEP_AFTER_MS - && current_scene.can_sleep() - { - _ = lcd_driver.print(0, "Sleep", PrintAlign::Center, true); - _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); - lcd_driver.display_on_lcd(&mut lcd).await; - lcd.backlight_off(); + let current_scene = current_state.scene.clone(); + let fut = async { + let _ = process_lcd( + current_state, + &global_state, + &mut lcd_driver, + &mut lcd, + &wifi_setup_sig, + &display, + ) + .await; + lcd_driver.display_on_lcd(&mut lcd).await; + + let mut scroll_ticker = + embassy_time::Ticker::every(Duration::from_millis(SCROLL_TICKER_INVERVAL_MS)); + loop { + scroll_ticker.next().await; + let changed = lcd_driver.scroll_step(); + if changed.is_ok_and(|c| c) { + lcd_driver.display_on_lcd(&mut lcd).await; + } + #[cfg(not(any(feature = "e2e", feature = "qa")))] + if !sleep_state() + && (Instant::now() - last_update).as_millis() > SLEEP_AFTER_MS + && current_scene.can_sleep() { - global_state.state.lock().await.server_connected = Some(false); - } + _ = lcd_driver.print(0, "Sleep", PrintAlign::Center, true); + _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); + lcd_driver.display_on_lcd(&mut lcd).await; + lcd.backlight_off(); + + { + global_state.state.lock().await.server_connected = Some(false); + } - unsafe { - crate::state::SLEEP_STATE = true; - crate::state::TRUST_SERVER = false; + unsafe { + crate::state::SLEEP_STATE = true; + crate::state::TRUST_SERVER = false; + } + + global_state.state.signal_reset(); } - global_state.state.signal_reset(); + #[cfg(not(any(feature = "e2e", feature = "qa")))] + if sleep_state() + && !deeper_sleep_state() + && (Instant::now() - last_update).as_millis() > DEEPER_SLEEP_AFTER_MS + { + _ = lcd_driver.print(0, "Deep Sleep", PrintAlign::Center, true); + _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); + lcd_driver.display_on_lcd(&mut lcd).await; + crate::utils::deeper_sleep(); + } } + }; - #[cfg(not(any(feature = "e2e", feature = "qa")))] - if sleep_state() - && !deeper_sleep_state() - && (Instant::now() - last_update).as_millis() > DEEPER_SLEEP_AFTER_MS - { - _ = lcd_driver.print(0, "Deep Sleep", PrintAlign::Center, true); - _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); - lcd_driver.display_on_lcd(&mut lcd).await; - crate::utils::deeper_sleep(); + let res = embassy_futures::select::select(fut, global_state.state.wait()).await; + match res { + embassy_futures::select::Either::First(_) => {} + embassy_futures::select::Either::Second(_) => { + continue; } } - }; - - let res = embassy_futures::select::select(fut, global_state.state.wait()).await; - match res { - embassy_futures::select::Either::First(_) => {} - embassy_futures::select::Either::Second(_) => { - continue; - } } - } + */ } async fn process_lcd( diff --git a/src/main.rs b/src/main.rs index b7747e6..2a2b000 100644 --- a/src/main.rs +++ b/src/main.rs @@ -114,93 +114,16 @@ async fn main(spawner: Spawner) { unsafe { crate::state::SIGN_KEY = sign_key }; } - let di = display_interface_i2c::I2CInterface::new(board.i2c.clone(), 0x3C, 0x40); - let raw_disp = - oled_async::builder::Builder::new(oled_async::displays::ssd1309::Ssd1309_128_64 {}) - .with_rotation(oled_async::prelude::DisplayRotation::Rotate180) - .connect(di); - - let mut disp: oled_async::mode::GraphicsMode<_, _> = raw_disp.into(); - let mut display_rst = board.display_rst; - disp.reset(&mut display_rst, &mut Delay); - - disp.init().await.unwrap(); - disp.clear(); - disp.flush().await.unwrap(); - - let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() - .font(&embedded_graphics::mono_font::ascii::FONT_6X10) - .text_color(embedded_graphics::pixelcolor::BinaryColor::On) - .build(); - - embedded_graphics::Drawable::draw( - &embedded_graphics::text::Text::with_baseline( - "test 123", - embedded_graphics::prelude::Point::zero(), - text_style, - embedded_graphics::text::Baseline::Top, - ), - &mut disp, - ) - .unwrap(); - - disp.flush().await.unwrap(); - - let mut data = [embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 64) as usize]; - let mut fbuf = embedded_graphics_framebuf::FrameBuf::new(&mut data, 128, 64); - - let start = Instant::now(); - loop { - embedded_graphics::prelude::DrawTarget::clear( - &mut fbuf, - embedded_graphics::pixelcolor::BinaryColor::Off, - ); - Timer::after(embassy_time::Duration::from_millis(1000 / 60)).await; - - let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() - .font(&embedded_graphics::mono_font::ascii::FONT_6X10) - .text_color(embedded_graphics::pixelcolor::BinaryColor::On) - .build(); - - let time_str = utils::stackmat::ms_to_time_str(start.elapsed().as_millis()); - embedded_graphics::Drawable::draw( - &embedded_graphics::text::Text::with_baseline( - &alloc::format!("Hello world! {time_str}"), - embedded_graphics::prelude::Point::zero(), - text_style, - embedded_graphics::text::Baseline::Top, - ), - &mut fbuf, - ) - .unwrap(); - - embedded_graphics::Drawable::draw( - &embedded_graphics::text::Text::with_baseline( - "Hello Rust!", - embedded_graphics::prelude::Point::new(0, 16), - text_style, - embedded_graphics::text::Baseline::Top, - ), - &mut fbuf, - ) - .unwrap(); - - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); - - if start.elapsed().as_secs() > 120 { - break; - } - } - - /* spawner.must_spawn(lcd::lcd_task( - board.lcd, + board.i2c.clone(), + board.display_rst, + //board.lcd, global_state.clone(), wifi_setup_sig.clone(), - board.digits_shifters.clone(), + //board.digits_shifters.clone(), )); + /* spawner.must_spawn(battery::battery_read_task( board.battery, board.adc1, From 9aee3a6edc5fea385054d8417dce043e08582f85 Mon Sep 17 00:00:00 2001 From: filipton Date: Mon, 9 Mar 2026 19:43:46 +0100 Subject: [PATCH 05/31] wip: bq27441 gauage module --- Cargo.lock | 314 ++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/battery.rs | 22 +++- src/lcd.rs | 4 - src/main.rs | 7 +- 5 files changed, 330 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37ec1ff..8c3de96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,18 @@ dependencies = [ "embedded-hal-async", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "allocator-api2" version = "0.3.1" @@ -68,6 +80,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "as-slice" version = "0.1.5" @@ -80,6 +98,43 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "askama" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +dependencies = [ + "askama_parser", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "syn 2.0.117", +] + +[[package]] +name = "askama_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +dependencies = [ + "memchr", + "winnow", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -136,6 +191,18 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -145,6 +212,22 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "bq27441" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "401a2308f1e83ceaaa9345f409759ac6e891259a4d909da8fb7332321ae428a4" +dependencies = [ + "defmt 0.3.100", + "device-driver", + "device-driver-generation", + "embedded-hal 1.0.0", + "embedded-hal-async", + "prettyplease", + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "bt-hci" version = "0.6.0" @@ -219,6 +302,15 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "convert_case" version = "0.8.0" @@ -386,6 +478,17 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "dd-manifest-tree" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5793572036e0a6638977c7370c6afc423eac848ee8495f079b8fd3964de7b9f9" +dependencies = [ + "serde_json", + "toml", + "yaml-rust2", +] + [[package]] name = "defmt" version = "0.3.100" @@ -448,6 +551,47 @@ dependencies = [ "zeroize", ] +[[package]] +name = "device-driver" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa3d97b2acf349b9d52c75470e2ccfc7224c49597ec12c2fb0e28826e910495" +dependencies = [ + "defmt 0.3.100", + "device-driver-macros", + "embedded-io 0.6.1", + "embedded-io-async 0.6.1", +] + +[[package]] +name = "device-driver-generation" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d23ac9a75e0bba43928e1a50c7b1beaaa67a98b64eaa28f353a16de1a5fd985" +dependencies = [ + "anyhow", + "askama", + "bitvec", + "convert_case 0.6.0", + "dd-manifest-tree", + "itertools 0.14.0", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "device-driver-macros" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec62c8588239d60d7470326cd78ba2b264bef24f0afa20c666668487c843d03" +dependencies = [ + "device-driver-generation", + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "digest" version = "0.10.7" @@ -802,6 +946,9 @@ name = "embedded-io" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +dependencies = [ + "defmt 0.3.100", +] [[package]] name = "embedded-io" @@ -895,6 +1042,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enumset" version = "1.1.10" @@ -1388,6 +1544,7 @@ dependencies = [ "adv-shift-registers", "ag-lcd-async", "anyhow", + "bq27441", "critical-section", "display-interface", "display-interface-i2c", @@ -1459,6 +1616,12 @@ dependencies = [ "gcd", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.32" @@ -1614,12 +1777,30 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heapless" version = "0.6.1" @@ -1695,7 +1876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -1738,6 +1919,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -1805,7 +1995,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" name = "macros" version = "0.1.0" dependencies = [ - "convert_case", + "convert_case 0.8.0", "proc-macro2", "quote", "serde", @@ -1899,6 +2089,12 @@ dependencies = [ "embedded-hal 1.0.0", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -1923,6 +2119,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1986,6 +2188,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -2001,7 +2213,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -2044,6 +2256,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -2160,6 +2378,12 @@ dependencies = [ "svgbobdoc", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustversion" version = "1.0.22" @@ -2227,6 +2451,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "indexmap", "itoa", "memchr", "serde", @@ -2234,6 +2459,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -2379,6 +2613,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "termcolor" version = "1.4.1" @@ -2408,6 +2648,28 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -2417,6 +2679,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_edit" version = "0.23.10+spec-1.0.0" @@ -2424,7 +2700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow", ] @@ -2438,6 +2714,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "trouble-host" version = "0.5.1" @@ -2468,7 +2750,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcb85bec3a8393c22ca1a7c25c82c2d33689ab412f3487c492fd01a033ede7c2" dependencies = [ - "convert_case", + "convert_case 0.8.0", "darling 0.20.11", "proc-macro2", "quote", @@ -2606,12 +2888,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67fe343b024b086505b3c647de6eae0c26235934cb9138a180e3ac5f97a6aeda" dependencies = [ - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xtensa-lx" version = "0.13.0" @@ -2643,6 +2934,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "yaml-rust2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1a1c0bc9823338a3bdf8c61f994f23ac004c6fa32c08cd152984499b445e8d" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "zerocopy" version = "0.8.39" diff --git a/Cargo.toml b/Cargo.toml index 243c9f8..54955c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ display-interface-i2c = "0.5.0" embedded-graphics = "0.8.2" embedded-graphics-framebuf = "0.5.0" oled_async = { version = "0.2.0", features = ["i2c"] } +bq27441 = { version = "0.1.0", features = ["async", "embassy"] } [patch.crates-io] elliptic-curve = { git = "https://github.com/filipton/rust-crypto-traits.git" } diff --git a/src/battery.rs b/src/battery.rs index 93523c5..831353d 100644 --- a/src/battery.rs +++ b/src/battery.rs @@ -1,5 +1,7 @@ use crate::{ - consts::BATTERY_SEND_INTERVAL_MS, state::sleep_state, utils::rolling_average::RollingAverage, + consts::BATTERY_SEND_INTERVAL_MS, + state::sleep_state, + utils::{rolling_average::RollingAverage, shared_i2c::SharedI2C}, }; use embassy_time::{Duration, Instant, Timer}; use esp_hal::analog::adc::{Adc, AdcConfig, Attenuation}; @@ -24,22 +26,27 @@ const BAT_MAX: f64 = BATTERY_CURVE[BATTERY_CURVE.len() - 1].0; #[embassy_executor::task] pub async fn battery_read_task( - adc_pin: esp_hal::peripherals::GPIO2<'static>, - adc: esp_hal::peripherals::ADC1<'static>, + i2c: SharedI2C, + //adc_pin: esp_hal::peripherals::GPIO2<'static>, + //adc: esp_hal::peripherals::ADC1<'static>, state: crate::state::GlobalState, ) { + /* let mut adc_config = AdcConfig::new(); let mut adc_pin = adc_config.enable_pin_with_cal::<_, AdcCal>(adc_pin, Attenuation::_11dB); let mut adc = Adc::new(adc, adc_config).into_async(); - let mut battery_start = Instant::now().saturating_add(Duration::from_millis(300)); let base_freq = 2.0; let sample_freq = 1000.0; let sensitivity = 0.5; let mut smoother = dyn_smooth::DynamicSmootherEcoF32::new(base_freq, sample_freq, sensitivity); let mut avg = RollingAverage::<128>::new(); + */ + let mut gauge = bq27441::Bq27441Async::new(i2c).await.unwrap(); + + let mut battery_start = Instant::now().saturating_add(Duration::from_millis(300)); let mut lcd_sent = false; let mut sample_rate_millis = 10; @@ -50,9 +57,12 @@ pub async fn battery_read_task( continue; } + /* let read = adc.read_oneshot(&mut adc_pin).await; let read = smoother.tick(read as f32); avg.push(read); + */ + let read = 0; #[cfg(feature = "bat_dev_lcd")] { @@ -62,6 +72,10 @@ pub async fn battery_read_task( let now = Instant::now(); if !lcd_sent && battery_start <= now { + let voltage = gauge.voltage().await.unwrap(); + let soc = gauge.state_of_charge().await.unwrap(); + log::info!("{voltage} {soc}%"); + state .show_battery .signal(bat_percentage(calculate(read as f64))); diff --git a/src/lcd.rs b/src/lcd.rs index 5c087a0..cbcc85b 100644 --- a/src/lcd.rs +++ b/src/lcd.rs @@ -104,10 +104,6 @@ pub async fn lcd_task( embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); disp.flush().await.unwrap(); - - if start.elapsed().as_secs() > 120 { - break; - } } /* diff --git a/src/main.rs b/src/main.rs index 2a2b000..67446e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -123,13 +123,12 @@ async fn main(spawner: Spawner) { //board.digits_shifters.clone(), )); - /* spawner.must_spawn(battery::battery_read_task( - board.battery, - board.adc1, + board.i2c.clone(), + //board.battery, + //board.adc1, global_state.clone(), )); - */ spawner.must_spawn(buttons::buttons_task( global_state.clone(), board.buttons, From 71218d7e1f60cf134cd7ef9edc4bf009191b4bf1 Mon Sep 17 00:00:00 2001 From: filipton Date: Mon, 9 Mar 2026 21:16:08 +0100 Subject: [PATCH 06/31] wip: ported everything (messy as fuck, everything is to change) --- src/battery.rs | 52 +++++----- src/lcd.rs | 204 +++++++++++++++++++++++++++++++------- src/utils/lcd_abstract.rs | 36 +++++++ 3 files changed, 232 insertions(+), 60 deletions(-) diff --git a/src/battery.rs b/src/battery.rs index 831353d..550ecf6 100644 --- a/src/battery.rs +++ b/src/battery.rs @@ -44,14 +44,14 @@ pub async fn battery_read_task( let mut smoother = dyn_smooth::DynamicSmootherEcoF32::new(base_freq, sample_freq, sensitivity); let mut avg = RollingAverage::<128>::new(); */ - let mut gauge = bq27441::Bq27441Async::new(i2c).await.unwrap(); + let Ok(mut gauge) = bq27441::Bq27441Async::new(i2c).await else { + state.show_battery.signal(0); + log::error!("BQ27441 init failed!"); + return; + }; - let mut battery_start = Instant::now().saturating_add(Duration::from_millis(300)); let mut lcd_sent = false; - - let mut sample_rate_millis = 10; loop { - Timer::after_millis(sample_rate_millis).await; if sleep_state() { Timer::after_millis(500).await; continue; @@ -62,53 +62,55 @@ pub async fn battery_read_task( let read = smoother.tick(read as f32); avg.push(read); */ - let read = 0; + /* #[cfg(feature = "bat_dev_lcd")] { let mut state = state.state.lock().await; state.current_bat_read = Some(read); } + */ - let now = Instant::now(); - if !lcd_sent && battery_start <= now { - let voltage = gauge.voltage().await.unwrap(); - let soc = gauge.state_of_charge().await.unwrap(); - log::info!("{voltage} {soc}%"); - - state - .show_battery - .signal(bat_percentage(calculate(read as f64))); + if !lcd_sent { + let mut soc = gauge.state_of_charge().await.unwrap_or(0) as u8; + if soc == 0 { + let volt = gauge.voltage().await.unwrap_or(0) as f64; + soc = bat_percentage(calculate(volt)); + } + state.show_battery.signal(soc); lcd_sent = true; - sample_rate_millis = 100; - } - - if battery_start > now || (now - battery_start).as_millis() < BATTERY_SEND_INTERVAL_MS { + Timer::after_millis(BATTERY_SEND_INTERVAL_MS).await; continue; } - battery_start = Instant::now(); - let bat_calc_mv = calculate(read as f64); - let bat_percentage = bat_percentage(bat_calc_mv); + let mut soc = gauge.state_of_charge().await.unwrap_or(0) as u8; + let mv = gauge.voltage().await.unwrap_or(0) as f64; + if soc == 0 { + soc = bat_percentage(calculate(mv)); + } + state.show_battery.signal(soc); if state.state.lock().await.server_connected == Some(true) { crate::ws::send_packet(crate::structs::TimerPacket { tag: None, data: crate::structs::TimerPacketInner::Battery { - level: Some(bat_percentage as f64), - voltage: Some(bat_calc_mv / 1000.0), + level: Some(soc as f64), + voltage: Some(mv), }, }) .await; } - log::info!("calc({read}): {bat_calc_mv}mV {bat_percentage}%"); + log::info!("Battery {mv}mv {soc}%"); + Timer::after_millis(BATTERY_SEND_INTERVAL_MS).await; + /* #[cfg(feature = "bat_dev_lcd")] { let mut state = state.state.lock().await; state.avg_bat_read = avg.average(); } + */ } } diff --git a/src/lcd.rs b/src/lcd.rs index cbcc85b..2dfca7d 100644 --- a/src/lcd.rs +++ b/src/lcd.rs @@ -1,11 +1,20 @@ use adv_shift_registers::wrappers::ShifterValueRange; use ag_lcd_async::LcdDisplay; use alloc::{rc::Rc, string::ToString}; +use display_interface_i2c::I2CInterface; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; use embassy_time::{Delay, Duration, Instant, Timer}; +use embedded_graphics::{ + Drawable, + pixelcolor::BinaryColor, + prelude::{DrawTarget, Point}, + text::{Alignment, Text}, +}; +use embedded_graphics_framebuf::FrameBuf; use embedded_hal::digital::OutputPin; use embedded_hal_async::delay::DelayNs; use esp_hal::gpio::Output; +use oled_async::{displays::ssd1309::Ssd1309_128_64, mode::GraphicsMode}; use crate::{ consts::{ @@ -45,27 +54,10 @@ pub async fn lcd_task( disp.clear(); disp.flush().await.unwrap(); - let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() - .font(&embedded_graphics::mono_font::ascii::FONT_6X10) - .text_color(embedded_graphics::pixelcolor::BinaryColor::On) - .build(); - - embedded_graphics::Drawable::draw( - &embedded_graphics::text::Text::with_baseline( - "test 123", - embedded_graphics::prelude::Point::zero(), - text_style, - embedded_graphics::text::Baseline::Top, - ), - &mut disp, - ) - .unwrap(); - - disp.flush().await.unwrap(); - let mut data = [embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 64) as usize]; let mut fbuf = embedded_graphics_framebuf::FrameBuf::new(&mut data, 128, 64); + /* let start = Instant::now(); loop { embedded_graphics::prelude::DrawTarget::clear( @@ -91,20 +83,145 @@ pub async fn lcd_task( ) .unwrap(); - embedded_graphics::Drawable::draw( - &embedded_graphics::text::Text::with_baseline( - "Hello Rust!", - embedded_graphics::prelude::Point::new(0, 16), - text_style, - embedded_graphics::text::Baseline::Top, - ), - &mut fbuf, + Text::with_alignment( + "First line\nSecond line", + Point::new(128 / 2, 16), + text_style, + Alignment::Center, ) + .draw(&mut fbuf) .unwrap(); embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); disp.flush().await.unwrap(); } + */ + + let mut lcd_driver: LcdAbstract<80, 16, 2, 3> = LcdAbstract::new(); + _ = lcd_driver.print( + 0, + &alloc::format!("{:X}", crate::utils::get_efuse_u32()), + PrintAlign::Left, + true, + ); + _ = lcd_driver.print(1, crate::version::VERSION, PrintAlign::Center, true); + fbuf.clear(BinaryColor::Off); + lcd_driver.display_on_oled(&mut fbuf).await; + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); + + _ = lcd_driver.print( + 0, + &alloc::format!("{}%", global_state.show_battery.wait().await), + PrintAlign::Right, + false, + ); + fbuf.clear(BinaryColor::Off); + lcd_driver.display_on_oled(&mut fbuf).await; + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); + + Timer::after_millis(2500).await; + + fbuf.clear(BinaryColor::Off); + _ = lcd_driver.clear_all(); + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); + + let mut last_update; + loop { + let current_state = global_state.state.value().await.clone(); + log::debug!("lcd current_state: {current_state:?}"); + last_update = Instant::now(); + + if sleep_state() { + //lcd.backlight_on(); + + unsafe { + crate::state::SLEEP_STATE = false; + } + } + + let current_scene = current_state.scene.clone(); + let fut = async { + let _ = process_lcd( + current_state, + &global_state, + &mut lcd_driver, + &wifi_setup_sig, + &mut fbuf, + &mut disp, + ) + .await; + fbuf.clear(BinaryColor::Off); + lcd_driver.display_on_oled(&mut fbuf).await; + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); + + let mut scroll_ticker = + embassy_time::Ticker::every(Duration::from_millis(SCROLL_TICKER_INVERVAL_MS)); + loop { + scroll_ticker.next().await; + let changed = lcd_driver.scroll_step(); + if changed.is_ok_and(|c| c) { + fbuf.clear(BinaryColor::Off); + lcd_driver.display_on_oled(&mut fbuf).await; + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()) + .unwrap(); + disp.flush().await.unwrap(); + } + + #[cfg(not(any(feature = "e2e", feature = "qa")))] + if !sleep_state() + && (Instant::now() - last_update).as_millis() > SLEEP_AFTER_MS + && current_scene.can_sleep() + { + _ = lcd_driver.print(0, "Sleep", PrintAlign::Center, true); + _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); + fbuf.clear(BinaryColor::Off); + lcd_driver.display_on_oled(&mut fbuf).await; + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()) + .unwrap(); + disp.flush().await.unwrap(); + //lcd.backlight_off(); + + { + global_state.state.lock().await.server_connected = Some(false); + } + + unsafe { + crate::state::SLEEP_STATE = true; + crate::state::TRUST_SERVER = false; + } + + global_state.state.signal_reset(); + } + + #[cfg(not(any(feature = "e2e", feature = "qa")))] + if sleep_state() + && !deeper_sleep_state() + && (Instant::now() - last_update).as_millis() > DEEPER_SLEEP_AFTER_MS + { + _ = lcd_driver.print(0, "Deep Sleep", PrintAlign::Center, true); + _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); + fbuf.clear(BinaryColor::Off); + lcd_driver.display_on_oled(&mut fbuf).await; + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()) + .unwrap(); + disp.flush().await.unwrap(); + crate::utils::deeper_sleep(); + } + } + }; + + let res = embassy_futures::select::select(fut, global_state.state.wait()).await; + match res { + embassy_futures::select::Either::First(_) => {} + embassy_futures::select::Either::Second(_) => { + continue; + } + } + } /* let mut lcd = { @@ -236,13 +353,15 @@ pub async fn lcd_task( */ } -async fn process_lcd( +async fn process_lcd( current_state: SignaledGlobalStateInner, global_state: &GlobalState, lcd_driver: &mut LcdAbstract<80, 16, 2, 3>, - lcd: &mut LcdDisplay, + //lcd: &mut LcdDisplay, wifi_setup_sig: &Signal, - display: &ShifterValueRange, + //display: &ShifterValueRange, + fbuf: &mut FrameBuf, + disp: &mut GraphicsMode>, ) -> Option<()> { #[cfg(feature = "bat_dev_lcd")] { @@ -344,7 +463,10 @@ async fn process_lcd( true, ) .ok()?; - lcd_driver.display_on_lcd(lcd).await; + fbuf.clear(BinaryColor::Off); + lcd_driver.display_on_oled(fbuf).await; + embedded_graphics::prelude::DrawTarget::draw_iter(disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); Timer::after_millis(300).await; lcd_driver @@ -472,7 +594,10 @@ async fn process_lcd( ) .ok()?; - lcd_driver.display_on_lcd(lcd).await; + fbuf.clear(BinaryColor::Off); + lcd_driver.display_on_oled(fbuf).await; + embedded_graphics::prelude::DrawTarget::draw_iter(disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); wifi_setup_sig.wait().await; global_state.state.lock().await.scene = Scene::AutoSetupWait; } @@ -607,7 +732,10 @@ async fn process_lcd( .print(0, &time_str, PrintAlign::Center, true) .ok()?; - lcd_driver.display_on_lcd(lcd).await; + fbuf.clear(BinaryColor::Off); + lcd_driver.display_on_oled(fbuf).await; + embedded_graphics::prelude::DrawTarget::draw_iter(disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); Timer::after_millis(LCD_INSPECTION_FRAME_TIME).await; } } @@ -618,8 +746,11 @@ async fn process_lcd( .print(0, &time_str, PrintAlign::Center, true) .ok()?; - display.set_data_raw(&crate::utils::stackmat::time_str_to_display(&time_str)); - lcd_driver.display_on_lcd(lcd).await; + //display.set_data_raw(&crate::utils::stackmat::time_str_to_display(&time_str)); + fbuf.clear(BinaryColor::Off); + lcd_driver.display_on_oled(fbuf).await; + embedded_graphics::prelude::DrawTarget::draw_iter(disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); }, Scene::Finished => { let solve_time = current_state.solve_time.unwrap_or(0); @@ -704,7 +835,10 @@ async fn process_lcd( let progress = global_state.update_progress.wait().await; _ = lcd_driver.print(1, &alloc::format!("{progress}%"), PrintAlign::Center, true); - lcd_driver.display_on_lcd(lcd).await; + fbuf.clear(BinaryColor::Off); + lcd_driver.display_on_oled(fbuf).await; + embedded_graphics::prelude::DrawTarget::draw_iter(disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); } } } diff --git a/src/utils/lcd_abstract.rs b/src/utils/lcd_abstract.rs index bc09ef5..39bab97 100644 --- a/src/utils/lcd_abstract.rs +++ b/src/utils/lcd_abstract.rs @@ -1,4 +1,12 @@ use ag_lcd_async::LcdDisplay; +use alloc::{string::String, vec::Vec}; +use embedded_graphics::{ + Drawable, + pixelcolor::BinaryColor, + prelude::Point, + text::{Alignment, Text}, +}; +use embedded_graphics_framebuf::FrameBuf; use embedded_hal::digital::OutputPin; use embedded_hal_async::delay::DelayNs; @@ -164,4 +172,32 @@ impl, + ) { + let display_data = self.display_data(); + + let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() + .font(&embedded_graphics::mono_font::ascii::FONT_6X10) + .text_color(embedded_graphics::pixelcolor::BinaryColor::On) + .build(); + + let text = display_data + .0 + .iter() + .map(|l| l.0.iter().map(|c| *c as char).collect::()) + .collect::>() + .join("\n"); + + Text::with_alignment( + &text, + Point::new(128 / 2, 26), + text_style, + Alignment::Center, + ) + .draw(fbuf) + .unwrap(); + } } From a2a70605b00af1274d14046ef8d0bd62c9bb52cd Mon Sep 17 00:00:00 2001 From: filipton Date: Mon, 9 Mar 2026 22:06:32 +0100 Subject: [PATCH 07/31] wip: store oled framebuffer in heap memory --- Cargo.lock | 17 ---- Cargo.toml | 2 - src/lcd.rs | 177 ++------------------------------------ src/utils/lcd_abstract.rs | 14 --- 4 files changed, 5 insertions(+), 205 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c3de96..eaed00e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,15 +47,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ag-lcd-async" -version = "0.3.0" -source = "git+https://github.com/filipton/ag-lcd-async#c4834c24769bfb0c5c597c71c675b9f0b6305bf3" -dependencies = [ - "embedded-hal 1.0.0", - "embedded-hal-async", -] - [[package]] name = "ahash" version = "0.8.12" @@ -645,12 +636,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "dyn-smooth" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47fac87554ece38a037e0d53e750ee5129670b6f97e712bb0c411268181f3700" - [[package]] name = "ecdsa" version = "0.16.9" @@ -1542,14 +1527,12 @@ name = "fkm-firmware" version = "0.1.0" dependencies = [ "adv-shift-registers", - "ag-lcd-async", "anyhow", "bq27441", "critical-section", "display-interface", "display-interface-i2c", "dotenvy", - "dyn-smooth", "embassy-executor", "embassy-futures", "embassy-net", diff --git a/Cargo.toml b/Cargo.toml index 54955c9..f4c9039 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,6 @@ portable-atomic = { version = "1.13.1", default-features = false } critical-section = "1.2.0" getrandom = { version = "=0.2.15", features = ["custom"] } uuid = { version = "=1.12.1", default-features = false, features = ["v4"] } -dyn-smooth = "0.2.0" esp-hal-ota = { version = "0.4.6", features = ["esp32c3"] } esp32c3 = { version = "0.31.0" } anyhow = { version = "1.0.102", default-features = false } @@ -44,7 +43,6 @@ embedded-hal-bus = { version = "0.3.0", features = ["async"] } #embedded-tls = { git = "https://github.com/drogue-iot/embedded-tls.git", default-features = false, features = ["alloc", "embedded-io-adapters", "log"] } embedded-tls = { git = "https://github.com/filipton/embedded-tls", default-features = false, features = ["alloc", "embedded-io-adapters", "log"] } rand_core = { version = "0.6.4", features = ["getrandom"] } -ag-lcd-async = { git = "https://github.com/filipton/ag-lcd-async", features = [] } esp-hal-mfrc522 = { version = "0.3.3", features = ["embassy-time"] } esp-bootloader-esp-idf = { version = "0.4.0", features = ["log-04", "esp32c3"] } trouble-host = { version = "0.5.1", features = ["scan", "security"] } diff --git a/src/lcd.rs b/src/lcd.rs index 2dfca7d..150660c 100644 --- a/src/lcd.rs +++ b/src/lcd.rs @@ -1,5 +1,4 @@ use adv_shift_registers::wrappers::ShifterValueRange; -use ag_lcd_async::LcdDisplay; use alloc::{rc::Rc, string::ToString}; use display_interface_i2c::I2CInterface; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; @@ -44,7 +43,7 @@ pub async fn lcd_task( let di = display_interface_i2c::I2CInterface::new(i2c, 0x3C, 0x40); let raw_disp = oled_async::builder::Builder::new(oled_async::displays::ssd1309::Ssd1309_128_64 {}) - .with_rotation(oled_async::prelude::DisplayRotation::Rotate180) + .with_rotation(oled_async::prelude::DisplayRotation::Rotate0) .connect(di); let mut disp: oled_async::mode::GraphicsMode<_, _> = raw_disp.into(); @@ -54,48 +53,11 @@ pub async fn lcd_task( disp.clear(); disp.flush().await.unwrap(); - let mut data = [embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 64) as usize]; - let mut fbuf = embedded_graphics_framebuf::FrameBuf::new(&mut data, 128, 64); + let mut data = + alloc::vec![embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 64) as usize]; + let data = data.as_mut_array().unwrap(); - /* - let start = Instant::now(); - loop { - embedded_graphics::prelude::DrawTarget::clear( - &mut fbuf, - embedded_graphics::pixelcolor::BinaryColor::Off, - ); - Timer::after(embassy_time::Duration::from_millis(1000 / 60)).await; - - let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() - .font(&embedded_graphics::mono_font::ascii::FONT_6X10) - .text_color(embedded_graphics::pixelcolor::BinaryColor::On) - .build(); - - let time_str = crate::utils::stackmat::ms_to_time_str(start.elapsed().as_millis()); - embedded_graphics::Drawable::draw( - &embedded_graphics::text::Text::with_baseline( - &alloc::format!("Hello world! {time_str}"), - embedded_graphics::prelude::Point::zero(), - text_style, - embedded_graphics::text::Baseline::Top, - ), - &mut fbuf, - ) - .unwrap(); - - Text::with_alignment( - "First line\nSecond line", - Point::new(128 / 2, 16), - text_style, - Alignment::Center, - ) - .draw(&mut fbuf) - .unwrap(); - - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); - } - */ + let mut fbuf = embedded_graphics_framebuf::FrameBuf::new(data, 128, 64); let mut lcd_driver: LcdAbstract<80, 16, 2, 3> = LcdAbstract::new(); _ = lcd_driver.print( @@ -222,135 +184,6 @@ pub async fn lcd_task( } } } - - /* - let mut lcd = { - let bl_pin = lcd_shifter.get_pin_mut(1, true); - let rs_pin = lcd_shifter.get_pin_mut(2, true); - let en_pin = lcd_shifter.get_pin_mut(3, true); - let d4_pin = lcd_shifter.get_pin_mut(4, false); - let d5_pin = lcd_shifter.get_pin_mut(5, false); - let d6_pin = lcd_shifter.get_pin_mut(6, false); - let d7_pin = lcd_shifter.get_pin_mut(7, false); - LcdDisplay::new(rs_pin, en_pin, Delay) - .with_display(ag_lcd_async::Display::On) - .with_blink(ag_lcd_async::Blink::Off) - .with_cursor(ag_lcd_async::Cursor::Off) - .with_size(ag_lcd_async::Size::Dots5x8) - .with_cols(16) - .with_lines(ag_lcd_async::Lines::TwoLines) - .with_half_bus(d4_pin, d5_pin, d6_pin, d7_pin) - .with_backlight(bl_pin) - .build() - .await - }; - - lcd.clear().await; - lcd.backlight_on(); - - let mut lcd_driver: LcdAbstract<80, 16, 2, 3> = LcdAbstract::new(); - - _ = lcd_driver.print( - 0, - &alloc::format!("{:X}", crate::utils::get_efuse_u32()), - PrintAlign::Left, - true, - ); - _ = lcd_driver.print(1, crate::version::VERSION, PrintAlign::Center, true); - lcd_driver.display_on_lcd(&mut lcd).await; - - _ = lcd_driver.print( - 0, - &alloc::format!("{}%", global_state.show_battery.wait().await), - PrintAlign::Right, - false, - ); - lcd_driver.display_on_lcd(&mut lcd).await; - - #[cfg(not(feature = "bat_dev_lcd"))] - Timer::after_millis(2500).await; - - _ = lcd_driver.clear_all(); - let mut last_update; - loop { - let current_state = global_state.state.value().await.clone(); - log::debug!("lcd current_state: {current_state:?}"); - last_update = Instant::now(); - - if sleep_state() { - lcd.backlight_on(); - - unsafe { - crate::state::SLEEP_STATE = false; - } - } - - let current_scene = current_state.scene.clone(); - let fut = async { - let _ = process_lcd( - current_state, - &global_state, - &mut lcd_driver, - &mut lcd, - &wifi_setup_sig, - &display, - ) - .await; - lcd_driver.display_on_lcd(&mut lcd).await; - - let mut scroll_ticker = - embassy_time::Ticker::every(Duration::from_millis(SCROLL_TICKER_INVERVAL_MS)); - loop { - scroll_ticker.next().await; - let changed = lcd_driver.scroll_step(); - if changed.is_ok_and(|c| c) { - lcd_driver.display_on_lcd(&mut lcd).await; - } - - #[cfg(not(any(feature = "e2e", feature = "qa")))] - if !sleep_state() - && (Instant::now() - last_update).as_millis() > SLEEP_AFTER_MS - && current_scene.can_sleep() - { - _ = lcd_driver.print(0, "Sleep", PrintAlign::Center, true); - _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); - lcd_driver.display_on_lcd(&mut lcd).await; - lcd.backlight_off(); - - { - global_state.state.lock().await.server_connected = Some(false); - } - - unsafe { - crate::state::SLEEP_STATE = true; - crate::state::TRUST_SERVER = false; - } - - global_state.state.signal_reset(); - } - - #[cfg(not(any(feature = "e2e", feature = "qa")))] - if sleep_state() - && !deeper_sleep_state() - && (Instant::now() - last_update).as_millis() > DEEPER_SLEEP_AFTER_MS - { - _ = lcd_driver.print(0, "Deep Sleep", PrintAlign::Center, true); - _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); - lcd_driver.display_on_lcd(&mut lcd).await; - crate::utils::deeper_sleep(); - } - } - }; - - let res = embassy_futures::select::select(fut, global_state.state.wait()).await; - match res { - embassy_futures::select::Either::First(_) => {} - embassy_futures::select::Either::Second(_) => { - continue; - } - } - } - */ } async fn process_lcd( diff --git a/src/utils/lcd_abstract.rs b/src/utils/lcd_abstract.rs index 39bab97..459bc5a 100644 --- a/src/utils/lcd_abstract.rs +++ b/src/utils/lcd_abstract.rs @@ -1,4 +1,3 @@ -use ag_lcd_async::LcdDisplay; use alloc::{string::String, vec::Vec}; use embedded_graphics::{ Drawable, @@ -160,19 +159,6 @@ impl(&mut self, lcd: &mut LcdDisplay) { - let display_data = self.display_data(); - for (y, line) in display_data.0.iter().enumerate() { - if line.1 { - lcd.set_position(0, y as u8).await; - lcd.print(unsafe { core::str::from_utf8_unchecked(line.0) }) - .await; - - display_data.1[y].copy_from_slice(line.0); - } - } - } - pub async fn display_on_oled( &mut self, fbuf: &mut FrameBuf, From e3e7b07668757704dea034c42414d1e6a9fab57e Mon Sep 17 00:00:00 2001 From: filipton Date: Wed, 11 Mar 2026 21:29:39 +0100 Subject: [PATCH 08/31] wip: v4 feature --- Cargo.toml | 16 +++--- src/{battery.rs => battery_v4.rs} | 52 +---------------- src/board.rs | 93 ++++++++++--------------------- src/buttons.rs | 10 ++-- src/{lcd.rs => lcd_v4.rs} | 2 - src/main.rs | 49 ++++++++++------ src/rfid.rs | 35 +++++++----- src/stackmat.rs | 22 +++++--- src/utils/buttons.rs | 11 ++-- src/utils/stackmat.rs | 3 + 10 files changed, 121 insertions(+), 172 deletions(-) rename src/{battery.rs => battery_v4.rs} (63%) rename src/{lcd.rs => lcd_v4.rs} (99%) diff --git a/Cargo.toml b/Cargo.toml index f4c9039..3246fa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,18 +47,18 @@ esp-hal-mfrc522 = { version = "0.3.3", features = ["embassy-time"] } esp-bootloader-esp-idf = { version = "0.4.0", features = ["log-04", "esp32c3"] } trouble-host = { version = "0.5.1", features = ["scan", "security"] } -display-interface = "0.5.0" -display-interface-i2c = "0.5.0" -embedded-graphics = "0.8.2" -embedded-graphics-framebuf = "0.5.0" -oled_async = { version = "0.2.0", features = ["i2c"] } -bq27441 = { version = "0.1.0", features = ["async", "embassy"] } +display-interface = { version = "0.5.0", optional = true } +display-interface-i2c = { version = "0.5.0", optional = true } +embedded-graphics = { version = "0.8.2", optional = true } +embedded-graphics-framebuf = { version = "0.5.0", optional = true } +oled_async = { version = "0.2.0", features = ["i2c"], optional = true } +bq27441 = { version = "0.1.0", features = ["async", "embassy"], optional = true } [patch.crates-io] elliptic-curve = { git = "https://github.com/filipton/rust-crypto-traits.git" } [features] -default = ["sleep"] +default = ["sleep", "v4"] gen_version = [] bat_dev_lcd = [] release_build = ["sleep"] @@ -67,7 +67,7 @@ qa = [] sleep = [] auto_add = [] v3 = [] -v4 = [] +v4 = ["dep:display-interface", "dep:display-interface-i2c", "dep:embedded-graphics", "dep:embedded-graphics-framebuf", "dep:oled_async", "dep:bq27441"] [profile.dev] opt-level = 3 diff --git a/src/battery.rs b/src/battery_v4.rs similarity index 63% rename from src/battery.rs rename to src/battery_v4.rs index 550ecf6..5708021 100644 --- a/src/battery.rs +++ b/src/battery_v4.rs @@ -1,12 +1,5 @@ -use crate::{ - consts::BATTERY_SEND_INTERVAL_MS, - state::sleep_state, - utils::{rolling_average::RollingAverage, shared_i2c::SharedI2C}, -}; -use embassy_time::{Duration, Instant, Timer}; -use esp_hal::analog::adc::{Adc, AdcConfig, Attenuation}; - -type AdcCal = esp_hal::analog::adc::AdcCalCurve>; +use crate::{consts::BATTERY_SEND_INTERVAL_MS, state::sleep_state, utils::shared_i2c::SharedI2C}; +use embassy_time::Timer; const BATTERY_CURVE: [(f64, u8); 11] = [ (3350.0, 0), @@ -25,25 +18,7 @@ const BAT_MIN: f64 = BATTERY_CURVE[0].0; const BAT_MAX: f64 = BATTERY_CURVE[BATTERY_CURVE.len() - 1].0; #[embassy_executor::task] -pub async fn battery_read_task( - i2c: SharedI2C, - //adc_pin: esp_hal::peripherals::GPIO2<'static>, - //adc: esp_hal::peripherals::ADC1<'static>, - state: crate::state::GlobalState, -) { - /* - let mut adc_config = AdcConfig::new(); - - let mut adc_pin = adc_config.enable_pin_with_cal::<_, AdcCal>(adc_pin, Attenuation::_11dB); - let mut adc = Adc::new(adc, adc_config).into_async(); - - - let base_freq = 2.0; - let sample_freq = 1000.0; - let sensitivity = 0.5; - let mut smoother = dyn_smooth::DynamicSmootherEcoF32::new(base_freq, sample_freq, sensitivity); - let mut avg = RollingAverage::<128>::new(); - */ +pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) { let Ok(mut gauge) = bq27441::Bq27441Async::new(i2c).await else { state.show_battery.signal(0); log::error!("BQ27441 init failed!"); @@ -57,20 +32,6 @@ pub async fn battery_read_task( continue; } - /* - let read = adc.read_oneshot(&mut adc_pin).await; - let read = smoother.tick(read as f32); - avg.push(read); - */ - - /* - #[cfg(feature = "bat_dev_lcd")] - { - let mut state = state.state.lock().await; - state.current_bat_read = Some(read); - } - */ - if !lcd_sent { let mut soc = gauge.state_of_charge().await.unwrap_or(0) as u8; if soc == 0 { @@ -104,13 +65,6 @@ pub async fn battery_read_task( log::info!("Battery {mv}mv {soc}%"); Timer::after_millis(BATTERY_SEND_INTERVAL_MS).await; - /* - #[cfg(feature = "bat_dev_lcd")] - { - let mut state = state.state.lock().await; - state.avg_bat_read = avg.average(); - } - */ } } diff --git a/src/board.rs b/src/board.rs index abca3c9..2446996 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1,24 +1,20 @@ -use crate::utils::{ - shared_i2c::SharedI2C, - stackmat::{DEC_DIGITS, DOT_MOD}, -}; -use adv_shift_registers::wrappers::ShifterValueRange; -use alloc::rc::Rc; -use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; -use embedded_hal::digital::OutputPin; use esp_hal::{ - Async, - gpio::{AnyPin, Input, InputConfig, Level, Output, Pin, Pull}, + gpio::{AnyPin, Input, InputConfig, Output, Pin, Pull}, i2c::master::I2c, peripherals::{ - self, ADC1, AES, BT, FLASH, I2C0, Peripherals, SPI2, SW_INTERRUPT, TIMG0, TIMG1, UART1, - WIFI, + ADC1, AES, BT, FLASH, Peripherals, SPI2, SW_INTERRUPT, TIMG0, TIMG1, UART1, WIFI, }, rng::Rng, time::Rate, timer::timg::TimerGroup, }; +#[cfg(feature = "v3")] +use crate::utils::stackmat::{DEC_DIGITS, DOT_MOD}; + +#[cfg(feature = "v4")] +use crate::utils::shared_i2c::SharedI2C; + #[allow(dead_code)] pub struct Board { // peripherals @@ -36,23 +32,36 @@ pub struct Board { pub sw_interrupt: SW_INTERRUPT<'static>, // spi - //pub miso: AnyPin<'static>, - //pub mosi: AnyPin<'static>, - //pub sck: AnyPin<'static>, - //pub cs: adv_shift_registers::wrappers::ShifterPin, + #[cfg(feature = "v3")] + pub miso: AnyPin<'static>, + #[cfg(feature = "v3")] + pub mosi: AnyPin<'static>, + #[cfg(feature = "v3")] + pub sck: AnyPin<'static>, + #[cfg(feature = "v3")] + pub cs: adv_shift_registers::wrappers::ShifterPin, + + #[cfg(feature = "v4")] pub display_rst: Output<'static>, + #[cfg(feature = "v4")] pub i2c: SharedI2C, pub stackmat_rx: AnyPin<'static>, + #[cfg(feature = "v4")] pub buttons: [Input<'static>; 4], - // pub battery: esp_hal::peripherals::GPIO2<'static>, - //pub button_input: Input<'static>, - //pub digits_shifters: ShifterValueRange, + #[cfg(feature = "v3")] + pub battery: esp_hal::peripherals::GPIO2<'static>, + #[cfg(feature = "v3")] + pub button_input: Input<'static>, + #[cfg(feature = "v3")] + pub digits_shifters: adv_shift_registers::wrappers::ShifterValueRange, - //pub buttons_shifter: adv_shift_registers::wrappers::ShifterValue, - // pub lcd: adv_shift_registers::wrappers::ShifterValue, + #[cfg(feature = "v3")] + pub buttons_shifter: adv_shift_registers::wrappers::ShifterValue, + #[cfg(feature = "v3")] + pub lcd: adv_shift_registers::wrappers::ShifterValue, // usb pins pub usb_dp: AnyPin<'static>, @@ -74,10 +83,6 @@ impl Board { let flash = peripherals.FLASH; let sw_interrupt = peripherals.SW_INTERRUPT; - //let sck = peripherals.GPIO4.degrade(); - //let miso = peripherals.GPIO5.degrade(); - //let mosi = peripherals.GPIO6.degrade(); - //let battery = peripherals.GPIO2; let stackmat_rx = peripherals.GPIO20.degrade(); let usb_dp = peripherals.GPIO19.degrade(); let usb_dm = peripherals.GPIO18.degrade(); @@ -121,35 +126,6 @@ impl Board { .into_async(); let i2c = SharedI2C::new(i2c); - //let button_input = Input::new( - // peripherals.GPIO3, - // InputConfig::default().with_pull(Pull::Down), - //); - - //let shifter_data_pin = Output::new(peripherals.GPIO10, Level::Low, Default::default()); - //let shifter_latch_pin = Output::new(peripherals.GPIO5, Level::Low, Default::default()); - //let shifter_clk_pin = Output::new(peripherals.GPIO21, Level::Low, Default::default()); - - //let adv_shift_reg = adv_shift_registers::AdvancedShiftRegister::<8, _>::new( - // shifter_data_pin, - // shifter_clk_pin, - // shifter_latch_pin, - // 0, - //); - //let adv_shift_reg = alloc::boxed::Box::new(adv_shift_reg); - //let adv_shift_reg = alloc::boxed::Box::leak(adv_shift_reg); - - //let mut backlight = adv_shift_reg.get_pin_mut(1, 1, false); - //_ = backlight.set_high(); - - //let buttons_shifter = adv_shift_reg.get_shifter_mut(0); - //let lcd = adv_shift_reg.get_shifter_mut(1); - //let digits_shifters = adv_shift_reg.get_shifter_range_mut(2..8); - //digits_shifters.set_data(&[!DEC_DIGITS[8] ^ DOT_MOD; 6]); - - //let mut cs = adv_shift_reg.get_pin_mut(1, 0, true); - //_ = cs.set_high(); - Board { timg0, timg1, @@ -164,21 +140,12 @@ impl Board { flash, sw_interrupt, - //miso, - //mosi, - //sck, - //cs, i2c, display_rst, stackmat_rx, buttons: [b1, b2, b3, b4], - // battery, - //button_input, - //buttons_shifter, - //digits_shifters, - //lcd, usb_dp, usb_dm, } diff --git a/src/buttons.rs b/src/buttons.rs index 19bc2fb..42bbbb3 100644 --- a/src/buttons.rs +++ b/src/buttons.rs @@ -15,9 +15,9 @@ macros::generate_button_handler_enum!(triggered: &ButtonTrigger, hold_time: u64, #[embassy_executor::task] pub async fn buttons_task( state: GlobalState, - button_inputs: [Input<'static>; 4], - //button_input: Input<'static>, - //button_reg: adv_shift_registers::wrappers::ShifterValue, + #[cfg(feature = "v4")] button_inputs: [Input<'static>; 4], + #[cfg(feature = "v3")] button_input: Input<'static>, + #[cfg(feature = "v3")] button_reg: adv_shift_registers::wrappers::ShifterValue, ) { let mut handler = ButtonsHandler::new(Some(wakeup_button())); handler.add_handler(Button::Third, ButtonTrigger::Up, submit_up()); @@ -56,7 +56,9 @@ pub async fn buttons_task( ); handler.add_handler(Button::Second, ButtonTrigger::Up, delegate_hold()); - //handler.run(&state, &button_input, &button_reg).await; + #[cfg(feature = "v3")] + handler.run(&state, &button_input, &button_reg).await; + #[cfg(feature = "v4")] handler.run(&state, &button_inputs).await; } diff --git a/src/lcd.rs b/src/lcd_v4.rs similarity index 99% rename from src/lcd.rs rename to src/lcd_v4.rs index 150660c..d501a23 100644 --- a/src/lcd.rs +++ b/src/lcd_v4.rs @@ -10,8 +10,6 @@ use embedded_graphics::{ text::{Alignment, Text}, }; use embedded_graphics_framebuf::FrameBuf; -use embedded_hal::digital::OutputPin; -use embedded_hal_async::delay::DelayNs; use esp_hal::gpio::Output; use oled_async::{displays::ssd1309::Ssd1309_128_64, mode::GraphicsMode}; diff --git a/src/main.rs b/src/main.rs index 67446e3..8a89b85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use board::Board; use consts::LOG_SEND_INTERVAL_MS; use embassy_executor::Spawner; use embassy_sync::signal::Signal; -use embassy_time::{Delay, Instant, Timer}; +use embassy_time::{Instant, Timer}; use esp_backtrace as _; use esp_hal::interrupt::software::SoftwareInterruptControl; use esp_hal_wifimanager::{Nvs, WIFI_NVS_KEY}; @@ -21,12 +21,10 @@ use structs::ConnSettings; use utils::{logger::FkmLogger, set_brownout_detection}; use ws_framer::{WsUrl, WsUrlOwned}; -mod battery; mod bluetooth; mod board; mod buttons; mod consts; -mod lcd; mod mdns; mod rfid; mod stackmat; @@ -37,6 +35,11 @@ mod utils; mod version; mod ws; +#[cfg(feature = "v4")] +mod battery_v4; +#[cfg(feature = "v4")] +mod lcd_v4; + #[cfg(feature = "qa")] mod qa; @@ -114,41 +117,51 @@ async fn main(spawner: Spawner) { unsafe { crate::state::SIGN_KEY = sign_key }; } - spawner.must_spawn(lcd::lcd_task( + #[cfg(feature = "v4")] + spawner.must_spawn(lcd_v4::lcd_task( board.i2c.clone(), board.display_rst, - //board.lcd, global_state.clone(), wifi_setup_sig.clone(), - //board.digits_shifters.clone(), )); - spawner.must_spawn(battery::battery_read_task( + #[cfg(feature = "v4")] + spawner.must_spawn(battery_v4::battery_read_task( board.i2c.clone(), - //board.battery, - //board.adc1, global_state.clone(), )); + spawner.must_spawn(buttons::buttons_task( global_state.clone(), + #[cfg(feature = "v4")] board.buttons, - //board.button_input, - //board.buttons_shifter, + #[cfg(feature = "v3")] + board.button_input, + #[cfg(feature = "v3")] + board.buttons_shifter, )); spawner.must_spawn(stackmat::stackmat_task( board.uart1, board.stackmat_rx, - // board.digits_shifters, + #[cfg(feature = "v3")] + board.digits_shifters, global_state.clone(), )); spawner.must_spawn(rfid::rfid_task( + #[cfg(feature = "v4")] board.i2c.clone(), - //board.miso, - //board.mosi, - //board.sck, - //board.cs, - //board.spi2, - //board.spi_dma, + #[cfg(feature = "v3")] + board.miso, + #[cfg(feature = "v3")] + board.mosi, + #[cfg(feature = "v3")] + board.sck, + #[cfg(feature = "v3")] + board.cs, + #[cfg(feature = "v3")] + board.spi2, + #[cfg(feature = "v3")] + board.spi_dma, global_state.clone(), )); diff --git a/src/rfid.rs b/src/rfid.rs index d37fabd..4e20c65 100644 --- a/src/rfid.rs +++ b/src/rfid.rs @@ -2,46 +2,55 @@ use crate::consts::RFID_RETRY_INIT_MS; use crate::state::{GlobalState, MenuScene, current_epoch, sleep_state}; use crate::structs::{CardInfoResponsePacket, SolveConfirmPacket}; use crate::translations::{TranslationKey, get_translation}; -use crate::utils::shared_i2c::SharedI2C; use alloc::string::ToString; use anyhow::{Result, anyhow}; use embassy_time::{Duration, Instant, Timer}; +use esp_hal_mfrc522::consts::UidSize; + +#[cfg(feature = "v3")] use esp_hal::i2c::master::I2c; +#[cfg(feature = "v3")] use esp_hal::time::Rate; +#[cfg(feature = "v3")] use esp_hal::{ dma::{DmaRxBuf, DmaTxBuf}, dma_buffers, gpio::AnyPin, spi::{Mode, master::Spi}, }; -use esp_hal_mfrc522::consts::UidSize; #[embassy_executor::task] pub async fn rfid_task( - i2c: SharedI2C, - //miso: AnyPin<'static>, - //mosi: AnyPin<'static>, - //sck: AnyPin<'static>, - //cs_pin: adv_shift_registers::wrappers::ShifterPin, - //spi: esp_hal::peripherals::SPI2<'static>, - //dma_chan: esp_hal::peripherals::DMA_CH0<'static>, + #[cfg(feature = "v4")] i2c: crate::utils::shared_i2c::SharedI2C, + #[cfg(feature = "v3")] miso: AnyPin<'static>, + #[cfg(feature = "v3")] mosi: AnyPin<'static>, + #[cfg(feature = "v3")] sck: AnyPin<'static>, + #[cfg(feature = "v3")] cs_pin: adv_shift_registers::wrappers::ShifterPin, + #[cfg(feature = "v3")] spi: esp_hal::peripherals::SPI2<'static>, + #[cfg(feature = "v3")] dma_chan: esp_hal::peripherals::DMA_CH0<'static>, global_state: GlobalState, ) { - let driver = esp_hal_mfrc522::drivers::I2CDriver::new(i2c, 0x28); - let mut mfrc522 = esp_hal_mfrc522::MFRC522::new(driver); + #[cfg(feature = "v4")] + let mut mfrc522 = { + let driver = esp_hal_mfrc522::drivers::I2CDriver::new(i2c, 0x28); + esp_hal_mfrc522::MFRC522::new(driver) + }; - /* #[allow(clippy::manual_div_ceil)] + #[cfg(feature = "v3")] let (rx_buffer, rx_descriptors, tx_buffer, tx_descriptors) = dma_buffers!(512); + #[cfg(feature = "v3")] let Ok(dma_tx_buf) = DmaTxBuf::new(tx_descriptors, tx_buffer) else { log::error!("Dma tx buf failed"); return; }; + #[cfg(feature = "v3")] let Ok(dma_rx_buf) = DmaRxBuf::new(rx_descriptors, rx_buffer) else { log::error!("Dma rx buf failed"); return; }; + #[cfg(feature = "v3")] let Ok(spi) = Spi::new( spi, esp_hal::spi::master::Config::default() @@ -60,6 +69,7 @@ pub async fn rfid_task( return; }; + #[cfg(feature = "v3")] let mut mfrc522 = { let Ok(spi) = embedded_hal_bus::spi::ExclusiveDevice::new(spi, cs_pin, embassy_time::Delay) else { @@ -69,7 +79,6 @@ pub async fn rfid_task( esp_hal_mfrc522::MFRC522::new(esp_hal_mfrc522::drivers::SpiDriver::new(spi)) }; - */ #[cfg(not(feature = "e2e"))] loop { diff --git a/src/stackmat.rs b/src/stackmat.rs index baba252..0d2ca08 100644 --- a/src/stackmat.rs +++ b/src/stackmat.rs @@ -1,11 +1,8 @@ use crate::{ consts::{INSPECTION_TIME_DNF, INSPECTION_TIME_PLUS2}, state::{GlobalState, Scene, sleep_state}, - utils::stackmat::{ - StackmatTimerState, ms_to_time_str, parse_stackmat_data, time_str_to_display, - }, + utils::stackmat::{StackmatTimerState, parse_stackmat_data}, }; -use adv_shift_registers::wrappers::ShifterValueRange; use alloc::string::ToString; use embassy_time::{Instant, Timer}; use esp_hal::{gpio::AnyPin, peripherals::UART1, uart::UartRx}; @@ -16,7 +13,7 @@ pub static mut CURRENT_TIME: u64 = 0; pub async fn stackmat_task( uart: UART1<'static>, uart_pin: AnyPin<'static>, - // display: ShifterValueRange, + #[cfg(feature = "v3")] display: adv_shift_registers::wrappers::ShifterValueRange, global_state: GlobalState, ) { let serial_config = esp_hal::uart::Config::default().with_baudrate(1200); @@ -50,7 +47,8 @@ pub async fn stackmat_task( && last_state != Some(false) { last_state = Some(false); - // display.set_data(&[255; 6]); + #[cfg(feature = "v3")] + display.set_data(&[255; 6]); let mut state = global_state.state.lock().await; state.stackmat_connected = Some(false); @@ -202,8 +200,13 @@ pub async fn stackmat_task( saved_state.to_nvs(&global_state.nvs).await; } - let time_str = ms_to_time_str(parsed.1); - // display.set_data(&time_str_to_display(&time_str)); + #[cfg(feature = "v3")] + { + let time_str = crate::utils::stackmat::ms_to_time_str(parsed.1); + display.set_data(&crate::utils::stackmat::time_str_to_display( + &time_str, + )); + } #[cfg(feature = "qa")] crate::qa::send_qa_resp(crate::qa::QaSignal::Stackmat(parsed.1)); @@ -237,7 +240,8 @@ pub async fn stackmat_task( state.time_confirmed = true; } - // display.set_data(&[255; 6]); + #[cfg(feature = "v3")] + display.set_data(&[255; 6]); } last_stackmat_state = parsed.0; diff --git a/src/utils/buttons.rs b/src/utils/buttons.rs index 20bd1c8..5439d0b 100644 --- a/src/utils/buttons.rs +++ b/src/utils/buttons.rs @@ -68,9 +68,9 @@ impl ButtonsHandler { pub async fn run( &mut self, state: &GlobalState, - //button_input: &Input<'static>, - button_inputs: &[Input<'static>], - //button_reg: &adv_shift_registers::wrappers::ShifterValue, + #[cfg(feature = "v4")] button_inputs: &[Input<'static>], + #[cfg(feature = "v3")] button_input: &Input<'static>, + #[cfg(feature = "v3")] button_reg: &adv_shift_registers::wrappers::ShifterValue, ) { let mut debounce_time = esp_hal::time::Instant::now(); let mut old_debounced = i32::MAX; @@ -105,16 +105,16 @@ impl ButtonsHandler { } } + #[cfg(feature = "v4")] { for i in 0..4 { if button_inputs[i].is_high() { - // if button_input.is_high() { out_val |= 1 << i; } } } - /* + #[cfg(feature = "v3")] { let mut val = 0b10000000; for i in 0..4 { @@ -126,7 +126,6 @@ impl ButtonsHandler { val >>= 1; } } - */ if old_val != out_val { old_val = out_val; diff --git a/src/utils/stackmat.rs b/src/utils/stackmat.rs index d48a2be..8d2b623 100644 --- a/src/utils/stackmat.rs +++ b/src/utils/stackmat.rs @@ -1,4 +1,6 @@ +#[cfg(feature = "v3")] pub const DEC_DIGITS: [u8; 10] = [215, 132, 203, 206, 156, 94, 95, 196, 223, 222]; +#[cfg(feature = "v3")] pub const DOT_MOD: u8 = 32; pub fn parse_stackmat_data(data: &[u8; 8]) -> Result<(StackmatTimerState, u64), ()> { @@ -84,6 +86,7 @@ pub fn generate_stackmat_data(state: &StackmatTimerState, time_ms: u64) -> [u8; data } +#[cfg(feature = "v3")] pub fn time_str_to_display(time: &str) -> [u8; 6] { let mut data = [255; 6]; let mut i = 0; From 3b6de1515bd8413fe3caca64fc8a8e6243756ab7 Mon Sep 17 00:00:00 2001 From: filipton Date: Wed, 11 Mar 2026 21:47:35 +0100 Subject: [PATCH 09/31] wip: working? v3 feature --- .github/workflows/rust_ci.yaml | 7 +- Cargo.lock | 17 + Cargo.toml | 7 +- src/battery_v3.rs | 130 +++++++ src/board.rs | 96 ++++- src/lcd_v3.rs | 693 +++++++++++++++++++++++++++++++++ src/lcd_v4.rs | 80 ++-- src/main.rs | 18 + src/rfid.rs | 2 - src/utils/lcd_abstract.rs | 42 +- src/utils/mod.rs | 8 +- 11 files changed, 1029 insertions(+), 71 deletions(-) create mode 100644 src/battery_v3.rs create mode 100644 src/lcd_v3.rs diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml index 1a44426..2ae502b 100644 --- a/.github/workflows/rust_ci.yaml +++ b/.github/workflows/rust_ci.yaml @@ -25,6 +25,11 @@ jobs: args: --all -- --check --color always - command: clippy args: --no-default-features --target=riscv32imc-unknown-none-elf --workspace -- -D warnings + features: [v3, v4] + exclude: + - action: + command: fmt + features: v4 steps: - name: Checkout repository uses: actions/checkout@v4 @@ -37,4 +42,4 @@ jobs: - name: Enable caching uses: Swatinem/rust-cache@v2 - name: Run command - run: cargo ${{ matrix.action.command }} ${{ matrix.action.args }} + run: cargo ${{ matrix.action.command }} ${{ matrix.action.args }}${{ matrix.action.command != 'fmt' && format(' --features {0}', matrix.features) || '' }} diff --git a/Cargo.lock b/Cargo.lock index eaed00e..8c3de96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "ag-lcd-async" +version = "0.3.0" +source = "git+https://github.com/filipton/ag-lcd-async#c4834c24769bfb0c5c597c71c675b9f0b6305bf3" +dependencies = [ + "embedded-hal 1.0.0", + "embedded-hal-async", +] + [[package]] name = "ahash" version = "0.8.12" @@ -636,6 +645,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dyn-smooth" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47fac87554ece38a037e0d53e750ee5129670b6f97e712bb0c411268181f3700" + [[package]] name = "ecdsa" version = "0.16.9" @@ -1527,12 +1542,14 @@ name = "fkm-firmware" version = "0.1.0" dependencies = [ "adv-shift-registers", + "ag-lcd-async", "anyhow", "bq27441", "critical-section", "display-interface", "display-interface-i2c", "dotenvy", + "dyn-smooth", "embassy-executor", "embassy-futures", "embassy-net", diff --git a/Cargo.toml b/Cargo.toml index 3246fa9..bc0274a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ esp-hal-mfrc522 = { version = "0.3.3", features = ["embassy-time"] } esp-bootloader-esp-idf = { version = "0.4.0", features = ["log-04", "esp32c3"] } trouble-host = { version = "0.5.1", features = ["scan", "security"] } +# v4 only display-interface = { version = "0.5.0", optional = true } display-interface-i2c = { version = "0.5.0", optional = true } embedded-graphics = { version = "0.8.2", optional = true } @@ -54,6 +55,10 @@ embedded-graphics-framebuf = { version = "0.5.0", optional = true } oled_async = { version = "0.2.0", features = ["i2c"], optional = true } bq27441 = { version = "0.1.0", features = ["async", "embassy"], optional = true } +#v3 only +dyn-smooth = { version = "0.2.0", optional = true } +ag-lcd-async = { git = "https://github.com/filipton/ag-lcd-async", features = [], optional = true } + [patch.crates-io] elliptic-curve = { git = "https://github.com/filipton/rust-crypto-traits.git" } @@ -66,7 +71,7 @@ e2e = [] qa = [] sleep = [] auto_add = [] -v3 = [] +v3 = ["dep:dyn-smooth", "dep:ag-lcd-async"] v4 = ["dep:display-interface", "dep:display-interface-i2c", "dep:embedded-graphics", "dep:embedded-graphics-framebuf", "dep:oled_async", "dep:bq27441"] [profile.dev] diff --git a/src/battery_v3.rs b/src/battery_v3.rs new file mode 100644 index 0000000..93523c5 --- /dev/null +++ b/src/battery_v3.rs @@ -0,0 +1,130 @@ +use crate::{ + consts::BATTERY_SEND_INTERVAL_MS, state::sleep_state, utils::rolling_average::RollingAverage, +}; +use embassy_time::{Duration, Instant, Timer}; +use esp_hal::analog::adc::{Adc, AdcConfig, Attenuation}; + +type AdcCal = esp_hal::analog::adc::AdcCalCurve>; + +const BATTERY_CURVE: [(f64, u8); 11] = [ + (3350.0, 0), + (3400.0, 13), + (3450.0, 19), + (3500.0, 25), + (3550.0, 31), + (3600.0, 38), + (3700.0, 50), + (3800.0, 63), + (3900.0, 75), + (4000.0, 88), + (4100.0, 100), +]; +const BAT_MIN: f64 = BATTERY_CURVE[0].0; +const BAT_MAX: f64 = BATTERY_CURVE[BATTERY_CURVE.len() - 1].0; + +#[embassy_executor::task] +pub async fn battery_read_task( + adc_pin: esp_hal::peripherals::GPIO2<'static>, + adc: esp_hal::peripherals::ADC1<'static>, + state: crate::state::GlobalState, +) { + let mut adc_config = AdcConfig::new(); + + let mut adc_pin = adc_config.enable_pin_with_cal::<_, AdcCal>(adc_pin, Attenuation::_11dB); + let mut adc = Adc::new(adc, adc_config).into_async(); + + let mut battery_start = Instant::now().saturating_add(Duration::from_millis(300)); + + let base_freq = 2.0; + let sample_freq = 1000.0; + let sensitivity = 0.5; + let mut smoother = dyn_smooth::DynamicSmootherEcoF32::new(base_freq, sample_freq, sensitivity); + let mut avg = RollingAverage::<128>::new(); + let mut lcd_sent = false; + + let mut sample_rate_millis = 10; + loop { + Timer::after_millis(sample_rate_millis).await; + if sleep_state() { + Timer::after_millis(500).await; + continue; + } + + let read = adc.read_oneshot(&mut adc_pin).await; + let read = smoother.tick(read as f32); + avg.push(read); + + #[cfg(feature = "bat_dev_lcd")] + { + let mut state = state.state.lock().await; + state.current_bat_read = Some(read); + } + + let now = Instant::now(); + if !lcd_sent && battery_start <= now { + state + .show_battery + .signal(bat_percentage(calculate(read as f64))); + + lcd_sent = true; + sample_rate_millis = 100; + } + + if battery_start > now || (now - battery_start).as_millis() < BATTERY_SEND_INTERVAL_MS { + continue; + } + + battery_start = Instant::now(); + let bat_calc_mv = calculate(read as f64); + let bat_percentage = bat_percentage(bat_calc_mv); + + if state.state.lock().await.server_connected == Some(true) { + crate::ws::send_packet(crate::structs::TimerPacket { + tag: None, + data: crate::structs::TimerPacketInner::Battery { + level: Some(bat_percentage as f64), + voltage: Some(bat_calc_mv / 1000.0), + }, + }) + .await; + } + + log::info!("calc({read}): {bat_calc_mv}mV {bat_percentage}%"); + #[cfg(feature = "bat_dev_lcd")] + { + let mut state = state.state.lock().await; + state.avg_bat_read = avg.average(); + } + } +} + +fn interpolate(v1: f64, p1: u8, v2: f64, p2: u8, voltage: f64) -> u8 { + let percentage = p1 as f64 + (voltage - v1) * (p2 as f64 - p1 as f64) / (v2 - v1); + percentage as u8 +} + +fn bat_percentage(mv: f64) -> u8 { + if mv <= BAT_MIN { + return 0; + } + if mv >= BAT_MAX { + return 100; + } + + // Find the two closest voltage points in our curve + for window in BATTERY_CURVE.windows(2) { + let (v1, p1) = window[0]; + let (v2, p2) = window[1]; + + if mv >= v1 && mv <= v2 { + return interpolate(v1, p1, v2, p2, mv); + } + } + + // Fallback to linear interpolation if something goes wrong + ((mv - BAT_MIN) / (BAT_MAX - BAT_MIN) * 100.0) as u8 +} + +fn calculate(x: f64) -> f64 { + 1.69874 * x + 66.6103 +} diff --git a/src/board.rs b/src/board.rs index 2446996..35a1402 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1,11 +1,9 @@ use esp_hal::{ gpio::{AnyPin, Input, InputConfig, Output, Pin, Pull}, - i2c::master::I2c, peripherals::{ ADC1, AES, BT, FLASH, Peripherals, SPI2, SW_INTERRUPT, TIMG0, TIMG1, UART1, WIFI, }, rng::Rng, - time::Rate, timer::timg::TimerGroup, }; @@ -68,6 +66,7 @@ pub struct Board { pub usb_dm: AnyPin<'static>, } +#[cfg(feature = "v4")] impl Board { pub fn init(peripherals: Peripherals) -> Board { let timg0 = TimerGroup::new(peripherals.TIMG0); @@ -113,9 +112,10 @@ impl Board { InputConfig::default().with_pull(Pull::Down), ); - let Ok(i2c) = I2c::new( + let Ok(i2c) = esp_hal::i2c::master::I2c::new( peripherals.I2C0, - esp_hal::i2c::master::Config::default().with_frequency(Rate::from_khz(400)), + esp_hal::i2c::master::Config::default() + .with_frequency(esp_hal::time::Rate::from_khz(400)), ) else { log::error!("Rfid task error while creating Spi instance!"); panic!() @@ -151,3 +151,91 @@ impl Board { } } } + +#[cfg(feature = "v3")] +impl Board { + pub fn init(peripherals: Peripherals) -> Board { + use embedded_hal::digital::OutputPin; + use esp_hal::gpio::Level; + + let timg0 = TimerGroup::new(peripherals.TIMG0); + let timg1 = TimerGroup::new(peripherals.TIMG1); + let rng = Rng::new(); + let uart1 = peripherals.UART1; + let spi2 = peripherals.SPI2; + let spi_dma = peripherals.DMA_CH0; + let adc1 = peripherals.ADC1; + let wifi = peripherals.WIFI; + let bt = peripherals.BT; + let aes = peripherals.AES; + let flash = peripherals.FLASH; + let sw_interrupt = peripherals.SW_INTERRUPT; + + let sck = peripherals.GPIO4.degrade(); + let miso = peripherals.GPIO5.degrade(); + let mosi = peripherals.GPIO6.degrade(); + let battery = peripherals.GPIO2; + let stackmat_rx = peripherals.GPIO20.degrade(); + let usb_dp = peripherals.GPIO19.degrade(); + let usb_dm = peripherals.GPIO18.degrade(); + + let button_input = Input::new( + peripherals.GPIO3, + InputConfig::default().with_pull(Pull::Down), + ); + + let shifter_data_pin = Output::new(peripherals.GPIO10, Level::Low, Default::default()); + let shifter_latch_pin = Output::new(peripherals.GPIO1, Level::Low, Default::default()); + let shifter_clk_pin = Output::new(peripherals.GPIO21, Level::Low, Default::default()); + + let adv_shift_reg = adv_shift_registers::AdvancedShiftRegister::<8, _>::new( + shifter_data_pin, + shifter_clk_pin, + shifter_latch_pin, + 0, + ); + let adv_shift_reg = alloc::boxed::Box::new(adv_shift_reg); + let adv_shift_reg = alloc::boxed::Box::leak(adv_shift_reg); + + let mut backlight = adv_shift_reg.get_pin_mut(1, 1, false); + _ = backlight.set_high(); + + let buttons_shifter = adv_shift_reg.get_shifter_mut(0); + let lcd = adv_shift_reg.get_shifter_mut(1); + let digits_shifters = adv_shift_reg.get_shifter_range_mut(2..8); + digits_shifters.set_data(&[!DEC_DIGITS[8] ^ DOT_MOD; 6]); + + let mut cs = adv_shift_reg.get_pin_mut(1, 0, true); + _ = cs.set_high(); + + Board { + timg0, + timg1, + rng, + uart1, + spi2, + spi_dma, + adc1, + wifi, + bt, + aes, + flash, + sw_interrupt, + + miso, + mosi, + sck, + cs, + + battery, + stackmat_rx, + button_input, + + buttons_shifter, + digits_shifters, + lcd, + usb_dp, + usb_dm, + } + } +} diff --git a/src/lcd_v3.rs b/src/lcd_v3.rs new file mode 100644 index 0000000..d742682 --- /dev/null +++ b/src/lcd_v3.rs @@ -0,0 +1,693 @@ +use adv_shift_registers::wrappers::ShifterValueRange; +use ag_lcd_async::LcdDisplay; +use alloc::{rc::Rc, string::ToString}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; +use embassy_time::{Delay, Duration, Instant, Timer}; +use embedded_hal::digital::OutputPin; +use embedded_hal_async::delay::DelayNs; + +use crate::{ + consts::{ + DEEPER_SLEEP_AFTER_MS, INSPECTION_TIME_PLUS2, LCD_INSPECTION_FRAME_TIME, + SCROLL_TICKER_INVERVAL_MS, SLEEP_AFTER_MS, + }, + state::{ + GlobalState, MenuScene, Scene, SignaledGlobalStateInner, deeper_sleep_state, sleep_state, + }, + translations::{TranslationKey, get_translation, get_translation_params}, + utils::{ + lcd_abstract::{LcdAbstract, PrintAlign}, + stackmat::ms_to_time_str, + }, +}; + +#[embassy_executor::task] +pub async fn lcd_task( + lcd_shifter: adv_shift_registers::wrappers::ShifterValue, + global_state: GlobalState, + wifi_setup_sig: Rc>, + display: ShifterValueRange, +) { + let mut lcd = { + let bl_pin = lcd_shifter.get_pin_mut(1, true); + let rs_pin = lcd_shifter.get_pin_mut(2, true); + let en_pin = lcd_shifter.get_pin_mut(3, true); + let d4_pin = lcd_shifter.get_pin_mut(4, false); + let d5_pin = lcd_shifter.get_pin_mut(5, false); + let d6_pin = lcd_shifter.get_pin_mut(6, false); + let d7_pin = lcd_shifter.get_pin_mut(7, false); + LcdDisplay::new(rs_pin, en_pin, Delay) + .with_display(ag_lcd_async::Display::On) + .with_blink(ag_lcd_async::Blink::Off) + .with_cursor(ag_lcd_async::Cursor::Off) + .with_size(ag_lcd_async::Size::Dots5x8) + .with_cols(16) + .with_lines(ag_lcd_async::Lines::TwoLines) + .with_half_bus(d4_pin, d5_pin, d6_pin, d7_pin) + .with_backlight(bl_pin) + .build() + .await + }; + + lcd.clear().await; + lcd.backlight_on(); + + let mut lcd_driver: LcdAbstract<80, 16, 2, 3> = LcdAbstract::new(); + + _ = lcd_driver.print( + 0, + &alloc::format!("{:X}", crate::utils::get_efuse_u32()), + PrintAlign::Left, + true, + ); + _ = lcd_driver.print(1, crate::version::VERSION, PrintAlign::Center, true); + lcd_driver.display_on_lcd(&mut lcd).await; + + _ = lcd_driver.print( + 0, + &alloc::format!("{}%", global_state.show_battery.wait().await), + PrintAlign::Right, + false, + ); + lcd_driver.display_on_lcd(&mut lcd).await; + + #[cfg(not(feature = "bat_dev_lcd"))] + Timer::after_millis(2500).await; + + _ = lcd_driver.clear_all(); + let mut last_update; + loop { + let current_state = global_state.state.value().await.clone(); + log::debug!("lcd current_state: {current_state:?}"); + last_update = Instant::now(); + + if sleep_state() { + lcd.backlight_on(); + + unsafe { + crate::state::SLEEP_STATE = false; + } + } + + let current_scene = current_state.scene.clone(); + let fut = async { + let _ = process_lcd( + current_state, + &global_state, + &mut lcd_driver, + &mut lcd, + &wifi_setup_sig, + &display, + ) + .await; + lcd_driver.display_on_lcd(&mut lcd).await; + + let mut scroll_ticker = + embassy_time::Ticker::every(Duration::from_millis(SCROLL_TICKER_INVERVAL_MS)); + loop { + scroll_ticker.next().await; + let changed = lcd_driver.scroll_step(); + if changed.is_ok_and(|c| c) { + lcd_driver.display_on_lcd(&mut lcd).await; + } + + #[cfg(not(any(feature = "e2e", feature = "qa")))] + if !sleep_state() + && (Instant::now() - last_update).as_millis() > SLEEP_AFTER_MS + && current_scene.can_sleep() + { + _ = lcd_driver.print(0, "Sleep", PrintAlign::Center, true); + _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); + lcd_driver.display_on_lcd(&mut lcd).await; + lcd.backlight_off(); + + { + global_state.state.lock().await.server_connected = Some(false); + } + + unsafe { + crate::state::SLEEP_STATE = true; + crate::state::TRUST_SERVER = false; + } + + global_state.state.signal_reset(); + } + + #[cfg(not(any(feature = "e2e", feature = "qa")))] + if sleep_state() + && !deeper_sleep_state() + && (Instant::now() - last_update).as_millis() > DEEPER_SLEEP_AFTER_MS + { + _ = lcd_driver.print(0, "Deep Sleep", PrintAlign::Center, true); + _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); + lcd_driver.display_on_lcd(&mut lcd).await; + crate::utils::deeper_sleep(); + } + } + }; + + let res = embassy_futures::select::select(fut, global_state.state.wait()).await; + match res { + embassy_futures::select::Either::First(_) => {} + embassy_futures::select::Either::Second(_) => { + continue; + } + } + } +} + +async fn process_lcd( + current_state: SignaledGlobalStateInner, + global_state: &GlobalState, + lcd_driver: &mut LcdAbstract<80, 16, 2, 3>, + lcd: &mut LcdDisplay, + wifi_setup_sig: &Signal, + display: &ShifterValueRange, +) -> Option<()> { + #[cfg(feature = "bat_dev_lcd")] + { + let battery_read = current_state.current_bat_read.unwrap_or(-1.0); + lcd_driver + .print( + 0, + &alloc::format!("BAT: {battery_read}"), + PrintAlign::Left, + true, + ) + .ok()?; + + if let Some(avg) = current_state.avg_bat_read { + lcd_driver + .print(1, &alloc::format!("AVG: {avg}"), PrintAlign::Left, true) + .ok()?; + } + + return Some(()); + } + + if let Some(error_text) = current_state.error_text { + lcd_driver + .print( + 0, + &get_translation(TranslationKey::ERROR_HEADER), + PrintAlign::Center, + true, + ) + .ok()?; + + lcd_driver + .print(1, &error_text, PrintAlign::Center, true) + .ok()?; + + return Some(()); + } + + // display custom message on top of everything! + if let Some((line1, line2)) = ¤t_state.custom_message { + _ = lcd_driver.print(0, line1, PrintAlign::Center, true); + _ = lcd_driver.print(1, line2, PrintAlign::Center, true); + + return Some(()); + } + + if let Some(sel) = current_state.selected_config_menu { + lcd_driver.clear_all().ok()?; + lcd_driver.print(0, "<", PrintAlign::Left, false).ok()?; + lcd_driver.print(0, ">", PrintAlign::Right, false).ok()?; + + lcd_driver + .print(0, "Config Menu", PrintAlign::Center, false) + .ok()?; + + lcd_driver + .print( + 1, + &alloc::format!("{}. {}", sel + 1, crate::structs::CONFIG_MENU_ITEMS[sel]), + PrintAlign::Left, + true, + ) + .ok()?; + + return Some(()); + } + + match current_state.menu_scene { + Some(MenuScene::Signing) | Some(MenuScene::Unsigning) => { + let prefix = if current_state.menu_scene == Some(MenuScene::Signing) { + "S" + } else { + "Uns" + }; + + lcd_driver.clear_all().ok()?; + lcd_driver + .print( + 0, + &alloc::format!("{prefix}igning | Submit To Exit"), + PrintAlign::Left, + true, + ) + .ok()?; + + if global_state.sign_unsign_progress.signaled() { + let status = if global_state.sign_unsign_progress.wait().await { + "OK" + } else { + "FAIL" + }; + + lcd_driver + .print( + 1, + &alloc::format!("Operation {}", status), + PrintAlign::Center, + true, + ) + .ok()?; + lcd_driver.display_on_lcd(lcd).await; + + Timer::after_millis(300).await; + lcd_driver + .print(1, "Scan the card", PrintAlign::Center, true) + .ok()?; + } else { + lcd_driver + .print(1, "Scan the card", PrintAlign::Center, true) + .ok()?; + } + + return Some(()); + } + Some(crate::state::MenuScene::BtDisplay) => { + lcd_driver.clear_all().ok()?; + if current_state.selected_bluetooth_item + == current_state.discovered_bluetooth_devices.len() + { + lcd_driver + .print(0, "Unpair", PrintAlign::Center, true) + .ok()?; + } else if current_state.selected_bluetooth_item + == current_state.discovered_bluetooth_devices.len() + 1 + { + lcd_driver.print(0, "Exit", PrintAlign::Center, true).ok()?; + } else if current_state.selected_bluetooth_item + < current_state.discovered_bluetooth_devices.len() + { + if let Some(display_dev) = current_state + .discovered_bluetooth_devices + .get(current_state.selected_bluetooth_item) + { + lcd_driver + .print(0, &display_dev.name, PrintAlign::Center, true) + .ok()?; + + lcd_driver + .print( + 1, + &alloc::format!("{:x?}", display_dev.addr), + PrintAlign::Center, + true, + ) + .ok()?; + } + } else { + global_state.state.lock().await.selected_bluetooth_item = 0; + } + + return Some(()); + } + None => {} + } + + let overwritten = process_lcd_overwrite(¤t_state, global_state, lcd_driver).await; + if overwritten { + return Some(()); + } + + lcd_driver.clear_all().ok()?; + if let Some(time) = current_state.delegate_hold { + let delegate_remaining = 3 - time; + + if delegate_remaining == 0 { + lcd_driver + .print( + 0, + &get_translation(TranslationKey::WAITING_FOR_DELEGATE_HEADER), + PrintAlign::Center, + true, + ) + .ok()?; + + lcd_driver + .print( + 1, + &get_translation(TranslationKey::WAITING_FOR_DELEGATE_FOOTER), + PrintAlign::Center, + true, + ) + .ok()?; + } else { + lcd_driver + .print( + 0, + &get_translation(TranslationKey::CALLING_FOR_DELEGATE_HEADER), + PrintAlign::Center, + true, + ) + .ok()?; + + lcd_driver + .print( + 1, + &get_translation_params( + TranslationKey::CALLING_FOR_DELEGATE_FOOTER, + &[delegate_remaining], + ), + PrintAlign::Center, + true, + ) + .ok()?; + } + + return Some(()); + } + + match current_state.scene { + Scene::WifiConnect => { + lcd_driver + .print( + 0, + &get_translation(TranslationKey::WAITING_FOR_WIFI_HEADER), + PrintAlign::Center, + true, + ) + .ok()?; + + lcd_driver + .print( + 1, + &get_translation(TranslationKey::WAITING_FOR_WIFI_FOOTER), + PrintAlign::Center, + true, + ) + .ok()?; + + lcd_driver.display_on_lcd(lcd).await; + wifi_setup_sig.wait().await; + global_state.state.lock().await.scene = Scene::AutoSetupWait; + } + Scene::AutoSetupWait => { + let wifi_ssid = alloc::format!("FKM-{:X}", crate::utils::get_efuse_u32()); + lcd_driver + .print( + 0, + &get_translation(TranslationKey::WIFI_SETUP_HEADER), + PrintAlign::Center, + true, + ) + .ok()?; + + lcd_driver + .print(1, &wifi_ssid, PrintAlign::Center, true) + .ok()?; + } + Scene::MdnsWait => { + lcd_driver + .print( + 0, + &get_translation(TranslationKey::WAITING_FOR_MDNS_HEADER), + PrintAlign::Center, + true, + ) + .ok()?; + + lcd_driver + .print( + 1, + &get_translation(TranslationKey::WAITING_FOR_MDNS_FOOTER), + PrintAlign::Center, + true, + ) + .ok()?; + } + Scene::GroupSelect => { + lcd_driver.clear_all().ok()?; + lcd_driver.print(0, "<", PrintAlign::Left, false).ok()?; + lcd_driver.print(1, "<", PrintAlign::Left, false).ok()?; + lcd_driver.print(0, ">", PrintAlign::Right, false).ok()?; + lcd_driver.print(1, ">", PrintAlign::Right, false).ok()?; + + lcd_driver + .print( + 0, + &get_translation(TranslationKey::SELECT_GROUP), + PrintAlign::Center, + false, + ) + .ok()?; + + lcd_driver + .print( + 1, + ¤t_state.possible_groups[current_state.group_selected_idx].secondary_text, + PrintAlign::Center, + false, + ) + .ok()?; + } + Scene::WaitingForCompetitor => { + lcd_driver + .print( + 0, + &get_translation(TranslationKey::SCAN_COMPETITOR_CARD_HEADER), + PrintAlign::Center, + true, + ) + .ok()?; + + if let Some(solve_time) = current_state.solve_time { + let time_str = ms_to_time_str(solve_time); + lcd_driver + .print( + 1, + &get_translation_params( + TranslationKey::SCAN_COMPETITOR_CARD_WITH_TIME_FOOTER, + &[time_str], + ), + PrintAlign::Center, + true, + ) + .ok()?; + } else { + lcd_driver + .print( + 1, + &get_translation(TranslationKey::SCAN_COMPETITOR_CARD_FOOTER), + PrintAlign::Center, + true, + ) + .ok()?; + } + } + Scene::CompetitorInfo => { + lcd_driver + .print( + 0, + ¤t_state + .competitor_display + .unwrap_or("??????".to_string()), + PrintAlign::Center, + true, + ) + .ok()?; + + if let Some(group) = current_state.solve_group { + lcd_driver + .print(1, &group.secondary_text, PrintAlign::Center, true) + .ok()?; + } + } + Scene::Inspection => { + let inspection_start = global_state + .state + .value() + .await + .inspection_start + .unwrap_or(Instant::now()); + + lcd_driver + .print(1, "Inspection", PrintAlign::Center, true) + .ok()?; + + loop { + let elapsed = (Instant::now() - inspection_start).as_millis(); + let time_str = ms_to_time_str(elapsed); + + lcd_driver + .print(0, &time_str, PrintAlign::Center, true) + .ok()?; + + lcd_driver.display_on_lcd(lcd).await; + Timer::after_millis(LCD_INSPECTION_FRAME_TIME).await; + } + } + Scene::Timer => loop { + let time = global_state.timer_signal.wait().await; + let time_str = ms_to_time_str(time); + lcd_driver + .print(0, &time_str, PrintAlign::Center, true) + .ok()?; + + display.set_data_raw(&crate::utils::stackmat::time_str_to_display(&time_str)); + lcd_driver.display_on_lcd(lcd).await; + }, + Scene::Finished => { + let solve_time = current_state.solve_time.unwrap_or(0); + let time_str = if solve_time > 0 { + ms_to_time_str(solve_time) + } else { + heapless::String::new() + }; + + let inspection_time = + match (current_state.inspection_start, current_state.inspection_end) { + (Some(start), Some(end)) => { + Some(end.saturating_duration_since(start).as_millis()) + } + _ => None, + }; + + if current_state.use_inspection() + && inspection_time.unwrap_or(0) > INSPECTION_TIME_PLUS2 + { + let inspections_seconds = inspection_time.unwrap_or(0) / 1000; + lcd_driver + .print( + 0, + &alloc::format!("{time_str} ({inspections_seconds}s)"), + PrintAlign::Left, + true, + ) + .ok()?; + } else { + lcd_driver + .print(0, &time_str, PrintAlign::Left, true) + .ok()?; + } + + let penalty = current_state.penalty.unwrap_or(0); + let penalty_str = match penalty { + -2 => "DNS", + -1 => "DNF", + 1.. => &alloc::format!("+{penalty}"), + _ => "", + }; + + lcd_driver + .print(0, penalty_str, PrintAlign::Right, false) + .ok()?; + + if !current_state.time_confirmed { + lcd_driver + .print( + 1, + &get_translation(TranslationKey::CONFIRM_TIME), + PrintAlign::Right, + true, + ) + .ok()?; + } else if current_state.current_judge.is_none() { + lcd_driver + .print( + 1, + &get_translation(TranslationKey::SCAN_JUDGE_CARD), + PrintAlign::Right, + true, + ) + .ok()?; + } else if current_state.current_competitor.is_some() + && current_state.current_judge.is_some() + { + lcd_driver + .print( + 1, + &get_translation(TranslationKey::SCAN_COMPETITOR_CARD), + PrintAlign::Right, + true, + ) + .ok()?; + } + } + Scene::Update => { + _ = lcd_driver.print(0, "Updating...", PrintAlign::Center, true); + loop { + let progress = global_state.update_progress.wait().await; + _ = lcd_driver.print(1, &alloc::format!("{progress}%"), PrintAlign::Center, true); + + lcd_driver.display_on_lcd(lcd).await; + } + } + } + + Some(()) +} + +async fn process_lcd_overwrite( + current_state: &SignaledGlobalStateInner, + _global_state: &GlobalState, + lcd_driver: &mut LcdAbstract<80, 16, 2, 3>, +) -> bool { + if !current_state.scene.can_be_lcd_overwritten() { + return false; + } + + if current_state.server_connected == Some(false) { + if current_state.wifi_conn_lost { + // TODO: maybe add to this translation + _ = lcd_driver.print(0, "Wi-Fi", PrintAlign::Center, true); + _ = lcd_driver.print(1, "Connection lost", PrintAlign::Center, true); + } else { + _ = lcd_driver.print( + 0, + &get_translation(TranslationKey::SERVER_DISCONNECTED_HEADER), + PrintAlign::Center, + true, + ); + _ = lcd_driver.print( + 1, + &get_translation(TranslationKey::SERVER_DISCONNECTED_FOOTER), + PrintAlign::Center, + true, + ); + } + } else if current_state.device_added == Some(false) { + #[cfg(not(feature = "e2e"))] + let lines = ( + &get_translation(TranslationKey::DEVICE_NOT_ADDED_HEADER), + &get_translation(TranslationKey::DEVICE_NOT_ADDED_FOOTER), + ); + + #[cfg(feature = "e2e")] + let lines = ("Press submit", "To start HIL"); + + _ = lcd_driver.print(0, lines.0, PrintAlign::Center, true); + _ = lcd_driver.print(1, lines.1, PrintAlign::Center, true); + } else if current_state.stackmat_connected == Some(false) { + _ = lcd_driver.print( + 0, + &get_translation(TranslationKey::STACKMAT_DISCONNECTED_HEADER), + PrintAlign::Center, + true, + ); + _ = lcd_driver.print( + 1, + &get_translation(TranslationKey::STACKMAT_DISCONNECTED_FOOTER), + PrintAlign::Center, + true, + ); + } else { + return false; + } + + true +} diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index d501a23..fcf03cb 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -1,4 +1,3 @@ -use adv_shift_registers::wrappers::ShifterValueRange; use alloc::{rc::Rc, string::ToString}; use display_interface_i2c::I2CInterface; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; @@ -22,21 +21,15 @@ use crate::{ GlobalState, MenuScene, Scene, SignaledGlobalStateInner, deeper_sleep_state, sleep_state, }, translations::{TranslationKey, get_translation, get_translation_params}, - utils::{ - lcd_abstract::{LcdAbstract, PrintAlign}, - shared_i2c::SharedI2C, - stackmat::ms_to_time_str, - }, + utils::{shared_i2c::SharedI2C, stackmat::ms_to_time_str}, }; #[embassy_executor::task] pub async fn lcd_task( i2c: SharedI2C, mut display_rst: Output<'static>, - //lcd_shifter: adv_shift_registers::wrappers::ShifterValue, global_state: GlobalState, wifi_setup_sig: Rc>, - //display: ShifterValueRange, ) { let di = display_interface_i2c::I2CInterface::new(i2c, 0x3C, 0x40); let raw_disp = @@ -53,38 +46,63 @@ pub async fn lcd_task( let mut data = alloc::vec![embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 64) as usize]; - let data = data.as_mut_array().unwrap(); + let data: &mut [BinaryColor; 128 * 64] = data.as_mut_array().unwrap(); let mut fbuf = embedded_graphics_framebuf::FrameBuf::new(data, 128, 64); - let mut lcd_driver: LcdAbstract<80, 16, 2, 3> = LcdAbstract::new(); - _ = lcd_driver.print( - 0, - &alloc::format!("{:X}", crate::utils::get_efuse_u32()), - PrintAlign::Left, - true, - ); - _ = lcd_driver.print(1, crate::version::VERSION, PrintAlign::Center, true); - fbuf.clear(BinaryColor::Off); - lcd_driver.display_on_oled(&mut fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); + let start = Instant::now(); + loop { + embedded_graphics::prelude::DrawTarget::clear( + &mut fbuf, + embedded_graphics::pixelcolor::BinaryColor::Off, + ); + Timer::after(embassy_time::Duration::from_millis(1000 / 60)).await; + + let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() + .font(&embedded_graphics::mono_font::ascii::FONT_6X10) + .text_color(embedded_graphics::pixelcolor::BinaryColor::On) + .build(); + + let time_str = crate::utils::stackmat::ms_to_time_str(start.elapsed().as_millis()); + embedded_graphics::Drawable::draw( + &embedded_graphics::text::Text::with_baseline( + &alloc::format!("Hello world! {time_str}"), + embedded_graphics::prelude::Point::zero(), + text_style, + embedded_graphics::text::Baseline::Top, + ), + &mut fbuf, + ) + .unwrap(); + + embedded_graphics::Drawable::draw( + &embedded_graphics::text::Text::with_baseline( + "Hello Rust!", + embedded_graphics::prelude::Point::new(0, 16), + text_style, + embedded_graphics::text::Baseline::Top, + ), + &mut fbuf, + ) + .unwrap(); + + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); + + if start.elapsed().as_secs() > 120 { + break; + } + } - _ = lcd_driver.print( - 0, - &alloc::format!("{}%", global_state.show_battery.wait().await), - PrintAlign::Right, - false, - ); fbuf.clear(BinaryColor::Off); - lcd_driver.display_on_oled(&mut fbuf).await; + //lcd_driver.display_on_oled(&mut fbuf).await; embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); disp.flush().await.unwrap(); Timer::after_millis(2500).await; fbuf.clear(BinaryColor::Off); - _ = lcd_driver.clear_all(); + //_ = lcd_driver.clear_all(); embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); disp.flush().await.unwrap(); @@ -104,6 +122,7 @@ pub async fn lcd_task( let current_scene = current_state.scene.clone(); let fut = async { + /* let _ = process_lcd( current_state, &global_state, @@ -172,6 +191,7 @@ pub async fn lcd_task( crate::utils::deeper_sleep(); } } + */ }; let res = embassy_futures::select::select(fut, global_state.state.wait()).await; @@ -184,6 +204,7 @@ pub async fn lcd_task( } } +/* async fn process_lcd( current_state: SignaledGlobalStateInner, global_state: &GlobalState, @@ -736,3 +757,4 @@ async fn process_lcd_overwrite( true } +*/ diff --git a/src/main.rs b/src/main.rs index 8a89b85..3ebde2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,11 @@ mod utils; mod version; mod ws; +#[cfg(feature = "v3")] +mod battery_v3; +#[cfg(feature = "v3")] +mod lcd_v3; + #[cfg(feature = "v4")] mod battery_v4; #[cfg(feature = "v4")] @@ -117,6 +122,13 @@ async fn main(spawner: Spawner) { unsafe { crate::state::SIGN_KEY = sign_key }; } + #[cfg(feature = "v3")] + spawner.must_spawn(lcd_v3::lcd_task( + board.lcd, + global_state.clone(), + wifi_setup_sig.clone(), + board.digits_shifters.clone(), + )); #[cfg(feature = "v4")] spawner.must_spawn(lcd_v4::lcd_task( board.i2c.clone(), @@ -125,6 +137,12 @@ async fn main(spawner: Spawner) { wifi_setup_sig.clone(), )); + #[cfg(feature = "v3")] + spawner.must_spawn(battery_v3::battery_read_task( + board.battery, + board.adc1, + global_state.clone(), + )); #[cfg(feature = "v4")] spawner.must_spawn(battery_v4::battery_read_task( board.i2c.clone(), diff --git a/src/rfid.rs b/src/rfid.rs index 4e20c65..58f9c6b 100644 --- a/src/rfid.rs +++ b/src/rfid.rs @@ -7,8 +7,6 @@ use anyhow::{Result, anyhow}; use embassy_time::{Duration, Instant, Timer}; use esp_hal_mfrc522::consts::UidSize; -#[cfg(feature = "v3")] -use esp_hal::i2c::master::I2c; #[cfg(feature = "v3")] use esp_hal::time::Rate; #[cfg(feature = "v3")] diff --git a/src/utils/lcd_abstract.rs b/src/utils/lcd_abstract.rs index 459bc5a..bc09ef5 100644 --- a/src/utils/lcd_abstract.rs +++ b/src/utils/lcd_abstract.rs @@ -1,11 +1,4 @@ -use alloc::{string::String, vec::Vec}; -use embedded_graphics::{ - Drawable, - pixelcolor::BinaryColor, - prelude::Point, - text::{Alignment, Text}, -}; -use embedded_graphics_framebuf::FrameBuf; +use ag_lcd_async::LcdDisplay; use embedded_hal::digital::OutputPin; use embedded_hal_async::delay::DelayNs; @@ -159,31 +152,16 @@ impl, - ) { + pub async fn display_on_lcd(&mut self, lcd: &mut LcdDisplay) { let display_data = self.display_data(); + for (y, line) in display_data.0.iter().enumerate() { + if line.1 { + lcd.set_position(0, y as u8).await; + lcd.print(unsafe { core::str::from_utf8_unchecked(line.0) }) + .await; - let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() - .font(&embedded_graphics::mono_font::ascii::FONT_6X10) - .text_color(embedded_graphics::pixelcolor::BinaryColor::On) - .build(); - - let text = display_data - .0 - .iter() - .map(|l| l.0.iter().map(|c| *c as char).collect::()) - .collect::>() - .join("\n"); - - Text::with_alignment( - &text, - Point::new(128 / 2, 26), - text_style, - Alignment::Center, - ) - .draw(fbuf) - .unwrap(); + display_data.1[y].copy_from_slice(line.0); + } + } } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 2231d1a..c3b5dc3 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,13 +1,17 @@ pub mod arc; pub mod backtrace_store; pub mod buttons; -pub mod lcd_abstract; pub mod logger; pub mod rolling_average; -pub mod shared_i2c; pub mod signaled_mutex; pub mod stackmat; +#[cfg(feature = "v3")] +pub mod lcd_abstract; + +#[cfg(feature = "v4")] +pub mod shared_i2c; + pub fn set_brownout_detection(state: bool) { unsafe { let rtc_cntl = &*esp32c3::RTC_CNTL::ptr(); From 5ffa8e31cbb472e66c974f1384b195a4a630ea05 Mon Sep 17 00:00:00 2001 From: filipton Date: Wed, 11 Mar 2026 21:52:39 +0100 Subject: [PATCH 10/31] fix: ci features flags for clippy --- .github/workflows/rust_ci.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml index 2ae502b..cd09d00 100644 --- a/.github/workflows/rust_ci.yaml +++ b/.github/workflows/rust_ci.yaml @@ -42,4 +42,14 @@ jobs: - name: Enable caching uses: Swatinem/rust-cache@v2 - name: Run command - run: cargo ${{ matrix.action.command }} ${{ matrix.action.args }}${{ matrix.action.command != 'fmt' && format(' --features {0}', matrix.features) || '' }} + run: | + ARGS="${{ matrix.action.args }}" + if [ "${{ matrix.action.command }}" != "fmt" ]; then + FEATURES="--features ${{ matrix.features }}" + if [[ "$ARGS" == *" -- "* ]]; then + ARGS="${ARGS/ -- / $FEATURES -- }" + else + ARGS="$ARGS $FEATURES" + fi + fi + cargo ${{ matrix.action.command }} $ARGS From 5165e03293569f0009c61a933f772bd00932b06a Mon Sep 17 00:00:00 2001 From: filipton Date: Wed, 11 Mar 2026 21:57:04 +0100 Subject: [PATCH 11/31] fix: actually fix ci --- .github/workflows/rust_ci.yaml | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml index cd09d00..e56f746 100644 --- a/.github/workflows/rust_ci.yaml +++ b/.github/workflows/rust_ci.yaml @@ -20,16 +20,15 @@ jobs: matrix: action: - command: build - args: --no-default-features --target=riscv32imc-unknown-none-elf --release + args: --no-default-features --features v3 --target=riscv32imc-unknown-none-elf --release + - command: build + args: --no-default-features --features v4 --target=riscv32imc-unknown-none-elf --release - command: fmt args: --all -- --check --color always - command: clippy - args: --no-default-features --target=riscv32imc-unknown-none-elf --workspace -- -D warnings - features: [v3, v4] - exclude: - - action: - command: fmt - features: v4 + args: --no-default-features --features v3 --target=riscv32imc-unknown-none-elf --workspace -- -D warnings + - command: clippy + args: --no-default-features --features v4 --target=riscv32imc-unknown-none-elf --workspace -- -D warnings steps: - name: Checkout repository uses: actions/checkout@v4 @@ -42,14 +41,4 @@ jobs: - name: Enable caching uses: Swatinem/rust-cache@v2 - name: Run command - run: | - ARGS="${{ matrix.action.args }}" - if [ "${{ matrix.action.command }}" != "fmt" ]; then - FEATURES="--features ${{ matrix.features }}" - if [[ "$ARGS" == *" -- "* ]]; then - ARGS="${ARGS/ -- / $FEATURES -- }" - else - ARGS="$ARGS $FEATURES" - fi - fi - cargo ${{ matrix.action.command }} $ARGS + run: cargo ${{ matrix.action.command }} ${{ matrix.action.args }} From 0ecda58b4c66bbba8498925383b34bf6c66c359e Mon Sep 17 00:00:00 2001 From: filipton Date: Wed, 11 Mar 2026 23:25:22 +0100 Subject: [PATCH 12/31] wip: test claude generated icons on top bar --- Cargo.lock | 10 +++ Cargo.toml | 3 +- src/lcd_v4.rs | 202 +++++++++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 1 - 4 files changed, 210 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c3de96..27d3e30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1583,6 +1583,7 @@ dependencies = [ "nb 1.1.0", "oled_async", "portable-atomic", + "profont", "rand_core 0.6.4", "serde", "serde_json", @@ -2247,6 +2248,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profont" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016681370a9dd6e7ddb4c1a959922fd59dc45e5ebaa5ff5b13090267898ced34" +dependencies = [ + "embedded-graphics", +] + [[package]] name = "quote" version = "1.0.44" diff --git a/Cargo.toml b/Cargo.toml index bc0274a..80a864f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ embedded-graphics = { version = "0.8.2", optional = true } embedded-graphics-framebuf = { version = "0.5.0", optional = true } oled_async = { version = "0.2.0", features = ["i2c"], optional = true } bq27441 = { version = "0.1.0", features = ["async", "embassy"], optional = true } +profont = { version = "0.7.0", optional = true } #v3 only dyn-smooth = { version = "0.2.0", optional = true } @@ -72,7 +73,7 @@ qa = [] sleep = [] auto_add = [] v3 = ["dep:dyn-smooth", "dep:ag-lcd-async"] -v4 = ["dep:display-interface", "dep:display-interface-i2c", "dep:embedded-graphics", "dep:embedded-graphics-framebuf", "dep:oled_async", "dep:bq27441"] +v4 = ["dep:display-interface", "dep:display-interface-i2c", "dep:embedded-graphics", "dep:embedded-graphics-framebuf", "dep:oled_async", "dep:bq27441", "dep:profont"] [profile.dev] opt-level = 3 diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index fcf03cb..61775a1 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -3,10 +3,12 @@ use display_interface_i2c::I2CInterface; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; use embassy_time::{Delay, Duration, Instant, Timer}; use embedded_graphics::{ - Drawable, + Drawable, Pixel, + mono_font::MonoTextStyle, pixelcolor::BinaryColor, - prelude::{DrawTarget, Point}, - text::{Alignment, Text}, + prelude::{Angle, DrawTarget, Point, Primitive, Size}, + primitives::{Arc, Circle, Line, PrimitiveStyle, Rectangle}, + text::{Alignment, Baseline, Text, TextStyleBuilder}, }; use embedded_graphics_framebuf::FrameBuf; use esp_hal::gpio::Output; @@ -47,9 +49,200 @@ pub async fn lcd_task( let mut data = alloc::vec![embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 64) as usize]; let data: &mut [BinaryColor; 128 * 64] = data.as_mut_array().unwrap(); - let mut fbuf = embedded_graphics_framebuf::FrameBuf::new(data, 128, 64); + let charging = true; + let wifi_connected = true; + let server_connected = true; + let timer_connected = true; + + let stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 1); + + // ── WiFi icon (arcs centered at x=5, y=9 so they arc upward above the bar) ─── + Pixel(Point::new(5, 8), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + + if wifi_connected { + // Inner arc + Arc::new( + Point::new(3, 7), + 4, + Angle::from_degrees(210.0), + Angle::from_degrees(120.0), + ) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + // Middle arc + Arc::new( + Point::new(1, 5), + 8, + Angle::from_degrees(210.0), + Angle::from_degrees(120.0), + ) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + // Outer arc + Arc::new( + Point::new(-1, 3), + 12, + Angle::from_degrees(210.0), + Angle::from_degrees(120.0), + ) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + } else { + Line::new(Point::new(1, 2), Point::new(9, 8)) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + Line::new(Point::new(9, 2), Point::new(1, 8)) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + } + + // ── Server icon (x=13..24, y=1..8, bottom at y=8 → 1px gap before rule) ────── + Rectangle::new(Point::new(13, 1), Size::new(12, 8)) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + Line::new(Point::new(14, 5), Point::new(23, 5)) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + Pixel(Point::new(22, 3), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + Pixel(Point::new(22, 7), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + + if !server_connected { + Line::new(Point::new(13, 1), Point::new(24, 8)) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + Line::new(Point::new(24, 1), Point::new(13, 8)) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + } + + // ── Timer / stopwatch icon (x=27..35, y=1..8) ──────────────────────────────── + // Button on top + Line::new(Point::new(30, 1), Point::new(32, 1)) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + // Round body: diameter=7 → top_left=(27,2), center=(30,5), bottom=y=8 + Circle::new(Point::new(27, 2), 7) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + // Clock hand pointing upper-right + Line::new(Point::new(30, 5), Point::new(32, 3)) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + + if !timer_connected { + Line::new(Point::new(27, 2), Point::new(33, 8)) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + Line::new(Point::new(33, 2), Point::new(27, 8)) + .into_styled(stroke) + .draw(&mut disp) + .unwrap(); + } + + // ── Horizontal rule ─────────────────────────────────────────────────────────── + Line::new(Point::new(0, 10), Point::new(128, 10)) + .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) + .draw(&mut disp) + .unwrap(); + + // ── Battery % ──────────────────────────────────────────────────────────────── + let style = MonoTextStyle::new(&profont::PROFONT_10_POINT, BinaryColor::On); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Right) + .baseline(Baseline::Top) + .build(); + Text::with_text_style("69%", Point::new(128, -1), style, text_style) + .draw(&mut disp) + .unwrap(); + + // ── Charging bolt (right side, only when charging) ─────────────────────────── + // Sits to the left of the battery text (~20px wide text → bolt at x=104) + if charging { + let bx = 104i32; // top-left x of the bolt + let by = 1i32; // top-left y (leaves 1px gap before rule at y=10) + + // Row 0: 0001 + Pixel(Point::new(bx + 3, by + 0), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + // Row 1: 0010 + Pixel(Point::new(bx + 2, by + 1), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + // Row 2: 0100 + Pixel(Point::new(bx + 1, by + 2), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + // Row 3: 1111 + Pixel(Point::new(bx + 0, by + 3), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + Pixel(Point::new(bx + 1, by + 3), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + Pixel(Point::new(bx + 2, by + 3), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + Pixel(Point::new(bx + 3, by + 3), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + // Row 4: 0001 + Pixel(Point::new(bx + 3, by + 4), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + // Row 5: 1010 + Pixel(Point::new(bx + 0, by + 5), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + Pixel(Point::new(bx + 2, by + 5), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + // Row 6: 1100 + Pixel(Point::new(bx + 0, by + 6), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + Pixel(Point::new(bx + 1, by + 6), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + // Row 7: 1110 + Pixel(Point::new(bx + 0, by + 7), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + Pixel(Point::new(bx + 1, by + 7), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + Pixel(Point::new(bx + 2, by + 7), BinaryColor::On) + .draw(&mut disp) + .unwrap(); + } + + disp.flush().await.unwrap(); + + return; + + /* + let start = Instant::now(); loop { embedded_graphics::prelude::DrawTarget::clear( @@ -93,6 +286,7 @@ pub async fn lcd_task( break; } } + */ fbuf.clear(BinaryColor::Off); //lcd_driver.display_on_oled(&mut fbuf).await; diff --git a/src/main.rs b/src/main.rs index 3ebde2d..6143a40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ #![no_std] #![no_main] #![feature(impl_trait_in_assoc_type)] -#![feature(asm_experimental_arch)] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] extern crate alloc; From 7b45da360a3fb686e603b5a87ca1773c308cf0c6 Mon Sep 17 00:00:00 2001 From: filipton Date: Thu, 12 Mar 2026 17:25:20 +0100 Subject: [PATCH 13/31] wip: test layout with pixelart generator --- Cargo.lock | 846 +++++++++++++++++++++++++++++++- Cargo.toml | 1 + macros/Cargo.lock | 977 ++++++++++++++++++++++++++++++++++++- macros/Cargo.toml | 14 +- macros/src/lib.rs | 6 + macros/src/pixelart.rs | 152 ++++++ src/lcd_v4.rs | 264 ++++++++-- src/resources/CHARGING.png | Bin 0 -> 154 bytes 8 files changed, 2190 insertions(+), 70 deletions(-) create mode 100644 macros/src/pixelart.rs create mode 100644 src/resources/CHARGING.png diff --git a/Cargo.lock b/Cargo.lock index 27d3e30..de65fcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "adv-shift-registers" version = "0.2.5" @@ -68,6 +74,24 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice 0.2.1", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.3.1" @@ -80,12 +104,35 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "arraydeque" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "as-slice" version = "0.1.5" @@ -98,6 +145,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "askama" version = "0.14.0" @@ -141,6 +197,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + [[package]] name = "az" version = "1.2.1" @@ -159,6 +258,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitfield" version = "0.19.4" @@ -191,6 +296,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -251,6 +365,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "bytemuck" version = "1.25.0" @@ -263,6 +389,24 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -290,6 +434,12 @@ dependencies = [ "digest", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "const-default" version = "1.0.0" @@ -320,6 +470,24 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -329,12 +497,52 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "critical-section" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -983,6 +1191,27 @@ dependencies = [ "embedded-io 0.7.1", ] +[[package]] +name = "embedded-layout" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a90553247f2b05c59ac7894ea13d830636c2b1203fa03bff400eddbd1fa9f52" +dependencies = [ + "embedded-graphics", + "embedded-layout-macros", +] + +[[package]] +name = "embedded-layout-macros" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f6e621fe4c7e05b695274b722dc0a60bacd1c8696b58191baa0154713d52400" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "embedded-nal" version = "0.9.0" @@ -1072,6 +1301,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1276,7 +1525,7 @@ dependencies = [ "esp-radio", "esp-rtos", "esp-storage", - "getrandom", + "getrandom 0.2.15", "heapless 0.8.0", "heapless 0.9.2", "log", @@ -1527,6 +1776,50 @@ dependencies = [ "vcell", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.1" @@ -1537,6 +1830,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fkm-firmware" version = "0.1.0" @@ -1561,6 +1860,7 @@ dependencies = [ "embedded-hal-async", "embedded-hal-bus", "embedded-io-async 0.7.0", + "embedded-layout", "embedded-storage", "embedded-tls", "esp-alloc", @@ -1576,7 +1876,7 @@ dependencies = [ "esp-rtos", "esp-storage", "esp32c3", - "getrandom", + "getrandom 0.2.15", "heapless 0.9.2", "log", "macros", @@ -1593,6 +1893,16 @@ dependencies = [ "ws-framer", ] +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -1739,6 +2049,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "ghash" version = "0.5.1" @@ -1749,6 +2071,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "group" version = "0.13.0" @@ -1760,6 +2092,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hash32" version = "0.1.1" @@ -1808,7 +2151,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634bd4d29cbf24424d0a4bfcbf80c6960129dc24424752a7d1d1390607023422" dependencies = [ - "as-slice", + "as-slice 0.1.5", "generic-array 0.14.7", "hash32 0.1.1", "stable_deref_trait", @@ -1870,6 +2213,46 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "indexmap" version = "2.13.0" @@ -1911,6 +2294,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1959,12 +2353,38 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "linked_list_allocator" version = "0.10.5" @@ -1992,11 +2412,22 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "macros" version = "0.1.0" dependencies = [ - "convert_case 0.8.0", + "anyhow", + "convert_case 0.11.0", + "image", "proc-macro2", "quote", "serde", @@ -2010,6 +2441,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.8.0" @@ -2022,6 +2463,26 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "nb" version = "0.1.3" @@ -2037,6 +2498,37 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -2048,6 +2540,26 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2120,6 +2632,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2132,6 +2650,19 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polyval" version = "0.6.2" @@ -2248,6 +2779,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "profont" version = "0.7.0" @@ -2257,15 +2807,42 @@ dependencies = [ "embedded-graphics", ] +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "radium" version = "0.7.0" @@ -2281,6 +2858,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2291,13 +2878,23 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -2305,6 +2902,79 @@ name = "rand_core" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] [[package]] name = "rfc6979" @@ -2316,6 +2986,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + [[package]] name = "riscv" version = "0.15.0" @@ -2502,6 +3178,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signature" version = "2.2.0" @@ -2512,6 +3194,27 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "smoltcp" version = "0.12.0" @@ -2658,6 +3361,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "toml" version = "0.8.23" @@ -2746,8 +3463,8 @@ dependencies = [ "futures", "heapless 0.9.2", "p256", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "rand_core 0.6.4", "static_cell", "trouble-host-macros", @@ -2820,7 +3537,18 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", ] [[package]] @@ -2847,6 +3575,66 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2880,13 +3668,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + [[package]] name = "ws-framer" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75a918023503304dc681e5f014fc291f68fcdcc886993f9cc6fffc0a0cef329e" dependencies = [ - "getrandom", + "getrandom 0.2.15", "httparse", "itoa", "ws-framer-macros", @@ -2944,6 +3738,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yaml-rust2" version = "0.9.0" @@ -2986,3 +3786,27 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 80a864f..3fb8ca9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ embedded-graphics-framebuf = { version = "0.5.0", optional = true } oled_async = { version = "0.2.0", features = ["i2c"], optional = true } bq27441 = { version = "0.1.0", features = ["async", "embassy"], optional = true } profont = { version = "0.7.0", optional = true } +embedded-layout = { version = "0.4.2", optional = true } #v3 only dyn-smooth = { version = "0.2.0", optional = true } diff --git a/macros/Cargo.lock b/macros/Cargo.lock index 8a644bc..42b57be 100644 --- a/macros/Cargo.lock +++ b/macros/Cargo.lock @@ -3,25 +3,482 @@ version = 4 [[package]] -name = "convert_case" +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[package]] +name = "built" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "macros" version = "0.1.0" dependencies = [ + "anyhow", "convert_case", + "image", "proc-macro2", "quote", "serde", @@ -29,35 +486,327 @@ dependencies = [ "syn", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" -version = "1.0.41" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] -name = "ryu" -version = "1.0.19" +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rav1e" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "serde" @@ -91,28 +840,95 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", ] +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "syn" -version = "2.0.107" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "unicode-ident" version = "1.0.13" @@ -124,3 +940,136 @@ name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +dependencies = [ + "zune-core", +] diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 2c18044..97b8c25 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -7,9 +7,11 @@ edition = "2021" proc-macro = true [dependencies] -convert_case = "0.8.0" -proc-macro2 = "1.0.95" -quote = "1.0.40" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.142" -syn = { version = "2.0.104", features = ["extra-traits", "full"] } +anyhow = "1.0.102" +convert_case = "0.11.0" +image = "0.25.10" +proc-macro2 = "1.0.106" +quote = "1.0.45" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +syn = { version = "2.0.117", features = ["extra-traits", "full"] } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 80ff700..c3619fc 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,5 +1,6 @@ #![feature(proc_macro_span)] +mod pixelart; mod translations; use proc_macro::TokenStream; @@ -204,6 +205,11 @@ pub fn load_default_translations(args: TokenStream) -> TokenStream { translations::load_translations_macro(args) } +#[proc_macro] +pub fn load_lcd_resources(args: TokenStream) -> TokenStream { + pixelart::load_lcd_resources(args) +} + // Maybe not useful, most variables in functions that can fail are owned :( /* struct MacroInput { diff --git a/macros/src/pixelart.rs b/macros/src/pixelart.rs new file mode 100644 index 0000000..cc367db --- /dev/null +++ b/macros/src/pixelart.rs @@ -0,0 +1,152 @@ +use convert_case::Casing; +use image::{GenericImageView, ImageReader}; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use std::path::PathBuf; +use syn::{ + parse::{Parse, ParseStream}, + LitStr, +}; + +#[derive(Debug)] +#[allow(dead_code)] +struct PixelArtHandler { + path: String, +} + +impl Parse for PixelArtHandler { + fn parse(input: ParseStream) -> Result { + let path = input.parse::()?; + + Ok(PixelArtHandler { path: path.value() }) + } +} + +pub fn load_lcd_resources(args: TokenStream) -> TokenStream { + let PixelArtHandler { path } = syn::parse_macro_input!(args as PixelArtHandler); + + let mut resources = Vec::new(); + let mut res = std::fs::read_dir(path).expect("Cannot read specified path"); + while let Some(Ok(e)) = res.next() { + let metadata = e.metadata().unwrap(); + if metadata.is_file() { + if let Ok((width, height, data)) = parse_img(e.path()) { + let name = format_ident!( + "{}", + e.file_name() + .to_string_lossy() + .split(".") + .next() + .unwrap() + .to_case(convert_case::Case::Constant) + ); + let data_tokens = quote! { &[#(#data),*] }; + + resources.push(quote! { + pub const #name: PixelArt = PixelArt::new(#data_tokens, #width, #height); + }); + } + } + } + + quote! { + pub struct Resources {} + + impl Resources { + #(#resources)* + } + } + .into() +} + +fn parse_img(path: PathBuf) -> anyhow::Result<(u32, u32, Vec)> { + let img = ImageReader::open(path)?.decode()?; + let (w, h) = img.dimensions(); + + let mut bounding_box_left = 0; + let mut bounding_box_right = w; + let mut bounding_box_top = h; + let mut bounding_box_bottom = 0; + + for x in 0..w { + let mut all_clear = true; + for y in 0..h { + let px = img.get_pixel(x, y).0; + if px == [0, 0, 0, 255] { + all_clear = false; + break; + } + } + + if !all_clear { + bounding_box_right = x; + } + } + + for x in (0..w).rev() { + let mut all_clear = true; + for y in 0..h { + let px = img.get_pixel(x, y).0; + if px == [0, 0, 0, 255] { + all_clear = false; + break; + } + } + + if !all_clear { + bounding_box_left = x; + } + } + + for y in 0..h { + let mut all_clear = true; + for x in 0..w { + let px = img.get_pixel(x, y).0; + if px == [0, 0, 0, 255] { + all_clear = false; + break; + } + } + + if !all_clear { + bounding_box_top = y; + } + } + + for y in (0..h).rev() { + let mut all_clear = true; + for x in 0..w { + let px = img.get_pixel(x, y).0; + if px == [0, 0, 0, 255] { + all_clear = false; + break; + } + } + + if !all_clear { + bounding_box_bottom = y; + } + } + + let x_size = bounding_box_right - bounding_box_left + 1; + let y_size = bounding_box_top - bounding_box_bottom + 1; + + let bytes_per_row = (x_size + 7) / 8; + let mut data = vec![0u8; (bytes_per_row * y_size) as usize]; + for y in bounding_box_bottom..=bounding_box_top { + for x in bounding_box_left..=bounding_box_right { + let px = img.get_pixel(x, y).0; + + if px == [0, 0, 0, 255] { + let relative_x = x - bounding_box_left; + let relative_y = y - bounding_box_bottom; + + let byte_idx = (relative_y * bytes_per_row + relative_x / 8) as usize; + let bit_idx = 7 - (relative_x % 8); + data[byte_idx] |= 1 << bit_idx; + } + } + } + + Ok((x_size, y_size, data)) +} diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index 61775a1..10c4b43 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -6,13 +6,18 @@ use embedded_graphics::{ Drawable, Pixel, mono_font::MonoTextStyle, pixelcolor::BinaryColor, - prelude::{Angle, DrawTarget, Point, Primitive, Size}, + prelude::*, primitives::{Arc, Circle, Line, PrimitiveStyle, Rectangle}, text::{Alignment, Baseline, Text, TextStyleBuilder}, }; use embedded_graphics_framebuf::FrameBuf; +use embedded_layout::{ + layout::linear::{FixedMargin, LinearLayout}, + prelude::*, +}; use esp_hal::gpio::Output; use oled_async::{displays::ssd1309::Ssd1309_128_64, mode::GraphicsMode}; +use profont::PROFONT_12_POINT; use crate::{ consts::{ @@ -26,6 +31,140 @@ use crate::{ utils::{shared_i2c::SharedI2C, stackmat::ms_to_time_str}, }; +macros::load_lcd_resources!("src/resources"); + +#[derive(Clone, Copy)] +pub struct PixelArt { + data: &'static [u8], // packed bits, MSB first, row by row + width: u32, + height: u32, + top_left: Point, +} + +impl PixelArt { + pub const fn new(data: &'static [u8], width: u32, height: u32) -> Self { + Self { + data, + width, + height, + top_left: Point::zero(), + } + } + + fn bytes_per_row(&self) -> u32 { + (self.width + 7) / 8 + } +} + +impl Dimensions for PixelArt { + fn bounding_box(&self) -> Rectangle { + Rectangle::new(self.top_left, Size::new(self.width, self.height)) + } +} + +impl Transform for PixelArt { + fn translate(&self, by: Point) -> Self { + Self { + top_left: self.top_left + by, + ..*self + } + } + fn translate_mut(&mut self, by: Point) -> &mut Self { + self.top_left += by; + self + } +} + +impl Drawable for PixelArt { + type Color = BinaryColor; + type Output = (); + + fn draw>(&self, target: &mut D) -> Result<(), D::Error> { + let origin = self.top_left; + let bpr = self.bytes_per_row(); + let (w, h) = (self.width, self.height); + let data = self.data; + + target.draw_iter((0..h).flat_map(move |y| { + (0..w).map(move |x| { + let byte = data[(y * bpr + x / 8) as usize]; + let on = byte & (0x80 >> (x % 8)) != 0; + Pixel( + origin + Point::new(x as i32, y as i32), + if on { + BinaryColor::On + } else { + BinaryColor::Off + }, + ) + }) + })) + } +} + +pub struct Overlay { + base: A, + overlay: B, + top_left: Point, +} + +impl Overlay { + pub fn new(base: A, overlay: B) -> Self { + let base_box = base.bounding_box(); + let overlay_box = overlay.bounding_box(); + + // center overlay over base + let offset = Point::new( + (base_box.size.width as i32 - overlay_box.size.width as i32) / 2, + (base_box.size.height as i32 - overlay_box.size.height as i32) / 2, + ); + let overlay = overlay.translate(base_box.top_left + offset); + + Self { + top_left: base_box.top_left, + base, + overlay, + } + } +} + +impl Dimensions for Overlay { + fn bounding_box(&self) -> Rectangle { + Rectangle::new(self.top_left, self.base.bounding_box().size) + } +} + +impl Transform for Overlay { + fn translate(&self, by: Point) -> Self { + Self { + top_left: self.top_left + by, + base: self.base.translate(by), + overlay: self.overlay.translate(by), + } + } + fn translate_mut(&mut self, by: Point) -> &mut Self { + self.top_left += by; + self.base.translate_mut(by); + self.overlay.translate_mut(by); + self + } +} + +impl Drawable for Overlay +where + A: Drawable, + B: Drawable, +{ + type Color = BinaryColor; + type Output = (); + + fn draw>(&self, target: &mut D) -> Result<(), D::Error> { + self.base.draw(target)?; + self.overlay.draw(target)?; + Ok(()) + } +} + #[embassy_executor::task] pub async fn lcd_task( i2c: SharedI2C, @@ -47,10 +186,55 @@ pub async fn lcd_task( disp.flush().await.unwrap(); let mut data = - alloc::vec![embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 64) as usize]; - let data: &mut [BinaryColor; 128 * 64] = data.as_mut_array().unwrap(); - let mut fbuf = embedded_graphics_framebuf::FrameBuf::new(data, 128, 64); + alloc::vec![embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 11) as usize]; + let data: &mut [BinaryColor; 128 * 11] = data.as_mut_array().unwrap(); + let mut fbuf = embedded_graphics_framebuf::FrameBuf::new(data, 128, 11); + + /* + let thin_stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 1); + let cross_h = Line::new(Point::new(0, 0), Point::new(8, 8)).into_styled(thin_stroke); + let cross_v = Line::new(Point::new(0, 8), Point::new(8, 0)).into_styled(thin_stroke); + let cross = Overlay::new(cross_h, cross_v); + let smiley_with_cross = Overlay::new(SMILEY, cross); + */ + + let style = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_7X13, + BinaryColor::On, + ); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Right) + .baseline(Baseline::Top) + .build(); + let text = Text::with_text_style("100%", Point::zero(), style, text_style); + + LinearLayout::horizontal( + /* + Chain::new(SMILEY) + .append(smiley_with_cross) + .append(CHARGING) + .append(text), + */ + Chain::new(Resources::CHARGING).append(text) + ) + .with_alignment(embedded_layout::align::vertical::Center) + .with_spacing(FixedMargin(2)) + .arrange() + .align_to( + &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), + embedded_layout::align::horizontal::Right, + embedded_layout::align::vertical::Center, + ) + .draw(&mut fbuf) + .unwrap(); + + Line::new(Point::new(0, 10), Point::new(128, 10)) + .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) + .draw(&mut fbuf) + .unwrap(); + + /* let charging = true; let wifi_connected = true; let server_connected = true; @@ -60,7 +244,7 @@ pub async fn lcd_task( // ── WiFi icon (arcs centered at x=5, y=9 so they arc upward above the bar) ─── Pixel(Point::new(5, 8), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); if wifi_connected { @@ -72,7 +256,7 @@ pub async fn lcd_task( Angle::from_degrees(120.0), ) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // Middle arc Arc::new( @@ -82,7 +266,7 @@ pub async fn lcd_task( Angle::from_degrees(120.0), ) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // Outer arc Arc::new( @@ -92,43 +276,43 @@ pub async fn lcd_task( Angle::from_degrees(120.0), ) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); } else { Line::new(Point::new(1, 2), Point::new(9, 8)) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Line::new(Point::new(9, 2), Point::new(1, 8)) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); } // ── Server icon (x=13..24, y=1..8, bottom at y=8 → 1px gap before rule) ────── Rectangle::new(Point::new(13, 1), Size::new(12, 8)) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Line::new(Point::new(14, 5), Point::new(23, 5)) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Pixel(Point::new(22, 3), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Pixel(Point::new(22, 7), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); if !server_connected { Line::new(Point::new(13, 1), Point::new(24, 8)) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Line::new(Point::new(24, 1), Point::new(13, 8)) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); } @@ -136,34 +320,34 @@ pub async fn lcd_task( // Button on top Line::new(Point::new(30, 1), Point::new(32, 1)) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // Round body: diameter=7 → top_left=(27,2), center=(30,5), bottom=y=8 Circle::new(Point::new(27, 2), 7) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // Clock hand pointing upper-right Line::new(Point::new(30, 5), Point::new(32, 3)) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); if !timer_connected { Line::new(Point::new(27, 2), Point::new(33, 8)) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Line::new(Point::new(33, 2), Point::new(27, 8)) .into_styled(stroke) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); } // ── Horizontal rule ─────────────────────────────────────────────────────────── Line::new(Point::new(0, 10), Point::new(128, 10)) .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // ── Battery % ──────────────────────────────────────────────────────────────── @@ -172,8 +356,8 @@ pub async fn lcd_task( .alignment(Alignment::Right) .baseline(Baseline::Top) .build(); - Text::with_text_style("69%", Point::new(128, -1), style, text_style) - .draw(&mut disp) + Text::with_text_style("10%", Point::new(128, -1), style, text_style) + .draw(&mut fbuf) .unwrap(); // ── Charging bolt (right side, only when charging) ─────────────────────────── @@ -184,59 +368,61 @@ pub async fn lcd_task( // Row 0: 0001 Pixel(Point::new(bx + 3, by + 0), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // Row 1: 0010 Pixel(Point::new(bx + 2, by + 1), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // Row 2: 0100 Pixel(Point::new(bx + 1, by + 2), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // Row 3: 1111 Pixel(Point::new(bx + 0, by + 3), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Pixel(Point::new(bx + 1, by + 3), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Pixel(Point::new(bx + 2, by + 3), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Pixel(Point::new(bx + 3, by + 3), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // Row 4: 0001 Pixel(Point::new(bx + 3, by + 4), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // Row 5: 1010 Pixel(Point::new(bx + 0, by + 5), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Pixel(Point::new(bx + 2, by + 5), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // Row 6: 1100 Pixel(Point::new(bx + 0, by + 6), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Pixel(Point::new(bx + 1, by + 6), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); // Row 7: 1110 Pixel(Point::new(bx + 0, by + 7), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Pixel(Point::new(bx + 1, by + 7), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); Pixel(Point::new(bx + 2, by + 7), BinaryColor::On) - .draw(&mut disp) + .draw(&mut fbuf) .unwrap(); } + */ + disp.draw_iter(fbuf.into_iter()).unwrap(); disp.flush().await.unwrap(); return; diff --git a/src/resources/CHARGING.png b/src/resources/CHARGING.png new file mode 100644 index 0000000000000000000000000000000000000000..5288b1cb6710304b48a036ed964ccdaecb153f7f GIT binary patch literal 154 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CJ!3HGRcAO0XQvRMUjv*C{J?9v?4k!pPAKdo; ze{m{{qhULjV%Da4_lx8H$5=9+dat`-YU9 Date: Thu, 12 Mar 2026 18:07:21 +0100 Subject: [PATCH 14/31] feat: battery update oled state --- Cargo.toml | 2 +- src/battery_v4.rs | 52 +++++++-------- src/lcd_v4.rs | 136 +++++++++++++++++++++++++++------------ src/resources/SERVER.png | Bin 0 -> 164 bytes src/resources/TIMER.png | Bin 0 -> 171 bytes src/resources/WIFI.png | Bin 0 -> 182 bytes src/state.rs | 14 +++- 7 files changed, 132 insertions(+), 72 deletions(-) create mode 100644 src/resources/SERVER.png create mode 100644 src/resources/TIMER.png create mode 100644 src/resources/WIFI.png diff --git a/Cargo.toml b/Cargo.toml index 3fb8ca9..ca4034e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ qa = [] sleep = [] auto_add = [] v3 = ["dep:dyn-smooth", "dep:ag-lcd-async"] -v4 = ["dep:display-interface", "dep:display-interface-i2c", "dep:embedded-graphics", "dep:embedded-graphics-framebuf", "dep:oled_async", "dep:bq27441", "dep:profont"] +v4 = ["dep:display-interface", "dep:display-interface-i2c", "dep:embedded-graphics", "dep:embedded-graphics-framebuf", "dep:oled_async", "dep:bq27441", "dep:profont", "dep:embedded-layout"] [profile.dev] opt-level = 3 diff --git a/src/battery_v4.rs b/src/battery_v4.rs index 5708021..43ab6d2 100644 --- a/src/battery_v4.rs +++ b/src/battery_v4.rs @@ -1,5 +1,5 @@ use crate::{consts::BATTERY_SEND_INTERVAL_MS, state::sleep_state, utils::shared_i2c::SharedI2C}; -use embassy_time::Timer; +use embassy_time::{Instant, Timer}; const BATTERY_CURVE: [(f64, u8); 11] = [ (3350.0, 0), @@ -20,51 +20,47 @@ const BAT_MAX: f64 = BATTERY_CURVE[BATTERY_CURVE.len() - 1].0; #[embassy_executor::task] pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) { let Ok(mut gauge) = bq27441::Bq27441Async::new(i2c).await else { - state.show_battery.signal(0); log::error!("BQ27441 init failed!"); return; }; - let mut lcd_sent = false; + let mut last_sent = Instant::now(); loop { + Timer::after_millis(100).await; + if sleep_state() { Timer::after_millis(500).await; continue; } - if !lcd_sent { - let mut soc = gauge.state_of_charge().await.unwrap_or(0) as u8; - if soc == 0 { - let volt = gauge.voltage().await.unwrap_or(0) as f64; - soc = bat_percentage(calculate(volt)); - } - - state.show_battery.signal(soc); - lcd_sent = true; - Timer::after_millis(BATTERY_SEND_INTERVAL_MS).await; - continue; - } - let mut soc = gauge.state_of_charge().await.unwrap_or(0) as u8; let mv = gauge.voltage().await.unwrap_or(0) as f64; if soc == 0 { soc = bat_percentage(calculate(mv)); } - state.show_battery.signal(soc); - if state.state.lock().await.server_connected == Some(true) { - crate::ws::send_packet(crate::structs::TimerPacket { - tag: None, - data: crate::structs::TimerPacketInner::Battery { - level: Some(soc as f64), - voltage: Some(mv), - }, - }) - .await; + let ma = gauge.average_current().await.unwrap_or(0); + + { + let mut state = state.state.lock().await; + state.battery_status = (soc, ma >= 0) } - log::info!("Battery {mv}mv {soc}%"); - Timer::after_millis(BATTERY_SEND_INTERVAL_MS).await; + if last_sent.elapsed().as_millis() >= BATTERY_SEND_INTERVAL_MS { + if state.state.lock().await.server_connected == Some(true) { + crate::ws::send_packet(crate::structs::TimerPacket { + tag: None, + data: crate::structs::TimerPacketInner::Battery { + level: Some(soc as f64), + voltage: Some(mv), + }, + }) + .await; + } + + log::info!("Battery {mv}mv {soc}%"); + last_sent = Instant::now(); + } } } diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index 10c4b43..7038a50 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -1,4 +1,4 @@ -use alloc::{rc::Rc, string::ToString}; +use alloc::{format, rc::Rc, string::ToString}; use display_interface_i2c::I2CInterface; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; use embassy_time::{Delay, Duration, Instant, Timer}; @@ -188,7 +188,7 @@ pub async fn lcd_task( let mut data = alloc::vec![embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 11) as usize]; let data: &mut [BinaryColor; 128 * 11] = data.as_mut_array().unwrap(); - let mut fbuf = embedded_graphics_framebuf::FrameBuf::new(data, 128, 11); + let mut top_bar_fbuf = embedded_graphics_framebuf::FrameBuf::new(data, 128, 11); /* let thin_stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 1); @@ -199,41 +199,98 @@ pub async fn lcd_task( let smiley_with_cross = Overlay::new(SMILEY, cross); */ - let style = MonoTextStyle::new( - &embedded_graphics::mono_font::ascii::FONT_7X13, - BinaryColor::On, - ); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Right) - .baseline(Baseline::Top) - .build(); - let text = Text::with_text_style("100%", Point::zero(), style, text_style); - - LinearLayout::horizontal( - /* - Chain::new(SMILEY) - .append(smiley_with_cross) - .append(CHARGING) - .append(text), - */ - Chain::new(Resources::CHARGING).append(text) - ) - .with_alignment(embedded_layout::align::vertical::Center) - .with_spacing(FixedMargin(2)) - .arrange() - .align_to( - &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), - embedded_layout::align::horizontal::Right, - embedded_layout::align::vertical::Center, - ) - .draw(&mut fbuf) - .unwrap(); + let mut last_update; + loop { + let current_state = global_state.state.value().await.clone(); + log::debug!("lcd current_state: {current_state:?}"); + last_update = Instant::now(); - Line::new(Point::new(0, 10), Point::new(128, 10)) - .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) - .draw(&mut fbuf) + top_bar_fbuf.clear(BinaryColor::Off); + let style = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_7X13, + BinaryColor::On, + ); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Right) + .baseline(Baseline::Top) + .build(); + + let text = format!("{}%", current_state.battery_status.0); + let text = Text::with_text_style(&text, Point::zero(), style, text_style); + if current_state.battery_status.1 { + LinearLayout::horizontal(Chain::new(Resources::CHARGING).append(text)) + .with_alignment(embedded_layout::align::vertical::Center) + .with_spacing(FixedMargin(1)) + .arrange() + .align_to( + &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), + embedded_layout::align::horizontal::Right, + embedded_layout::align::vertical::Center, + ) + .draw(&mut top_bar_fbuf) + .unwrap(); + } else { + LinearLayout::horizontal(Chain::new(text)) + .with_alignment(embedded_layout::align::vertical::Center) + .with_spacing(FixedMargin(1)) + .arrange() + .align_to( + &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), + embedded_layout::align::horizontal::Right, + embedded_layout::align::vertical::Center, + ) + .draw(&mut top_bar_fbuf) + .unwrap(); + } + + LinearLayout::horizontal( + Chain::new(Resources::WIFI) + .append(Resources::SERVER) + .append(Resources::TIMER), + ) + .with_alignment(embedded_layout::align::vertical::Center) + .with_spacing(FixedMargin(2)) + .arrange() + .align_to( + &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), + embedded_layout::align::horizontal::Left, + embedded_layout::align::vertical::Center, + ) + .draw(&mut top_bar_fbuf) .unwrap(); + Line::new(Point::new(0, 10), Point::new(128, 10)) + .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) + .draw(&mut top_bar_fbuf) + .unwrap(); + + disp.draw_iter(top_bar_fbuf.into_iter()).unwrap(); + disp.flush().await.unwrap(); + + if sleep_state() { + //lcd.backlight_on(); + + unsafe { + crate::state::SLEEP_STATE = false; + } + } + + let current_scene = current_state.scene.clone(); + let fut = async { + loop { + Timer::after_millis(100).await; + } + }; + + let res = embassy_futures::select::select(fut, global_state.state.wait()).await; + match res { + embassy_futures::select::Either::First(_) => {} + embassy_futures::select::Either::Second(_) => { + continue; + } + } + } + /* let charging = true; let wifi_connected = true; @@ -422,9 +479,6 @@ pub async fn lcd_task( } */ - disp.draw_iter(fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); - return; /* @@ -474,16 +528,16 @@ pub async fn lcd_task( } */ - fbuf.clear(BinaryColor::Off); + top_bar_fbuf.clear(BinaryColor::Off); //lcd_driver.display_on_oled(&mut fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, top_bar_fbuf.into_iter()).unwrap(); disp.flush().await.unwrap(); Timer::after_millis(2500).await; - fbuf.clear(BinaryColor::Off); + top_bar_fbuf.clear(BinaryColor::Off); //_ = lcd_driver.clear_all(); - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); + embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, top_bar_fbuf.into_iter()).unwrap(); disp.flush().await.unwrap(); let mut last_update; diff --git a/src/resources/SERVER.png b/src/resources/SERVER.png new file mode 100644 index 0000000000000000000000000000000000000000..4d1c691570cd5c47157ce266bee8272f947cd90d GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CJ!3HGRcAO0XQjwl6jv*C{Q~Cn=4k++A?Y{f} zob>v-jw3TNmP9>SnIFr?ZnshW<&IN~J?^RMHvgZJf4%2^Bf38-mLV|stNZLKUIpcfgDLLc8JLe8sMb9;?*cPtn)8BJ zjE}_>%1<1KUNAMAp)>u!-h?xgu1ZTNR2&Ri_7`Zy|Nmt!YhHt_1lh&Rpz&Uaf2N>V Q0#Jm()78&qol`;+09O(_82|tP literal 0 HcmV?d00001 diff --git a/src/resources/WIFI.png b/src/resources/WIFI.png new file mode 100644 index 0000000000000000000000000000000000000000..82d22a442606fbb98104734bc4db7e51018dd7a8 GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CJ!3HGRcAO0XQrVs^jv*C{HRl<*8VqldTJOY}Hor1n*ZoI=0J3DuMv#VhVGw@hl`sebX~ zIe$wNn33w>sHGlLe!Ah1M>A+!PC{xWt~$(695xwMJoUR literal 0 HcmV?d00001 diff --git a/src/state.rs b/src/state.rs index c165369..1792814 100644 --- a/src/state.rs +++ b/src/state.rs @@ -148,11 +148,13 @@ pub struct GlobalStateInner { pub state: SignaledMutex, pub timer_signal: Signal, pub bt_display_signal: Signal, - pub show_battery: Signal, pub update_progress: Signal, pub sign_unsign_progress: Signal, pub ble_sig: Signal, + #[cfg(feature = "v3")] + pub show_battery: Signal, + pub nvs: Nvs, pub aes: Mutex>, @@ -166,11 +168,13 @@ impl GlobalStateInner { state: SignaledMutex::new(SignaledGlobalStateInner::new()), timer_signal: Signal::new(), bt_display_signal: Signal::new(), - show_battery: Signal::new(), update_progress: Signal::new(), sign_unsign_progress: Signal::new(), ble_sig: Signal::new(), + #[cfg(feature = "v3")] + show_battery: Signal::new(), + nvs: nvs.clone(), aes: Mutex::new(Aes::new(aes)), @@ -215,6 +219,9 @@ pub struct SignaledGlobalStateInner { pub delegate_used: bool, pub delegate_hold: Option, + #[cfg(feature = "v4")] + pub battery_status: (u8, bool), + #[cfg(feature = "bat_dev_lcd")] pub current_bat_read: Option, @@ -268,6 +275,9 @@ impl SignaledGlobalStateInner { delegate_used: false, delegate_hold: None, + #[cfg(feature = "v4")] + battery_status: (0, false), + #[cfg(feature = "bat_dev_lcd")] current_bat_read: None, From accf6787806843c209ea8f1b0413a6925236f375 Mon Sep 17 00:00:00 2001 From: filipton Date: Thu, 12 Mar 2026 19:29:37 +0100 Subject: [PATCH 15/31] feat: show status icons on top bar --- src/battery_v4.rs | 3 +- src/lcd_v3.rs | 2 +- src/lcd_v4.rs | 284 ++++++++++++++-------------------------------- src/main.rs | 4 + src/state.rs | 5 +- src/ws.rs | 6 +- 6 files changed, 99 insertions(+), 205 deletions(-) diff --git a/src/battery_v4.rs b/src/battery_v4.rs index 43ab6d2..e30462a 100644 --- a/src/battery_v4.rs +++ b/src/battery_v4.rs @@ -38,7 +38,6 @@ pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) if soc == 0 { soc = bat_percentage(calculate(mv)); } - let ma = gauge.average_current().await.unwrap_or(0); { @@ -58,7 +57,7 @@ pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) .await; } - log::info!("Battery {mv}mv {soc}%"); + log::info!("Battery {mv}mv {soc}% (avg current: {ma}mA)"); last_sent = Instant::now(); } } diff --git a/src/lcd_v3.rs b/src/lcd_v3.rs index d742682..0f37065 100644 --- a/src/lcd_v3.rs +++ b/src/lcd_v3.rs @@ -642,7 +642,7 @@ async fn process_lcd_overwrite( } if current_state.server_connected == Some(false) { - if current_state.wifi_conn_lost { + if current_state.wifi_connected == Some(false) { // TODO: maybe add to this translation _ = lcd_driver.print(0, "Wi-Fi", PrintAlign::Center, true); _ = lcd_driver.print(1, "Connection lost", PrintAlign::Center, true); diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index 7038a50..e471bd9 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -165,6 +165,78 @@ where } } +pub struct CrossedIcon { + base: A, + top_left: Point, + crossed: bool, + cross_size: u32, +} + +impl CrossedIcon { + pub fn new(base: A, crossed: bool, cross_size: u32) -> Self { + let base_box = base.bounding_box(); + Self { + base, + top_left: base_box.top_left, + crossed, + cross_size, + } + } +} + +impl Dimensions for CrossedIcon { + fn bounding_box(&self) -> Rectangle { + Rectangle::new(self.top_left, self.base.bounding_box().size) + } +} + +impl Transform for CrossedIcon { + fn translate(&self, by: Point) -> Self { + Self { + top_left: self.top_left + by, + base: self.base.translate(by), + crossed: self.crossed, + cross_size: self.cross_size, + } + } + fn translate_mut(&mut self, by: Point) -> &mut Self { + self.top_left += by; + self.base.translate_mut(by); + self + } +} + +impl Drawable for CrossedIcon +where + A: Drawable + Dimensions, +{ + type Color = BinaryColor; + type Output = (); + fn draw>(&self, target: &mut D) -> Result<(), D::Error> { + self.base.draw(target)?; + if self.crossed { + let thin_stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 1); + let bb = self.bounding_box(); + + // Center the cross over the bounding box + let offset = Point::new( + (bb.size.width as i32 - self.cross_size as i32) / 2, + (bb.size.height as i32 - self.cross_size as i32) / 2, + ); + let tl = bb.top_left + offset; + let s = self.cross_size as i32 - 1; + + Line::new(tl, tl + Point::new(s, s)) + .into_styled(thin_stroke) + .draw(target)?; + Line::new(tl + Point::new(0, s), tl + Point::new(s, 0)) + .into_styled(thin_stroke) + .draw(target)?; + } + Ok(()) + } +} + #[embassy_executor::task] pub async fn lcd_task( i2c: SharedI2C, @@ -190,15 +262,11 @@ pub async fn lcd_task( let data: &mut [BinaryColor; 128 * 11] = data.as_mut_array().unwrap(); let mut top_bar_fbuf = embedded_graphics_framebuf::FrameBuf::new(data, 128, 11); - /* let thin_stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 1); let cross_h = Line::new(Point::new(0, 0), Point::new(8, 8)).into_styled(thin_stroke); let cross_v = Line::new(Point::new(0, 8), Point::new(8, 0)).into_styled(thin_stroke); let cross = Overlay::new(cross_h, cross_v); - let smiley_with_cross = Overlay::new(SMILEY, cross); - */ - let mut last_update; loop { let current_state = global_state.state.value().await.clone(); @@ -244,9 +312,21 @@ pub async fn lcd_task( } LinearLayout::horizontal( - Chain::new(Resources::WIFI) - .append(Resources::SERVER) - .append(Resources::TIMER), + Chain::new(CrossedIcon::new( + Resources::WIFI, + !current_state.wifi_connected.unwrap_or(false), + 9, + )) + .append(CrossedIcon::new( + Resources::SERVER, + !current_state.server_connected.unwrap_or(false), + 9, + )) + .append(CrossedIcon::new( + Resources::TIMER, + !current_state.stackmat_connected.unwrap_or(false), + 9, + )), ) .with_alignment(embedded_layout::align::vertical::Center) .with_spacing(FixedMargin(2)) @@ -291,196 +371,6 @@ pub async fn lcd_task( } } - /* - let charging = true; - let wifi_connected = true; - let server_connected = true; - let timer_connected = true; - - let stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 1); - - // ── WiFi icon (arcs centered at x=5, y=9 so they arc upward above the bar) ─── - Pixel(Point::new(5, 8), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - - if wifi_connected { - // Inner arc - Arc::new( - Point::new(3, 7), - 4, - Angle::from_degrees(210.0), - Angle::from_degrees(120.0), - ) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - // Middle arc - Arc::new( - Point::new(1, 5), - 8, - Angle::from_degrees(210.0), - Angle::from_degrees(120.0), - ) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - // Outer arc - Arc::new( - Point::new(-1, 3), - 12, - Angle::from_degrees(210.0), - Angle::from_degrees(120.0), - ) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - } else { - Line::new(Point::new(1, 2), Point::new(9, 8)) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - Line::new(Point::new(9, 2), Point::new(1, 8)) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - } - - // ── Server icon (x=13..24, y=1..8, bottom at y=8 → 1px gap before rule) ────── - Rectangle::new(Point::new(13, 1), Size::new(12, 8)) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - Line::new(Point::new(14, 5), Point::new(23, 5)) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - Pixel(Point::new(22, 3), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - Pixel(Point::new(22, 7), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - - if !server_connected { - Line::new(Point::new(13, 1), Point::new(24, 8)) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - Line::new(Point::new(24, 1), Point::new(13, 8)) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - } - - // ── Timer / stopwatch icon (x=27..35, y=1..8) ──────────────────────────────── - // Button on top - Line::new(Point::new(30, 1), Point::new(32, 1)) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - // Round body: diameter=7 → top_left=(27,2), center=(30,5), bottom=y=8 - Circle::new(Point::new(27, 2), 7) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - // Clock hand pointing upper-right - Line::new(Point::new(30, 5), Point::new(32, 3)) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - - if !timer_connected { - Line::new(Point::new(27, 2), Point::new(33, 8)) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - Line::new(Point::new(33, 2), Point::new(27, 8)) - .into_styled(stroke) - .draw(&mut fbuf) - .unwrap(); - } - - // ── Horizontal rule ─────────────────────────────────────────────────────────── - Line::new(Point::new(0, 10), Point::new(128, 10)) - .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) - .draw(&mut fbuf) - .unwrap(); - - // ── Battery % ──────────────────────────────────────────────────────────────── - let style = MonoTextStyle::new(&profont::PROFONT_10_POINT, BinaryColor::On); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Right) - .baseline(Baseline::Top) - .build(); - Text::with_text_style("10%", Point::new(128, -1), style, text_style) - .draw(&mut fbuf) - .unwrap(); - - // ── Charging bolt (right side, only when charging) ─────────────────────────── - // Sits to the left of the battery text (~20px wide text → bolt at x=104) - if charging { - let bx = 104i32; // top-left x of the bolt - let by = 1i32; // top-left y (leaves 1px gap before rule at y=10) - - // Row 0: 0001 - Pixel(Point::new(bx + 3, by + 0), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - // Row 1: 0010 - Pixel(Point::new(bx + 2, by + 1), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - // Row 2: 0100 - Pixel(Point::new(bx + 1, by + 2), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - // Row 3: 1111 - Pixel(Point::new(bx + 0, by + 3), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - Pixel(Point::new(bx + 1, by + 3), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - Pixel(Point::new(bx + 2, by + 3), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - Pixel(Point::new(bx + 3, by + 3), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - // Row 4: 0001 - Pixel(Point::new(bx + 3, by + 4), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - // Row 5: 1010 - Pixel(Point::new(bx + 0, by + 5), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - Pixel(Point::new(bx + 2, by + 5), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - // Row 6: 1100 - Pixel(Point::new(bx + 0, by + 6), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - Pixel(Point::new(bx + 1, by + 6), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - // Row 7: 1110 - Pixel(Point::new(bx + 0, by + 7), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - Pixel(Point::new(bx + 1, by + 7), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - Pixel(Point::new(bx + 2, by + 7), BinaryColor::On) - .draw(&mut fbuf) - .unwrap(); - } - */ - - return; - /* let start = Instant::now(); diff --git a/src/main.rs b/src/main.rs index 6143a40..f51df23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -229,6 +229,10 @@ async fn main(spawner: Spawner) { esp_hal::system::software_reset(); }; + { + global_state.state.lock().await.wifi_connected = Some(true); + } + #[cfg(feature = "qa")] crate::qa::send_qa_resp(crate::qa::QaSignal::WifiSetup); diff --git a/src/state.rs b/src/state.rs index 1792814..c91067e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -210,8 +210,9 @@ pub struct SignaledGlobalStateInner { pub device_added: Option, pub server_connected: Option, - pub wifi_conn_lost: bool, + pub wifi_connected: Option, pub stackmat_connected: Option, + pub current_competitor: Option, pub current_judge: Option, pub competitor_display: Option, @@ -266,7 +267,7 @@ impl SignaledGlobalStateInner { device_added: None, server_connected: None, - wifi_conn_lost: false, + wifi_connected: None, stackmat_connected: None, current_competitor: None, current_judge: None, diff --git a/src/ws.rs b/src/ws.rs index 409fc42..12c97a4 100644 --- a/src/ws.rs +++ b/src/ws.rs @@ -143,7 +143,7 @@ async fn ws_loop( if let Err(e) = r { // but if wifi conneceted signal was sent remove wifi connection lost msg if wifi_conn_sig.signaled() && wifi_conn_sig.wait().await { - global_state.state.lock().await.wifi_conn_lost = false; + global_state.state.lock().await.wifi_connected = Some(true); } log::error!("connect error: {e:?}"); @@ -175,7 +175,7 @@ async fn ws_loop( { let mut state = global_state.state.lock().await; state.server_connected = Some(true); - state.wifi_conn_lost = false; // reset conn lost flag + state.wifi_connected = Some(true); } log::info!("connected!"); @@ -355,7 +355,7 @@ async fn ws_rw( continue; } - global_state.state.lock().await.wifi_conn_lost = true; + global_state.state.lock().await.wifi_connected = Some(false); log::error!("Wifi disconnected, ws_rw stop."); return Err(WsRwError::WifiDisconnected); } From 102e1e0acd1535119dc827d81ec1c1c2ccd5d623 Mon Sep 17 00:00:00 2001 From: filipton Date: Thu, 12 Mar 2026 23:00:59 +0100 Subject: [PATCH 16/31] wip: implement more oled stuff --- src/lcd_v4.rs | 375 +++++++++++++++++---------------------- src/resources/SERVER.png | Bin 164 -> 162 bytes src/stackmat.rs | 1 - 3 files changed, 164 insertions(+), 212 deletions(-) diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index e471bd9..7e3c8e1 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -1,4 +1,5 @@ use alloc::{format, rc::Rc, string::ToString}; +use anyhow::Result; use display_interface_i2c::I2CInterface; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; use embassy_time::{Delay, Duration, Instant, Timer}; @@ -237,6 +238,15 @@ where } } +pub const FBUF_WIDTH: usize = 128; +pub const FBUF_HEIGHT: usize = 64; +pub const FBUF_SIZE: usize = FBUF_WIDTH * FBUF_HEIGHT; + +pub struct OledData<'a> { + pub fbuf: FrameBuf, + pub disp: GraphicsMode>, +} + #[embassy_executor::task] pub async fn lcd_task( i2c: SharedI2C, @@ -253,19 +263,16 @@ pub async fn lcd_task( let mut disp: oled_async::mode::GraphicsMode<_, _> = raw_disp.into(); disp.reset(&mut display_rst, &mut Delay); + // TODO: remove all these unwraps! disp.init().await.unwrap(); disp.clear(); disp.flush().await.unwrap(); - let mut data = - alloc::vec![embedded_graphics::pixelcolor::BinaryColor::Off; (128 * 11) as usize]; - let data: &mut [BinaryColor; 128 * 11] = data.as_mut_array().unwrap(); - let mut top_bar_fbuf = embedded_graphics_framebuf::FrameBuf::new(data, 128, 11); + let mut data = alloc::vec![embedded_graphics::pixelcolor::BinaryColor::Off; FBUF_SIZE]; + let data: &mut [BinaryColor; FBUF_SIZE] = data.as_mut_array().unwrap(); + let fbuf = embedded_graphics_framebuf::FrameBuf::new(data, FBUF_WIDTH, FBUF_HEIGHT); - let thin_stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 1); - let cross_h = Line::new(Point::new(0, 0), Point::new(8, 8)).into_styled(thin_stroke); - let cross_v = Line::new(Point::new(0, 8), Point::new(8, 0)).into_styled(thin_stroke); - let cross = Overlay::new(cross_h, cross_v); + let mut oled = OledData { fbuf, disp }; let mut last_update; loop { @@ -273,80 +280,6 @@ pub async fn lcd_task( log::debug!("lcd current_state: {current_state:?}"); last_update = Instant::now(); - top_bar_fbuf.clear(BinaryColor::Off); - let style = MonoTextStyle::new( - &embedded_graphics::mono_font::ascii::FONT_7X13, - BinaryColor::On, - ); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Right) - .baseline(Baseline::Top) - .build(); - - let text = format!("{}%", current_state.battery_status.0); - let text = Text::with_text_style(&text, Point::zero(), style, text_style); - if current_state.battery_status.1 { - LinearLayout::horizontal(Chain::new(Resources::CHARGING).append(text)) - .with_alignment(embedded_layout::align::vertical::Center) - .with_spacing(FixedMargin(1)) - .arrange() - .align_to( - &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), - embedded_layout::align::horizontal::Right, - embedded_layout::align::vertical::Center, - ) - .draw(&mut top_bar_fbuf) - .unwrap(); - } else { - LinearLayout::horizontal(Chain::new(text)) - .with_alignment(embedded_layout::align::vertical::Center) - .with_spacing(FixedMargin(1)) - .arrange() - .align_to( - &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), - embedded_layout::align::horizontal::Right, - embedded_layout::align::vertical::Center, - ) - .draw(&mut top_bar_fbuf) - .unwrap(); - } - - LinearLayout::horizontal( - Chain::new(CrossedIcon::new( - Resources::WIFI, - !current_state.wifi_connected.unwrap_or(false), - 9, - )) - .append(CrossedIcon::new( - Resources::SERVER, - !current_state.server_connected.unwrap_or(false), - 9, - )) - .append(CrossedIcon::new( - Resources::TIMER, - !current_state.stackmat_connected.unwrap_or(false), - 9, - )), - ) - .with_alignment(embedded_layout::align::vertical::Center) - .with_spacing(FixedMargin(2)) - .arrange() - .align_to( - &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), - embedded_layout::align::horizontal::Left, - embedded_layout::align::vertical::Center, - ) - .draw(&mut top_bar_fbuf) - .unwrap(); - - Line::new(Point::new(0, 10), Point::new(128, 10)) - .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) - .draw(&mut top_bar_fbuf) - .unwrap(); - - disp.draw_iter(top_bar_fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); - if sleep_state() { //lcd.backlight_on(); @@ -357,136 +290,51 @@ pub async fn lcd_task( let current_scene = current_state.scene.clone(); let fut = async { - loop { - Timer::after_millis(100).await; - } - }; - - let res = embassy_futures::select::select(fut, global_state.state.wait()).await; - match res { - embassy_futures::select::Either::First(_) => {} - embassy_futures::select::Either::Second(_) => { - continue; - } - } - } - - /* - - let start = Instant::now(); - loop { - embedded_graphics::prelude::DrawTarget::clear( - &mut fbuf, - embedded_graphics::pixelcolor::BinaryColor::Off, - ); - Timer::after(embassy_time::Duration::from_millis(1000 / 60)).await; - - let text_style = embedded_graphics::mono_font::MonoTextStyleBuilder::new() - .font(&embedded_graphics::mono_font::ascii::FONT_6X10) - .text_color(embedded_graphics::pixelcolor::BinaryColor::On) - .build(); - - let time_str = crate::utils::stackmat::ms_to_time_str(start.elapsed().as_millis()); - embedded_graphics::Drawable::draw( - &embedded_graphics::text::Text::with_baseline( - &alloc::format!("Hello world! {time_str}"), - embedded_graphics::prelude::Point::zero(), - text_style, - embedded_graphics::text::Baseline::Top, - ), - &mut fbuf, - ) - .unwrap(); - - embedded_graphics::Drawable::draw( - &embedded_graphics::text::Text::with_baseline( - "Hello Rust!", - embedded_graphics::prelude::Point::new(0, 16), - text_style, - embedded_graphics::text::Baseline::Top, - ), - &mut fbuf, - ) - .unwrap(); - - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); - - if start.elapsed().as_secs() > 120 { - break; - } - } - */ - - top_bar_fbuf.clear(BinaryColor::Off); - //lcd_driver.display_on_oled(&mut fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, top_bar_fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); - - Timer::after_millis(2500).await; - - top_bar_fbuf.clear(BinaryColor::Off); - //_ = lcd_driver.clear_all(); - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, top_bar_fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); - - let mut last_update; - loop { - let current_state = global_state.state.value().await.clone(); - log::debug!("lcd current_state: {current_state:?}"); - last_update = Instant::now(); - - if sleep_state() { - //lcd.backlight_on(); - - unsafe { - crate::state::SLEEP_STATE = false; - } - } + oled.fbuf.clear(BinaryColor::Off); + _ = process_top_bar(¤t_state, &global_state, &mut oled).await; + _ = process_main(¤t_state, &global_state, &wifi_setup_sig, &mut oled).await; - let current_scene = current_state.scene.clone(); - let fut = async { - /* - let _ = process_lcd( - current_state, - &global_state, - &mut lcd_driver, - &wifi_setup_sig, - &mut fbuf, - &mut disp, - ) - .await; - fbuf.clear(BinaryColor::Off); - lcd_driver.display_on_oled(&mut fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); + oled.disp.draw_iter(oled.fbuf.into_iter()).unwrap(); + oled.disp.flush().await.unwrap(); - let mut scroll_ticker = - embassy_time::Ticker::every(Duration::from_millis(SCROLL_TICKER_INVERVAL_MS)); loop { - scroll_ticker.next().await; - let changed = lcd_driver.scroll_step(); - if changed.is_ok_and(|c| c) { - fbuf.clear(BinaryColor::Off); - lcd_driver.display_on_oled(&mut fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()) - .unwrap(); - disp.flush().await.unwrap(); - } + Timer::after_millis(1000).await; #[cfg(not(any(feature = "e2e", feature = "qa")))] if !sleep_state() && (Instant::now() - last_update).as_millis() > SLEEP_AFTER_MS && current_scene.can_sleep() { - _ = lcd_driver.print(0, "Sleep", PrintAlign::Center, true); - _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); - fbuf.clear(BinaryColor::Off); - lcd_driver.display_on_oled(&mut fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()) - .unwrap(); - disp.flush().await.unwrap(); - //lcd.backlight_off(); + oled.fbuf.clear(BinaryColor::Off); + + let style = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_7X13, + BinaryColor::On, + ); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Center) + .baseline(Baseline::Middle) + .build(); + + let text = Text::with_text_style( + "Sleep\nPress any key...", + Point::zero(), + style, + text_style, + ); + + LinearLayout::horizontal(Chain::new(text)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &Rectangle::new(Point::new(0, 0), Size::new(128, 64)), + embedded_layout::align::horizontal::Center, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf); + + oled.disp.draw_iter(oled.fbuf.into_iter()).unwrap(); + oled.disp.flush().await.unwrap(); { global_state.state.lock().await.server_connected = Some(false); @@ -505,17 +353,39 @@ pub async fn lcd_task( && !deeper_sleep_state() && (Instant::now() - last_update).as_millis() > DEEPER_SLEEP_AFTER_MS { - _ = lcd_driver.print(0, "Deep Sleep", PrintAlign::Center, true); - _ = lcd_driver.print(1, "Press any key", PrintAlign::Center, true); - fbuf.clear(BinaryColor::Off); - lcd_driver.display_on_oled(&mut fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(&mut disp, fbuf.into_iter()) - .unwrap(); - disp.flush().await.unwrap(); + oled.fbuf.clear(BinaryColor::Off); + let style = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_7X13, + BinaryColor::On, + ); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Center) + .baseline(Baseline::Middle) + .build(); + + let text = Text::with_text_style( + "Deep Sleep\nPress any key...", + Point::zero(), + style, + text_style, + ); + + LinearLayout::horizontal(Chain::new(text)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &Rectangle::new(Point::new(0, 0), Size::new(128, 64)), + embedded_layout::align::horizontal::Center, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf); + + oled.disp.draw_iter(oled.fbuf.into_iter()).unwrap(); + oled.disp.flush().await.unwrap(); + crate::utils::deeper_sleep(); } } - */ }; let res = embassy_futures::select::select(fut, global_state.state.wait()).await; @@ -528,6 +398,89 @@ pub async fn lcd_task( } } +async fn process_top_bar( + current_state: &SignaledGlobalStateInner, + _global_state: &GlobalState, + oled: &mut OledData<'_>, +) -> Result<()> { + let style = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_7X13, + BinaryColor::On, + ); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Right) + .baseline(Baseline::Top) + .build(); + + let text = format!("{}%", current_state.battery_status.0); + let text = Text::with_text_style(&text, Point::zero(), style, text_style); + if current_state.battery_status.1 { + LinearLayout::horizontal(Chain::new(Resources::CHARGING).append(text)) + .with_alignment(embedded_layout::align::vertical::Center) + .with_spacing(FixedMargin(1)) + .arrange() + .align_to( + &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), + embedded_layout::align::horizontal::Right, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf)?; + } else { + LinearLayout::horizontal(Chain::new(text)) + .with_alignment(embedded_layout::align::vertical::Center) + .with_spacing(FixedMargin(1)) + .arrange() + .align_to( + &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), + embedded_layout::align::horizontal::Right, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf)?; + } + + LinearLayout::horizontal( + Chain::new(CrossedIcon::new( + Resources::WIFI, + !current_state.wifi_connected.unwrap_or(false), + 9, + )) + .append(CrossedIcon::new( + Resources::SERVER, + !current_state.server_connected.unwrap_or(false), + 9, + )) + .append(CrossedIcon::new( + Resources::TIMER, + !current_state.stackmat_connected.unwrap_or(false), + 9, + )), + ) + .with_alignment(embedded_layout::align::vertical::Center) + .with_spacing(FixedMargin(2)) + .arrange() + .align_to( + &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), + embedded_layout::align::horizontal::Left, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf)?; + + Line::new(Point::new(0, 10), Point::new(128, 10)) + .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) + .draw(&mut oled.fbuf)?; + + Ok(()) +} + +async fn process_main( + current_state: &SignaledGlobalStateInner, + global_state: &GlobalState, + wifi_setup_sig: &Signal, + oled: &mut OledData<'_>, +) -> Result<()> { + Ok(()) +} + /* async fn process_lcd( current_state: SignaledGlobalStateInner, diff --git a/src/resources/SERVER.png b/src/resources/SERVER.png index 4d1c691570cd5c47157ce266bee8272f947cd90d..cb0872eab031d20cf6f5375211bce15f59e9118a 100644 GIT binary patch delta 133 zcmV;00DAwV0ipqrBz{*(L_t(|0ep_p761?kL*xJdnQrMpXE-)ln@qdc^6LxUGV1}v z;nd+$ZWYK*GSjj;tOA${*r+)JD$u{fJnOgNwb diff --git a/src/stackmat.rs b/src/stackmat.rs index 0d2ca08..2cb913d 100644 --- a/src/stackmat.rs +++ b/src/stackmat.rs @@ -53,7 +53,6 @@ pub async fn stackmat_task( let mut state = global_state.state.lock().await; state.stackmat_connected = Some(false); - // TODO: re-think this and maybe reset whole state? if state.scene == Scene::Timer { if state.current_competitor.is_some() { state.scene = Scene::CompetitorInfo; From 1f80a4fcd1674fdd4b425e1f6c5b2c35491d56de Mon Sep 17 00:00:00 2001 From: filipton Date: Thu, 12 Mar 2026 23:31:49 +0100 Subject: [PATCH 17/31] feat: proper startup screen --- src/battery_v4.rs | 10 ++- src/lcd_v4.rs | 193 ++++++++++++++++++++++++++++++++++++++++++++-- src/state.rs | 4 - 3 files changed, 193 insertions(+), 14 deletions(-) diff --git a/src/battery_v4.rs b/src/battery_v4.rs index e30462a..afc31eb 100644 --- a/src/battery_v4.rs +++ b/src/battery_v4.rs @@ -24,10 +24,9 @@ pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) return; }; + let mut startup_sent = false; let mut last_sent = Instant::now(); loop { - Timer::after_millis(100).await; - if sleep_state() { Timer::after_millis(500).await; continue; @@ -45,6 +44,11 @@ pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) state.battery_status = (soc, ma >= 0) } + if !startup_sent { + state.show_battery.signal(soc); + startup_sent = true; + } + if last_sent.elapsed().as_millis() >= BATTERY_SEND_INTERVAL_MS { if state.state.lock().await.server_connected == Some(true) { crate::ws::send_packet(crate::structs::TimerPacket { @@ -60,6 +64,8 @@ pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) log::info!("Battery {mv}mv {soc}% (avg current: {ma}mA)"); last_sent = Instant::now(); } + + Timer::after_millis(100).await; } } diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index 7e3c8e1..ebe75dd 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -1,5 +1,5 @@ use alloc::{format, rc::Rc, string::ToString}; -use anyhow::Result; +use anyhow::{Result, anyhow}; use display_interface_i2c::I2CInterface; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; use embassy_time::{Delay, Duration, Instant, Timer}; @@ -247,6 +247,17 @@ pub struct OledData<'a> { pub disp: GraphicsMode>, } +impl OledData<'_> { + pub async fn flush(&mut self) -> Result<()> { + self.disp + .draw_iter(self.fbuf.into_iter()) + .map_err(|e| anyhow!("{e:?}"))?; + self.disp.flush().await.map_err(|e| anyhow!("{e:?}"))?; + + Ok(()) + } +} + #[embassy_executor::task] pub async fn lcd_task( i2c: SharedI2C, @@ -274,6 +285,43 @@ pub async fn lcd_task( let mut oled = OledData { fbuf, disp }; + global_state.show_battery.wait().await; + _ = process_top_bar( + &global_state.state.value().await.clone(), + &global_state, + &mut oled, + ) + .await; + + let style = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_7X13, + BinaryColor::On, + ); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Center) + .baseline(Baseline::Middle) + .build(); + + let text = format!( + "S\\N: {:X}\nVER: {}", + crate::utils::get_efuse_u32(), + crate::version::VERSION + ); + let text = Text::with_text_style(&text, Point::zero(), style, text_style); + + LinearLayout::horizontal(Chain::new(text)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Center, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf); + _ = oled.flush().await; + + Timer::after_millis(2500).await; + let mut last_update; loop { let current_state = global_state.state.value().await.clone(); @@ -293,9 +341,7 @@ pub async fn lcd_task( oled.fbuf.clear(BinaryColor::Off); _ = process_top_bar(¤t_state, &global_state, &mut oled).await; _ = process_main(¤t_state, &global_state, &wifi_setup_sig, &mut oled).await; - - oled.disp.draw_iter(oled.fbuf.into_iter()).unwrap(); - oled.disp.flush().await.unwrap(); + _ = oled.flush().await; loop { Timer::after_millis(1000).await; @@ -333,8 +379,7 @@ pub async fn lcd_task( ) .draw(&mut oled.fbuf); - oled.disp.draw_iter(oled.fbuf.into_iter()).unwrap(); - oled.disp.flush().await.unwrap(); + _ = oled.flush().await; { global_state.state.lock().await.server_connected = Some(false); @@ -380,8 +425,7 @@ pub async fn lcd_task( ) .draw(&mut oled.fbuf); - oled.disp.draw_iter(oled.fbuf.into_iter()).unwrap(); - oled.disp.flush().await.unwrap(); + _ = oled.flush().await; crate::utils::deeper_sleep(); } @@ -472,15 +516,148 @@ async fn process_top_bar( Ok(()) } +pub const MAIN_RECT: Rectangle = Rectangle::new(Point::new(0, 11), Size::new(128, 53)); + async fn process_main( current_state: &SignaledGlobalStateInner, global_state: &GlobalState, wifi_setup_sig: &Signal, oled: &mut OledData<'_>, ) -> Result<()> { + let overwritten = process_main_overwrite(¤t_state, global_state, oled).await; + if overwritten { + return Ok(()); + } + Ok(()) } +async fn process_main_overwrite( + current_state: &SignaledGlobalStateInner, + _global_state: &GlobalState, + oled: &mut OledData<'_>, +) -> bool { + if !current_state.scene.can_be_lcd_overwritten() { + return false; + } + + if current_state.server_connected == Some(false) { + if current_state.wifi_connected == Some(false) { + let style = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_7X13, + BinaryColor::On, + ); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Center) + .baseline(Baseline::Middle) + .build(); + + let text = + Text::with_text_style("Wi-Fi\nConnection lost", Point::zero(), style, text_style); + + LinearLayout::horizontal(Chain::new(text)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Center, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf); + + oled.disp.draw_iter(oled.fbuf.into_iter()).unwrap(); + oled.disp.flush().await.unwrap(); + } else { + let style = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_7X13, + BinaryColor::On, + ); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Center) + .baseline(Baseline::Middle) + .build(); + + let text = format!( + "{}\n{}", + get_translation(TranslationKey::SERVER_DISCONNECTED_HEADER), + get_translation(TranslationKey::SERVER_DISCONNECTED_FOOTER) + ); + let text = Text::with_text_style(&text, Point::zero(), style, text_style); + + LinearLayout::horizontal(Chain::new(text)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Center, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf); + } + } else if current_state.device_added == Some(false) { + #[cfg(not(feature = "e2e"))] + let lines = ( + &get_translation(TranslationKey::DEVICE_NOT_ADDED_HEADER), + &get_translation(TranslationKey::DEVICE_NOT_ADDED_FOOTER), + ); + + #[cfg(feature = "e2e")] + let lines = ("Press submit", "To start HIL"); + + let style = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_7X13, + BinaryColor::On, + ); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Center) + .baseline(Baseline::Middle) + .build(); + + let text = format!("{}\n{}", lines.0, lines.1); + let text = Text::with_text_style(&text, Point::zero(), style, text_style); + + LinearLayout::horizontal(Chain::new(text)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Center, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf); + } else if current_state.stackmat_connected == Some(false) { + let style = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_7X13, + BinaryColor::On, + ); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Center) + .baseline(Baseline::Middle) + .build(); + + let text = format!( + "{}\n{}", + get_translation(TranslationKey::STACKMAT_DISCONNECTED_HEADER), + get_translation(TranslationKey::STACKMAT_DISCONNECTED_FOOTER) + ); + let text = Text::with_text_style(&text, Point::zero(), style, text_style); + + LinearLayout::horizontal(Chain::new(text)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Center, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf); + } else { + return false; + } + + true +} + /* async fn process_lcd( current_state: SignaledGlobalStateInner, diff --git a/src/state.rs b/src/state.rs index c91067e..b122195 100644 --- a/src/state.rs +++ b/src/state.rs @@ -151,8 +151,6 @@ pub struct GlobalStateInner { pub update_progress: Signal, pub sign_unsign_progress: Signal, pub ble_sig: Signal, - - #[cfg(feature = "v3")] pub show_battery: Signal, pub nvs: Nvs, @@ -171,8 +169,6 @@ impl GlobalStateInner { update_progress: Signal::new(), sign_unsign_progress: Signal::new(), ble_sig: Signal::new(), - - #[cfg(feature = "v3")] show_battery: Signal::new(), nvs: nvs.clone(), From 9865d8a89684883253c060fb3edb61603e600f21 Mon Sep 17 00:00:00 2001 From: filipton Date: Thu, 12 Mar 2026 23:48:17 +0100 Subject: [PATCH 18/31] wip: timer scene --- src/lcd_v4.rs | 93 ++++++++++++++------------------------------------- 1 file changed, 25 insertions(+), 68 deletions(-) diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index ebe75dd..595328a 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -303,7 +303,7 @@ pub async fn lcd_task( .build(); let text = format!( - "S\\N: {:X}\nVER: {}", + "S/N: {:X}\nVER: {}", crate::utils::get_efuse_u32(), crate::version::VERSION ); @@ -529,6 +529,29 @@ async fn process_main( return Ok(()); } + match current_state.scene { + Scene::Timer => { + oled.fbuf.fill_solid(&MAIN_RECT, BinaryColor::Off); + let text_rect = Rectangle::new(Point::new(0, 28), Size::new(128, 17)); + + loop { + let time = global_state.timer_signal.wait().await; + let time_str = ms_to_time_str(time); + let style = MonoTextStyle::new(&profont::PROFONT_14_POINT, BinaryColor::On); + let text_style = TextStyleBuilder::new() + .alignment(Alignment::Center) + .baseline(Baseline::Middle) + .build(); + + _ = oled.disp.fill_solid(&text_rect, BinaryColor::Off); + _ = Text::with_text_style(&time_str, Point::new(64, 36), style, text_style) + .draw(&mut oled.disp); + _ = oled.disp.flush().await; + } + } + _ => {} + } + Ok(()) } @@ -564,9 +587,6 @@ async fn process_main_overwrite( embedded_layout::align::vertical::Center, ) .draw(&mut oled.fbuf); - - oled.disp.draw_iter(oled.fbuf.into_iter()).unwrap(); - oled.disp.flush().await.unwrap(); } else { let style = MonoTextStyle::new( &embedded_graphics::mono_font::ascii::FONT_7X13, @@ -626,10 +646,7 @@ async fn process_main_overwrite( ) .draw(&mut oled.fbuf); } else if current_state.stackmat_connected == Some(false) { - let style = MonoTextStyle::new( - &embedded_graphics::mono_font::ascii::FONT_7X13, - BinaryColor::On, - ); + let style = MonoTextStyle::new(&profont::PROFONT_14_POINT, BinaryColor::On); let text_style = TextStyleBuilder::new() .alignment(Alignment::Center) .baseline(Baseline::Middle) @@ -1151,64 +1168,4 @@ async fn process_lcd( Some(()) } - -async fn process_lcd_overwrite( - current_state: &SignaledGlobalStateInner, - _global_state: &GlobalState, - lcd_driver: &mut LcdAbstract<80, 16, 2, 3>, -) -> bool { - if !current_state.scene.can_be_lcd_overwritten() { - return false; - } - - if current_state.server_connected == Some(false) { - if current_state.wifi_conn_lost { - // TODO: maybe add to this translation - _ = lcd_driver.print(0, "Wi-Fi", PrintAlign::Center, true); - _ = lcd_driver.print(1, "Connection lost", PrintAlign::Center, true); - } else { - _ = lcd_driver.print( - 0, - &get_translation(TranslationKey::SERVER_DISCONNECTED_HEADER), - PrintAlign::Center, - true, - ); - _ = lcd_driver.print( - 1, - &get_translation(TranslationKey::SERVER_DISCONNECTED_FOOTER), - PrintAlign::Center, - true, - ); - } - } else if current_state.device_added == Some(false) { - #[cfg(not(feature = "e2e"))] - let lines = ( - &get_translation(TranslationKey::DEVICE_NOT_ADDED_HEADER), - &get_translation(TranslationKey::DEVICE_NOT_ADDED_FOOTER), - ); - - #[cfg(feature = "e2e")] - let lines = ("Press submit", "To start HIL"); - - _ = lcd_driver.print(0, lines.0, PrintAlign::Center, true); - _ = lcd_driver.print(1, lines.1, PrintAlign::Center, true); - } else if current_state.stackmat_connected == Some(false) { - _ = lcd_driver.print( - 0, - &get_translation(TranslationKey::STACKMAT_DISCONNECTED_HEADER), - PrintAlign::Center, - true, - ); - _ = lcd_driver.print( - 1, - &get_translation(TranslationKey::STACKMAT_DISCONNECTED_FOOTER), - PrintAlign::Center, - true, - ); - } else { - return false; - } - - true -} */ From 7e2452d00e0e695c2dbd468816f7f311ad408b80 Mon Sep 17 00:00:00 2001 From: filipton Date: Fri, 13 Mar 2026 18:56:06 +0100 Subject: [PATCH 19/31] feat: slightly cleanup layouts code --- src/lcd_v4.rs | 449 +++++++----------------------------- src/utils/lcd_resourcese.rs | 213 +++++++++++++++++ src/utils/mod.rs | 3 + 3 files changed, 293 insertions(+), 372 deletions(-) create mode 100644 src/utils/lcd_resourcese.rs diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index 595328a..7ebed73 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -9,16 +9,16 @@ use embedded_graphics::{ pixelcolor::BinaryColor, prelude::*, primitives::{Arc, Circle, Line, PrimitiveStyle, Rectangle}, - text::{Alignment, Baseline, Text, TextStyleBuilder}, + text::{Alignment, Baseline, Text, TextStyle, TextStyleBuilder}, }; use embedded_graphics_framebuf::FrameBuf; use embedded_layout::{ - layout::linear::{FixedMargin, LinearLayout}, + layout::linear::{FixedMargin, Horizontal, LinearLayout}, prelude::*, + view_group::ViewGroup, }; use esp_hal::gpio::Output; use oled_async::{displays::ssd1309::Ssd1309_128_64, mode::GraphicsMode}; -use profont::PROFONT_12_POINT; use crate::{ consts::{ @@ -29,215 +29,13 @@ use crate::{ GlobalState, MenuScene, Scene, SignaledGlobalStateInner, deeper_sleep_state, sleep_state, }, translations::{TranslationKey, get_translation, get_translation_params}, - utils::{shared_i2c::SharedI2C, stackmat::ms_to_time_str}, + utils::{ + lcd_resourcese::{CrossedIcon, Resources}, + shared_i2c::SharedI2C, + stackmat::ms_to_time_str, + }, }; -macros::load_lcd_resources!("src/resources"); - -#[derive(Clone, Copy)] -pub struct PixelArt { - data: &'static [u8], // packed bits, MSB first, row by row - width: u32, - height: u32, - top_left: Point, -} - -impl PixelArt { - pub const fn new(data: &'static [u8], width: u32, height: u32) -> Self { - Self { - data, - width, - height, - top_left: Point::zero(), - } - } - - fn bytes_per_row(&self) -> u32 { - (self.width + 7) / 8 - } -} - -impl Dimensions for PixelArt { - fn bounding_box(&self) -> Rectangle { - Rectangle::new(self.top_left, Size::new(self.width, self.height)) - } -} - -impl Transform for PixelArt { - fn translate(&self, by: Point) -> Self { - Self { - top_left: self.top_left + by, - ..*self - } - } - fn translate_mut(&mut self, by: Point) -> &mut Self { - self.top_left += by; - self - } -} - -impl Drawable for PixelArt { - type Color = BinaryColor; - type Output = (); - - fn draw>(&self, target: &mut D) -> Result<(), D::Error> { - let origin = self.top_left; - let bpr = self.bytes_per_row(); - let (w, h) = (self.width, self.height); - let data = self.data; - - target.draw_iter((0..h).flat_map(move |y| { - (0..w).map(move |x| { - let byte = data[(y * bpr + x / 8) as usize]; - let on = byte & (0x80 >> (x % 8)) != 0; - Pixel( - origin + Point::new(x as i32, y as i32), - if on { - BinaryColor::On - } else { - BinaryColor::Off - }, - ) - }) - })) - } -} - -pub struct Overlay { - base: A, - overlay: B, - top_left: Point, -} - -impl Overlay { - pub fn new(base: A, overlay: B) -> Self { - let base_box = base.bounding_box(); - let overlay_box = overlay.bounding_box(); - - // center overlay over base - let offset = Point::new( - (base_box.size.width as i32 - overlay_box.size.width as i32) / 2, - (base_box.size.height as i32 - overlay_box.size.height as i32) / 2, - ); - let overlay = overlay.translate(base_box.top_left + offset); - - Self { - top_left: base_box.top_left, - base, - overlay, - } - } -} - -impl Dimensions for Overlay { - fn bounding_box(&self) -> Rectangle { - Rectangle::new(self.top_left, self.base.bounding_box().size) - } -} - -impl Transform for Overlay { - fn translate(&self, by: Point) -> Self { - Self { - top_left: self.top_left + by, - base: self.base.translate(by), - overlay: self.overlay.translate(by), - } - } - fn translate_mut(&mut self, by: Point) -> &mut Self { - self.top_left += by; - self.base.translate_mut(by); - self.overlay.translate_mut(by); - self - } -} - -impl Drawable for Overlay -where - A: Drawable, - B: Drawable, -{ - type Color = BinaryColor; - type Output = (); - - fn draw>(&self, target: &mut D) -> Result<(), D::Error> { - self.base.draw(target)?; - self.overlay.draw(target)?; - Ok(()) - } -} - -pub struct CrossedIcon { - base: A, - top_left: Point, - crossed: bool, - cross_size: u32, -} - -impl CrossedIcon { - pub fn new(base: A, crossed: bool, cross_size: u32) -> Self { - let base_box = base.bounding_box(); - Self { - base, - top_left: base_box.top_left, - crossed, - cross_size, - } - } -} - -impl Dimensions for CrossedIcon { - fn bounding_box(&self) -> Rectangle { - Rectangle::new(self.top_left, self.base.bounding_box().size) - } -} - -impl Transform for CrossedIcon { - fn translate(&self, by: Point) -> Self { - Self { - top_left: self.top_left + by, - base: self.base.translate(by), - crossed: self.crossed, - cross_size: self.cross_size, - } - } - fn translate_mut(&mut self, by: Point) -> &mut Self { - self.top_left += by; - self.base.translate_mut(by); - self - } -} - -impl Drawable for CrossedIcon -where - A: Drawable + Dimensions, -{ - type Color = BinaryColor; - type Output = (); - fn draw>(&self, target: &mut D) -> Result<(), D::Error> { - self.base.draw(target)?; - if self.crossed { - let thin_stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 1); - let bb = self.bounding_box(); - - // Center the cross over the bounding box - let offset = Point::new( - (bb.size.width as i32 - self.cross_size as i32) / 2, - (bb.size.height as i32 - self.cross_size as i32) / 2, - ); - let tl = bb.top_left + offset; - let s = self.cross_size as i32 - 1; - - Line::new(tl, tl + Point::new(s, s)) - .into_styled(thin_stroke) - .draw(target)?; - Line::new(tl + Point::new(0, s), tl + Point::new(s, 0)) - .into_styled(thin_stroke) - .draw(target)?; - } - Ok(()) - } -} - pub const FBUF_WIDTH: usize = 128; pub const FBUF_HEIGHT: usize = 64; pub const FBUF_SIZE: usize = FBUF_WIDTH * FBUF_HEIGHT; @@ -258,6 +56,39 @@ impl OledData<'_> { } } +pub const MAIN_RECT: Rectangle = Rectangle::new(Point::new(0, 11), Size::new(128, 53)); + +pub const NORMAL_FONT: MonoTextStyle<'_, BinaryColor> = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_7X13, + BinaryColor::On, +); + +pub const TIMER_FONT: MonoTextStyle<'_, BinaryColor> = + MonoTextStyle::new(&profont::PROFONT_14_POINT, BinaryColor::On); + +pub const TEXT_CENTER: TextStyle = TextStyleBuilder::new() + .alignment(Alignment::Center) + .baseline(Baseline::Middle) + .build(); + +pub const TEXT_TOPBAR: TextStyle = TextStyleBuilder::new() + .alignment(Alignment::Right) + .baseline(Baseline::Top) + .build(); + +fn center_layout( + content: VG, +) -> LinearLayout, VG> { + LinearLayout::horizontal(content) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Center, + embedded_layout::align::vertical::Center, + ) +} + #[embassy_executor::task] pub async fn lcd_task( i2c: SharedI2C, @@ -274,10 +105,18 @@ pub async fn lcd_task( let mut disp: oled_async::mode::GraphicsMode<_, _> = raw_disp.into(); disp.reset(&mut display_rst, &mut Delay); - // TODO: remove all these unwraps! - disp.init().await.unwrap(); - disp.clear(); - disp.flush().await.unwrap(); + let disp_init = async { + disp.init().await?; + disp.clear(); + disp.flush().await?; + + anyhow::Result::<(), display_interface::DisplayError>::Ok(()) + } + .await; + + if let Err(e) = disp_init { + log::error!("Disp init error: {e:?} (but continuing i guess)"); + } let mut data = alloc::vec![embedded_graphics::pixelcolor::BinaryColor::Off; FBUF_SIZE]; let data: &mut [BinaryColor; FBUF_SIZE] = data.as_mut_array().unwrap(); @@ -293,31 +132,14 @@ pub async fn lcd_task( ) .await; - let style = MonoTextStyle::new( - &embedded_graphics::mono_font::ascii::FONT_7X13, - BinaryColor::On, - ); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Center) - .baseline(Baseline::Middle) - .build(); - let text = format!( "S/N: {:X}\nVER: {}", crate::utils::get_efuse_u32(), crate::version::VERSION ); - let text = Text::with_text_style(&text, Point::zero(), style, text_style); + let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); - LinearLayout::horizontal(Chain::new(text)) - .with_alignment(embedded_layout::align::vertical::Center) - .arrange() - .align_to( - &MAIN_RECT, - embedded_layout::align::horizontal::Center, - embedded_layout::align::vertical::Center, - ) - .draw(&mut oled.fbuf); + center_layout(Chain::new(text)).draw(&mut oled.fbuf); _ = oled.flush().await; Timer::after_millis(2500).await; @@ -353,32 +175,14 @@ pub async fn lcd_task( { oled.fbuf.clear(BinaryColor::Off); - let style = MonoTextStyle::new( - &embedded_graphics::mono_font::ascii::FONT_7X13, - BinaryColor::On, - ); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Center) - .baseline(Baseline::Middle) - .build(); - let text = Text::with_text_style( "Sleep\nPress any key...", Point::zero(), - style, - text_style, + NORMAL_FONT, + TEXT_CENTER, ); - LinearLayout::horizontal(Chain::new(text)) - .with_alignment(embedded_layout::align::vertical::Center) - .arrange() - .align_to( - &Rectangle::new(Point::new(0, 0), Size::new(128, 64)), - embedded_layout::align::horizontal::Center, - embedded_layout::align::vertical::Center, - ) - .draw(&mut oled.fbuf); - + center_layout(Chain::new(text)).draw(&mut oled.fbuf); _ = oled.flush().await; { @@ -399,32 +203,14 @@ pub async fn lcd_task( && (Instant::now() - last_update).as_millis() > DEEPER_SLEEP_AFTER_MS { oled.fbuf.clear(BinaryColor::Off); - let style = MonoTextStyle::new( - &embedded_graphics::mono_font::ascii::FONT_7X13, - BinaryColor::On, - ); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Center) - .baseline(Baseline::Middle) - .build(); - let text = Text::with_text_style( "Deep Sleep\nPress any key...", Point::zero(), - style, - text_style, + NORMAL_FONT, + TEXT_CENTER, ); - LinearLayout::horizontal(Chain::new(text)) - .with_alignment(embedded_layout::align::vertical::Center) - .arrange() - .align_to( - &Rectangle::new(Point::new(0, 0), Size::new(128, 64)), - embedded_layout::align::horizontal::Center, - embedded_layout::align::vertical::Center, - ) - .draw(&mut oled.fbuf); - + center_layout(Chain::new(text)).draw(&mut oled.fbuf); _ = oled.flush().await; crate::utils::deeper_sleep(); @@ -447,17 +233,8 @@ async fn process_top_bar( _global_state: &GlobalState, oled: &mut OledData<'_>, ) -> Result<()> { - let style = MonoTextStyle::new( - &embedded_graphics::mono_font::ascii::FONT_7X13, - BinaryColor::On, - ); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Right) - .baseline(Baseline::Top) - .build(); - let text = format!("{}%", current_state.battery_status.0); - let text = Text::with_text_style(&text, Point::zero(), style, text_style); + let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_TOPBAR); if current_state.battery_status.1 { LinearLayout::horizontal(Chain::new(Resources::CHARGING).append(text)) .with_alignment(embedded_layout::align::vertical::Center) @@ -467,8 +244,7 @@ async fn process_top_bar( &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), embedded_layout::align::horizontal::Right, embedded_layout::align::vertical::Center, - ) - .draw(&mut oled.fbuf)?; + ); } else { LinearLayout::horizontal(Chain::new(text)) .with_alignment(embedded_layout::align::vertical::Center) @@ -516,8 +292,6 @@ async fn process_top_bar( Ok(()) } -pub const MAIN_RECT: Rectangle = Rectangle::new(Point::new(0, 11), Size::new(128, 53)); - async fn process_main( current_state: &SignaledGlobalStateInner, global_state: &GlobalState, @@ -537,14 +311,9 @@ async fn process_main( loop { let time = global_state.timer_signal.wait().await; let time_str = ms_to_time_str(time); - let style = MonoTextStyle::new(&profont::PROFONT_14_POINT, BinaryColor::On); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Center) - .baseline(Baseline::Middle) - .build(); _ = oled.disp.fill_solid(&text_rect, BinaryColor::Off); - _ = Text::with_text_style(&time_str, Point::new(64, 36), style, text_style) + _ = Text::with_text_style(&time_str, Point::new(64, 36), TIMER_FONT, TEXT_CENTER) .draw(&mut oled.disp); _ = oled.disp.flush().await; } @@ -566,53 +335,22 @@ async fn process_main_overwrite( if current_state.server_connected == Some(false) { if current_state.wifi_connected == Some(false) { - let style = MonoTextStyle::new( - &embedded_graphics::mono_font::ascii::FONT_7X13, - BinaryColor::On, - ); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Center) - .baseline(Baseline::Middle) - .build(); - - let text = - Text::with_text_style("Wi-Fi\nConnection lost", Point::zero(), style, text_style); - - LinearLayout::horizontal(Chain::new(text)) - .with_alignment(embedded_layout::align::vertical::Center) - .arrange() - .align_to( - &MAIN_RECT, - embedded_layout::align::horizontal::Center, - embedded_layout::align::vertical::Center, - ) - .draw(&mut oled.fbuf); - } else { - let style = MonoTextStyle::new( - &embedded_graphics::mono_font::ascii::FONT_7X13, - BinaryColor::On, + let text = Text::with_text_style( + "Wi-Fi\nConnection lost", + Point::zero(), + NORMAL_FONT, + TEXT_CENTER, ); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Center) - .baseline(Baseline::Middle) - .build(); + center_layout(Chain::new(text)).draw(&mut oled.fbuf); + } else { let text = format!( "{}\n{}", get_translation(TranslationKey::SERVER_DISCONNECTED_HEADER), get_translation(TranslationKey::SERVER_DISCONNECTED_FOOTER) ); - let text = Text::with_text_style(&text, Point::zero(), style, text_style); - - LinearLayout::horizontal(Chain::new(text)) - .with_alignment(embedded_layout::align::vertical::Center) - .arrange() - .align_to( - &MAIN_RECT, - embedded_layout::align::horizontal::Center, - embedded_layout::align::vertical::Center, - ) - .draw(&mut oled.fbuf); + let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); + center_layout(Chain::new(text)).draw(&mut oled.fbuf); } } else if current_state.device_added == Some(false) { #[cfg(not(feature = "e2e"))] @@ -624,50 +362,17 @@ async fn process_main_overwrite( #[cfg(feature = "e2e")] let lines = ("Press submit", "To start HIL"); - let style = MonoTextStyle::new( - &embedded_graphics::mono_font::ascii::FONT_7X13, - BinaryColor::On, - ); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Center) - .baseline(Baseline::Middle) - .build(); - let text = format!("{}\n{}", lines.0, lines.1); - let text = Text::with_text_style(&text, Point::zero(), style, text_style); - - LinearLayout::horizontal(Chain::new(text)) - .with_alignment(embedded_layout::align::vertical::Center) - .arrange() - .align_to( - &MAIN_RECT, - embedded_layout::align::horizontal::Center, - embedded_layout::align::vertical::Center, - ) - .draw(&mut oled.fbuf); + let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); + center_layout(Chain::new(text)).draw(&mut oled.fbuf); } else if current_state.stackmat_connected == Some(false) { - let style = MonoTextStyle::new(&profont::PROFONT_14_POINT, BinaryColor::On); - let text_style = TextStyleBuilder::new() - .alignment(Alignment::Center) - .baseline(Baseline::Middle) - .build(); - let text = format!( "{}\n{}", get_translation(TranslationKey::STACKMAT_DISCONNECTED_HEADER), get_translation(TranslationKey::STACKMAT_DISCONNECTED_FOOTER) ); - let text = Text::with_text_style(&text, Point::zero(), style, text_style); - - LinearLayout::horizontal(Chain::new(text)) - .with_alignment(embedded_layout::align::vertical::Center) - .arrange() - .align_to( - &MAIN_RECT, - embedded_layout::align::horizontal::Center, - embedded_layout::align::vertical::Center, - ) - .draw(&mut oled.fbuf); + let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); + center_layout(Chain::new(text)).draw(&mut oled.fbuf); } else { return false; } diff --git a/src/utils/lcd_resourcese.rs b/src/utils/lcd_resourcese.rs new file mode 100644 index 0000000..2f586ff --- /dev/null +++ b/src/utils/lcd_resourcese.rs @@ -0,0 +1,213 @@ +use embedded_graphics::{ + pixelcolor::BinaryColor, + prelude::*, + primitives::{Line, PrimitiveStyle, Rectangle}, +}; + +macros::load_lcd_resources!("src/resources"); + +#[derive(Clone, Copy)] +pub struct PixelArt { + data: &'static [u8], // packed bits, MSB first, row by row + width: u32, + height: u32, + top_left: Point, +} + +impl PixelArt { + pub const fn new(data: &'static [u8], width: u32, height: u32) -> Self { + Self { + data, + width, + height, + top_left: Point::zero(), + } + } + + fn bytes_per_row(&self) -> u32 { + (self.width + 7) / 8 + } +} + +impl Dimensions for PixelArt { + fn bounding_box(&self) -> Rectangle { + Rectangle::new(self.top_left, Size::new(self.width, self.height)) + } +} + +impl Transform for PixelArt { + fn translate(&self, by: Point) -> Self { + Self { + top_left: self.top_left + by, + ..*self + } + } + fn translate_mut(&mut self, by: Point) -> &mut Self { + self.top_left += by; + self + } +} + +impl Drawable for PixelArt { + type Color = BinaryColor; + type Output = (); + + fn draw>(&self, target: &mut D) -> Result<(), D::Error> { + let origin = self.top_left; + let bpr = self.bytes_per_row(); + let (w, h) = (self.width, self.height); + let data = self.data; + + target.draw_iter((0..h).flat_map(move |y| { + (0..w).map(move |x| { + let byte = data[(y * bpr + x / 8) as usize]; + let on = byte & (0x80 >> (x % 8)) != 0; + Pixel( + origin + Point::new(x as i32, y as i32), + if on { + BinaryColor::On + } else { + BinaryColor::Off + }, + ) + }) + })) + } +} + +#[allow(dead_code)] +pub struct Overlay { + base: A, + overlay: B, + top_left: Point, +} + +#[allow(dead_code)] +impl Overlay { + pub fn new(base: A, overlay: B) -> Self { + let base_box = base.bounding_box(); + let overlay_box = overlay.bounding_box(); + + // center overlay over base + let offset = Point::new( + (base_box.size.width as i32 - overlay_box.size.width as i32) / 2, + (base_box.size.height as i32 - overlay_box.size.height as i32) / 2, + ); + let overlay = overlay.translate(base_box.top_left + offset); + + Self { + top_left: base_box.top_left, + base, + overlay, + } + } +} + +impl Dimensions for Overlay { + fn bounding_box(&self) -> Rectangle { + Rectangle::new(self.top_left, self.base.bounding_box().size) + } +} + +impl Transform for Overlay { + fn translate(&self, by: Point) -> Self { + Self { + top_left: self.top_left + by, + base: self.base.translate(by), + overlay: self.overlay.translate(by), + } + } + fn translate_mut(&mut self, by: Point) -> &mut Self { + self.top_left += by; + self.base.translate_mut(by); + self.overlay.translate_mut(by); + self + } +} + +impl Drawable for Overlay +where + A: Drawable, + B: Drawable, +{ + type Color = BinaryColor; + type Output = (); + + fn draw>(&self, target: &mut D) -> Result<(), D::Error> { + self.base.draw(target)?; + self.overlay.draw(target)?; + Ok(()) + } +} + +pub struct CrossedIcon { + base: A, + top_left: Point, + crossed: bool, + cross_size: u32, +} + +impl CrossedIcon { + pub fn new(base: A, crossed: bool, cross_size: u32) -> Self { + let base_box = base.bounding_box(); + Self { + base, + top_left: base_box.top_left, + crossed, + cross_size, + } + } +} + +impl Dimensions for CrossedIcon { + fn bounding_box(&self) -> Rectangle { + Rectangle::new(self.top_left, self.base.bounding_box().size) + } +} + +impl Transform for CrossedIcon { + fn translate(&self, by: Point) -> Self { + Self { + top_left: self.top_left + by, + base: self.base.translate(by), + crossed: self.crossed, + cross_size: self.cross_size, + } + } + fn translate_mut(&mut self, by: Point) -> &mut Self { + self.top_left += by; + self.base.translate_mut(by); + self + } +} + +impl Drawable for CrossedIcon +where + A: Drawable + Dimensions, +{ + type Color = BinaryColor; + type Output = (); + fn draw>(&self, target: &mut D) -> Result<(), D::Error> { + self.base.draw(target)?; + if self.crossed { + let thin_stroke = PrimitiveStyle::with_stroke(BinaryColor::On, 1); + let bb = self.bounding_box(); + + // Center the cross over the bounding box + let offset = Point::new( + (bb.size.width as i32 - self.cross_size as i32) / 2, + (bb.size.height as i32 - self.cross_size as i32) / 2, + ); + let tl = bb.top_left + offset; + let s = self.cross_size as i32 - 1; + + Line::new(tl, tl + Point::new(s, s)) + .into_styled(thin_stroke) + .draw(target)?; + Line::new(tl + Point::new(0, s), tl + Point::new(s, 0)) + .into_styled(thin_stroke) + .draw(target)?; + } + Ok(()) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index c3b5dc3..057940e 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -12,6 +12,9 @@ pub mod lcd_abstract; #[cfg(feature = "v4")] pub mod shared_i2c; +#[cfg(feature = "v4")] +pub mod lcd_resourcese; + pub fn set_brownout_detection(state: bool) { unsafe { let rtc_cntl = &*esp32c3::RTC_CNTL::ptr(); From 46d3bdde94c35500ee3afb46f7413f8a3d850e45 Mon Sep 17 00:00:00 2001 From: filipton Date: Fri, 13 Mar 2026 19:24:50 +0100 Subject: [PATCH 20/31] feat: implement config menus --- src/lcd_v4.rs | 198 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 167 insertions(+), 31 deletions(-) diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index 7ebed73..9546770 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -54,6 +54,11 @@ impl OledData<'_> { Ok(()) } + + pub fn clear_main(&mut self) -> Result<()> { + self.fbuf.fill_solid(&MAIN_RECT, BinaryColor::Off); + Ok(()) + } } pub const MAIN_RECT: Rectangle = Rectangle::new(Point::new(0, 11), Size::new(128, 53)); @@ -89,6 +94,18 @@ fn center_layout( ) } +fn center_text_layout( + text: &str, +) -> LinearLayout< + Horizontal, + embedded_layout::object_chain::Chain< + embedded_graphics::text::Text<'_, MonoTextStyle<'_, BinaryColor>>, + >, +> { + let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); + center_layout(Chain::new(text)) +} + #[embassy_executor::task] pub async fn lcd_task( i2c: SharedI2C, @@ -151,8 +168,6 @@ pub async fn lcd_task( last_update = Instant::now(); if sleep_state() { - //lcd.backlight_on(); - unsafe { crate::state::SLEEP_STATE = false; } @@ -228,6 +243,34 @@ pub async fn lcd_task( } } +fn battery_layout( + content: VG, +) -> LinearLayout, VG> { + LinearLayout::horizontal(content) + .with_alignment(embedded_layout::align::vertical::Center) + .with_spacing(FixedMargin(1)) + .arrange() + .align_to( + &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), + embedded_layout::align::horizontal::Right, + embedded_layout::align::vertical::Center, + ) +} + +fn topbar_icons_layout( + content: VG, +) -> LinearLayout, VG> { + LinearLayout::horizontal(content) + .with_alignment(embedded_layout::align::vertical::Center) + .with_spacing(FixedMargin(2)) + .arrange() + .align_to( + &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), + embedded_layout::align::horizontal::Left, + embedded_layout::align::vertical::Center, + ) +} + async fn process_top_bar( current_state: &SignaledGlobalStateInner, _global_state: &GlobalState, @@ -236,29 +279,12 @@ async fn process_top_bar( let text = format!("{}%", current_state.battery_status.0); let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_TOPBAR); if current_state.battery_status.1 { - LinearLayout::horizontal(Chain::new(Resources::CHARGING).append(text)) - .with_alignment(embedded_layout::align::vertical::Center) - .with_spacing(FixedMargin(1)) - .arrange() - .align_to( - &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), - embedded_layout::align::horizontal::Right, - embedded_layout::align::vertical::Center, - ); + battery_layout(Chain::new(Resources::CHARGING).append(text)).draw(&mut oled.fbuf)?; } else { - LinearLayout::horizontal(Chain::new(text)) - .with_alignment(embedded_layout::align::vertical::Center) - .with_spacing(FixedMargin(1)) - .arrange() - .align_to( - &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), - embedded_layout::align::horizontal::Right, - embedded_layout::align::vertical::Center, - ) - .draw(&mut oled.fbuf)?; + battery_layout(Chain::new(text)).draw(&mut oled.fbuf)?; } - LinearLayout::horizontal( + topbar_icons_layout( Chain::new(CrossedIcon::new( Resources::WIFI, !current_state.wifi_connected.unwrap_or(false), @@ -275,14 +301,6 @@ async fn process_top_bar( 9, )), ) - .with_alignment(embedded_layout::align::vertical::Center) - .with_spacing(FixedMargin(2)) - .arrange() - .align_to( - &Rectangle::new(Point::new(0, 0), Size::new(128, 10)), - embedded_layout::align::horizontal::Left, - embedded_layout::align::vertical::Center, - ) .draw(&mut oled.fbuf)?; Line::new(Point::new(0, 10), Point::new(128, 10)) @@ -298,6 +316,124 @@ async fn process_main( wifi_setup_sig: &Signal, oled: &mut OledData<'_>, ) -> Result<()> { + if let Some(ref error_text) = current_state.error_text { + center_text_layout(&format!( + "{}\n{}", + get_translation(TranslationKey::ERROR_HEADER), + error_text + )) + .draw(&mut oled.fbuf)?; + + return Ok(()); + } + + // display custom message on top of everything! + if let Some((line1, line2)) = ¤t_state.custom_message { + center_text_layout(&format!("{line1}\n{line2}")).draw(&mut oled.fbuf)?; + return Ok(()); + } + + if let Some(sel) = current_state.selected_config_menu { + let lt = Text::with_text_style("<", Point::zero(), TIMER_FONT, TEXT_CENTER); + let gt = Text::with_text_style(">", Point::zero(), TIMER_FONT, TEXT_CENTER); + LinearLayout::horizontal(Chain::new(lt)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Left, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf)?; + + LinearLayout::horizontal(Chain::new(gt)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Right, + embedded_layout::align::vertical::Center, + ) + .draw(&mut oled.fbuf)?; + + // TODO: this can be "scrollable" list btw + center_text_layout(&format!( + "Config Menu\n{}. {}", + sel + 1, + crate::structs::CONFIG_MENU_ITEMS[sel] + )) + .draw(&mut oled.fbuf)?; + + return Ok(()); + } + + match current_state.menu_scene { + Some(MenuScene::Signing) | Some(MenuScene::Unsigning) => { + let prefix = if current_state.menu_scene == Some(MenuScene::Signing) { + "S" + } else { + "Uns" + }; + + let main_text = format!("{prefix}igning"); + + if global_state.sign_unsign_progress.signaled() { + let status = if global_state.sign_unsign_progress.wait().await { + "OK" + } else { + "FAIL" + }; + + center_text_layout(&format!("{main_text}\nOperation: {status}")) + .draw(&mut oled.fbuf)?; + oled.flush().await?; + + Timer::after_millis(300).await; + oled.clear_main()?; + center_text_layout(&format!( + "{main_text}\nScan the card\n\nPress Submit to exit" + )) + .draw(&mut oled.fbuf)?; + } else { + center_text_layout(&format!( + "{main_text}\nScan the card\n\nPress Submit to exit" + )) + .draw(&mut oled.fbuf)?; + } + + return Ok(()); + } + Some(crate::state::MenuScene::BtDisplay) => { + if current_state.selected_bluetooth_item + == current_state.discovered_bluetooth_devices.len() + { + center_text_layout("BT Display:\nUnpair").draw(&mut oled.fbuf)?; + } else if current_state.selected_bluetooth_item + == current_state.discovered_bluetooth_devices.len() + 1 + { + center_text_layout("BT Display:\nExit").draw(&mut oled.fbuf)?; + } else if current_state.selected_bluetooth_item + < current_state.discovered_bluetooth_devices.len() + { + if let Some(display_dev) = current_state + .discovered_bluetooth_devices + .get(current_state.selected_bluetooth_item) + { + center_text_layout(&format!( + "BT Display:\n{} [{:X?}]", + display_dev.name, display_dev.addr + )) + .draw(&mut oled.fbuf)?; + } + } else { + global_state.state.lock().await.selected_bluetooth_item = 0; + } + + return Ok(()); + } + None => {} + } + let overwritten = process_main_overwrite(¤t_state, global_state, oled).await; if overwritten { return Ok(()); @@ -305,7 +441,7 @@ async fn process_main( match current_state.scene { Scene::Timer => { - oled.fbuf.fill_solid(&MAIN_RECT, BinaryColor::Off); + oled.clear_main()?; let text_rect = Rectangle::new(Point::new(0, 28), Size::new(128, 17)); loop { From e7649dba187e453c51db8b095affcfd3cfbca331 Mon Sep 17 00:00:00 2001 From: filipton Date: Fri, 13 Mar 2026 20:15:32 +0100 Subject: [PATCH 21/31] feat: implement the rest of oled display code --- src/board.rs | 22 +- src/consts.rs | 1 + src/lcd_v4.rs | 738 ++++++++++++------------------------ src/utils/buttons.rs | 4 +- src/utils/lcd_resourcese.rs | 2 +- src/utils/shared_i2c.rs | 17 +- 6 files changed, 271 insertions(+), 513 deletions(-) diff --git a/src/board.rs b/src/board.rs index 35a1402..7473e1f 100644 --- a/src/board.rs +++ b/src/board.rs @@ -112,19 +112,23 @@ impl Board { InputConfig::default().with_pull(Pull::Down), ); - let Ok(i2c) = esp_hal::i2c::master::I2c::new( + let i2c = esp_hal::i2c::master::I2c::new( peripherals.I2C0, esp_hal::i2c::master::Config::default() .with_frequency(esp_hal::time::Rate::from_khz(400)), - ) else { - log::error!("Rfid task error while creating Spi instance!"); - panic!() + ); + + let i2c = match i2c { + Ok(i2c) => { + let i2c = i2c + .with_sda(peripherals.GPIO8) + .with_scl(peripherals.GPIO9) + .into_async(); + + SharedI2C::new(Some(i2c)) + } + Err(_) => SharedI2C::new(None), }; - let i2c = i2c - .with_sda(peripherals.GPIO8) - .with_scl(peripherals.GPIO9) - .into_async(); - let i2c = SharedI2C::new(i2c); Board { timg0, diff --git a/src/consts.rs b/src/consts.rs index f0ad7c4..9e00f4a 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -16,6 +16,7 @@ pub const LOG_SEND_INTERVAL_MS: u64 = 5000; pub const BATTERY_SEND_INTERVAL_MS: u64 = 60000; +#[allow(dead_code)] pub const SCROLL_TICKER_INVERVAL_MS: u64 = 500; pub const LCD_INSPECTION_FRAME_TIME: u64 = 1000 / 30; diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index 9546770..2f47318 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -2,13 +2,13 @@ use alloc::{format, rc::Rc, string::ToString}; use anyhow::{Result, anyhow}; use display_interface_i2c::I2CInterface; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal}; -use embassy_time::{Delay, Duration, Instant, Timer}; +use embassy_time::{Delay, Instant, Timer}; use embedded_graphics::{ - Drawable, Pixel, + Drawable, mono_font::MonoTextStyle, pixelcolor::BinaryColor, prelude::*, - primitives::{Arc, Circle, Line, PrimitiveStyle, Rectangle}, + primitives::{Line, PrimitiveStyle, Rectangle}, text::{Alignment, Baseline, Text, TextStyle, TextStyleBuilder}, }; use embedded_graphics_framebuf::FrameBuf; @@ -22,8 +22,7 @@ use oled_async::{displays::ssd1309::Ssd1309_128_64, mode::GraphicsMode}; use crate::{ consts::{ - DEEPER_SLEEP_AFTER_MS, INSPECTION_TIME_PLUS2, LCD_INSPECTION_FRAME_TIME, - SCROLL_TICKER_INVERVAL_MS, SLEEP_AFTER_MS, + DEEPER_SLEEP_AFTER_MS, INSPECTION_TIME_PLUS2, LCD_INSPECTION_FRAME_TIME, SLEEP_AFTER_MS, }, state::{ GlobalState, MenuScene, Scene, SignaledGlobalStateInner, deeper_sleep_state, sleep_state, @@ -48,7 +47,7 @@ pub struct OledData<'a> { impl OledData<'_> { pub async fn flush(&mut self) -> Result<()> { self.disp - .draw_iter(self.fbuf.into_iter()) + .draw_iter(&self.fbuf) .map_err(|e| anyhow!("{e:?}"))?; self.disp.flush().await.map_err(|e| anyhow!("{e:?}"))?; @@ -102,7 +101,7 @@ fn center_text_layout( embedded_graphics::text::Text<'_, MonoTextStyle<'_, BinaryColor>>, >, > { - let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); + let text = Text::with_text_style(text, Point::zero(), NORMAL_FONT, TEXT_CENTER); center_layout(Chain::new(text)) } @@ -136,7 +135,10 @@ pub async fn lcd_task( } let mut data = alloc::vec![embedded_graphics::pixelcolor::BinaryColor::Off; FBUF_SIZE]; - let data: &mut [BinaryColor; FBUF_SIZE] = data.as_mut_array().unwrap(); + let Some(data): Option<&mut [BinaryColor; FBUF_SIZE]> = data.as_mut_array() else { + log::error!("Disp framebuffer data alloc failed!"); + return; + }; let fbuf = embedded_graphics_framebuf::FrameBuf::new(data, FBUF_WIDTH, FBUF_HEIGHT); let mut oled = OledData { fbuf, disp }; @@ -434,447 +436,129 @@ async fn process_main( None => {} } - let overwritten = process_main_overwrite(¤t_state, global_state, oled).await; + let overwritten = process_main_overwrite(current_state, global_state, oled).await; if overwritten { return Ok(()); } - match current_state.scene { - Scene::Timer => { - oled.clear_main()?; - let text_rect = Rectangle::new(Point::new(0, 28), Size::new(128, 17)); - - loop { - let time = global_state.timer_signal.wait().await; - let time_str = ms_to_time_str(time); - - _ = oled.disp.fill_solid(&text_rect, BinaryColor::Off); - _ = Text::with_text_style(&time_str, Point::new(64, 36), TIMER_FONT, TEXT_CENTER) - .draw(&mut oled.disp); - _ = oled.disp.flush().await; - } - } - _ => {} - } - - Ok(()) -} - -async fn process_main_overwrite( - current_state: &SignaledGlobalStateInner, - _global_state: &GlobalState, - oled: &mut OledData<'_>, -) -> bool { - if !current_state.scene.can_be_lcd_overwritten() { - return false; - } - - if current_state.server_connected == Some(false) { - if current_state.wifi_connected == Some(false) { - let text = Text::with_text_style( - "Wi-Fi\nConnection lost", - Point::zero(), - NORMAL_FONT, - TEXT_CENTER, - ); - - center_layout(Chain::new(text)).draw(&mut oled.fbuf); - } else { - let text = format!( - "{}\n{}", - get_translation(TranslationKey::SERVER_DISCONNECTED_HEADER), - get_translation(TranslationKey::SERVER_DISCONNECTED_FOOTER) - ); - let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); - center_layout(Chain::new(text)).draw(&mut oled.fbuf); - } - } else if current_state.device_added == Some(false) { - #[cfg(not(feature = "e2e"))] - let lines = ( - &get_translation(TranslationKey::DEVICE_NOT_ADDED_HEADER), - &get_translation(TranslationKey::DEVICE_NOT_ADDED_FOOTER), - ); - - #[cfg(feature = "e2e")] - let lines = ("Press submit", "To start HIL"); - - let text = format!("{}\n{}", lines.0, lines.1); - let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); - center_layout(Chain::new(text)).draw(&mut oled.fbuf); - } else if current_state.stackmat_connected == Some(false) { - let text = format!( - "{}\n{}", - get_translation(TranslationKey::STACKMAT_DISCONNECTED_HEADER), - get_translation(TranslationKey::STACKMAT_DISCONNECTED_FOOTER) - ); - let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); - center_layout(Chain::new(text)).draw(&mut oled.fbuf); - } else { - return false; - } - - true -} - -/* -async fn process_lcd( - current_state: SignaledGlobalStateInner, - global_state: &GlobalState, - lcd_driver: &mut LcdAbstract<80, 16, 2, 3>, - //lcd: &mut LcdDisplay, - wifi_setup_sig: &Signal, - //display: &ShifterValueRange, - fbuf: &mut FrameBuf, - disp: &mut GraphicsMode>, -) -> Option<()> { - #[cfg(feature = "bat_dev_lcd")] - { - let battery_read = current_state.current_bat_read.unwrap_or(-1.0); - lcd_driver - .print( - 0, - &alloc::format!("BAT: {battery_read}"), - PrintAlign::Left, - true, - ) - .ok()?; - - if let Some(avg) = current_state.avg_bat_read { - lcd_driver - .print(1, &alloc::format!("AVG: {avg}"), PrintAlign::Left, true) - .ok()?; - } - - return Some(()); - } - - if let Some(error_text) = current_state.error_text { - lcd_driver - .print( - 0, - &get_translation(TranslationKey::ERROR_HEADER), - PrintAlign::Center, - true, - ) - .ok()?; - - lcd_driver - .print(1, &error_text, PrintAlign::Center, true) - .ok()?; - - return Some(()); - } - - // display custom message on top of everything! - if let Some((line1, line2)) = ¤t_state.custom_message { - _ = lcd_driver.print(0, line1, PrintAlign::Center, true); - _ = lcd_driver.print(1, line2, PrintAlign::Center, true); - - return Some(()); - } - - if let Some(sel) = current_state.selected_config_menu { - lcd_driver.clear_all().ok()?; - lcd_driver.print(0, "<", PrintAlign::Left, false).ok()?; - lcd_driver.print(0, ">", PrintAlign::Right, false).ok()?; - - lcd_driver - .print(0, "Config Menu", PrintAlign::Center, false) - .ok()?; - - lcd_driver - .print( - 1, - &alloc::format!("{}. {}", sel + 1, crate::structs::CONFIG_MENU_ITEMS[sel]), - PrintAlign::Left, - true, - ) - .ok()?; - - return Some(()); - } - - match current_state.menu_scene { - Some(MenuScene::Signing) | Some(MenuScene::Unsigning) => { - let prefix = if current_state.menu_scene == Some(MenuScene::Signing) { - "S" - } else { - "Uns" - }; - - lcd_driver.clear_all().ok()?; - lcd_driver - .print( - 0, - &alloc::format!("{prefix}igning | Submit To Exit"), - PrintAlign::Left, - true, - ) - .ok()?; - - if global_state.sign_unsign_progress.signaled() { - let status = if global_state.sign_unsign_progress.wait().await { - "OK" - } else { - "FAIL" - }; - - lcd_driver - .print( - 1, - &alloc::format!("Operation {}", status), - PrintAlign::Center, - true, - ) - .ok()?; - fbuf.clear(BinaryColor::Off); - lcd_driver.display_on_oled(fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(disp, fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); - - Timer::after_millis(300).await; - lcd_driver - .print(1, "Scan the card", PrintAlign::Center, true) - .ok()?; - } else { - lcd_driver - .print(1, "Scan the card", PrintAlign::Center, true) - .ok()?; - } - - return Some(()); - } - Some(crate::state::MenuScene::BtDisplay) => { - lcd_driver.clear_all().ok()?; - if current_state.selected_bluetooth_item - == current_state.discovered_bluetooth_devices.len() - { - lcd_driver - .print(0, "Unpair", PrintAlign::Center, true) - .ok()?; - } else if current_state.selected_bluetooth_item - == current_state.discovered_bluetooth_devices.len() + 1 - { - lcd_driver.print(0, "Exit", PrintAlign::Center, true).ok()?; - } else if current_state.selected_bluetooth_item - < current_state.discovered_bluetooth_devices.len() - { - if let Some(display_dev) = current_state - .discovered_bluetooth_devices - .get(current_state.selected_bluetooth_item) - { - lcd_driver - .print(0, &display_dev.name, PrintAlign::Center, true) - .ok()?; - - lcd_driver - .print( - 1, - &alloc::format!("{:x?}", display_dev.addr), - PrintAlign::Center, - true, - ) - .ok()?; - } - } else { - global_state.state.lock().await.selected_bluetooth_item = 0; - } - - return Some(()); - } - None => {} - } - - let overwritten = process_lcd_overwrite(¤t_state, global_state, lcd_driver).await; - if overwritten { - return Some(()); - } - - lcd_driver.clear_all().ok()?; if let Some(time) = current_state.delegate_hold { let delegate_remaining = 3 - time; if delegate_remaining == 0 { - lcd_driver - .print( - 0, - &get_translation(TranslationKey::WAITING_FOR_DELEGATE_HEADER), - PrintAlign::Center, - true, - ) - .ok()?; - - lcd_driver - .print( - 1, - &get_translation(TranslationKey::WAITING_FOR_DELEGATE_FOOTER), - PrintAlign::Center, - true, - ) - .ok()?; + center_text_layout(&format!( + "{}\n{}", + get_translation(TranslationKey::WAITING_FOR_DELEGATE_HEADER), + get_translation(TranslationKey::WAITING_FOR_DELEGATE_FOOTER) + )) + .draw(&mut oled.fbuf)?; } else { - lcd_driver - .print( - 0, - &get_translation(TranslationKey::CALLING_FOR_DELEGATE_HEADER), - PrintAlign::Center, - true, - ) - .ok()?; - - lcd_driver - .print( - 1, - &get_translation_params( - TranslationKey::CALLING_FOR_DELEGATE_FOOTER, - &[delegate_remaining], - ), - PrintAlign::Center, - true, + center_text_layout(&format!( + "{}\n{}", + get_translation(TranslationKey::WAITING_FOR_DELEGATE_HEADER), + get_translation_params( + TranslationKey::CALLING_FOR_DELEGATE_FOOTER, + &[delegate_remaining], ) - .ok()?; + )) + .draw(&mut oled.fbuf)?; } - return Some(()); + return Ok(()); } match current_state.scene { Scene::WifiConnect => { - lcd_driver - .print( - 0, - &get_translation(TranslationKey::WAITING_FOR_WIFI_HEADER), - PrintAlign::Center, - true, - ) - .ok()?; - - lcd_driver - .print( - 1, - &get_translation(TranslationKey::WAITING_FOR_WIFI_FOOTER), - PrintAlign::Center, - true, - ) - .ok()?; + center_text_layout(&format!( + "{}\n{}", + get_translation(TranslationKey::WAITING_FOR_WIFI_HEADER), + get_translation(TranslationKey::WAITING_FOR_WIFI_FOOTER) + )) + .draw(&mut oled.fbuf)?; + oled.flush().await?; - fbuf.clear(BinaryColor::Off); - lcd_driver.display_on_oled(fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(disp, fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); wifi_setup_sig.wait().await; global_state.state.lock().await.scene = Scene::AutoSetupWait; } Scene::AutoSetupWait => { let wifi_ssid = alloc::format!("FKM-{:X}", crate::utils::get_efuse_u32()); - lcd_driver - .print( - 0, - &get_translation(TranslationKey::WIFI_SETUP_HEADER), - PrintAlign::Center, - true, - ) - .ok()?; - lcd_driver - .print(1, &wifi_ssid, PrintAlign::Center, true) - .ok()?; + center_text_layout(&format!( + "{}\n{wifi_ssid}", + get_translation(TranslationKey::WIFI_SETUP_HEADER), + )) + .draw(&mut oled.fbuf)?; } Scene::MdnsWait => { - lcd_driver - .print( - 0, - &get_translation(TranslationKey::WAITING_FOR_MDNS_HEADER), - PrintAlign::Center, - true, - ) - .ok()?; - - lcd_driver - .print( - 1, - &get_translation(TranslationKey::WAITING_FOR_MDNS_FOOTER), - PrintAlign::Center, - true, - ) - .ok()?; + center_text_layout(&format!( + "{}\n{}", + get_translation(TranslationKey::WAITING_FOR_MDNS_HEADER), + get_translation(TranslationKey::WAITING_FOR_MDNS_FOOTER) + )) + .draw(&mut oled.fbuf)?; } Scene::GroupSelect => { - lcd_driver.clear_all().ok()?; - lcd_driver.print(0, "<", PrintAlign::Left, false).ok()?; - lcd_driver.print(1, "<", PrintAlign::Left, false).ok()?; - lcd_driver.print(0, ">", PrintAlign::Right, false).ok()?; - lcd_driver.print(1, ">", PrintAlign::Right, false).ok()?; - - lcd_driver - .print( - 0, - &get_translation(TranslationKey::SELECT_GROUP), - PrintAlign::Center, - false, + let lt = Text::with_text_style("<", Point::zero(), TIMER_FONT, TEXT_CENTER); + let gt = Text::with_text_style(">", Point::zero(), TIMER_FONT, TEXT_CENTER); + LinearLayout::horizontal(Chain::new(lt)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Left, + embedded_layout::align::vertical::Center, ) - .ok()?; - - lcd_driver - .print( - 1, - ¤t_state.possible_groups[current_state.group_selected_idx].secondary_text, - PrintAlign::Center, - false, + .draw(&mut oled.fbuf)?; + + LinearLayout::horizontal(Chain::new(gt)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Right, + embedded_layout::align::vertical::Center, ) - .ok()?; + .draw(&mut oled.fbuf)?; + + center_text_layout(&format!( + "{}\n{}", + get_translation(TranslationKey::SELECT_GROUP), + current_state.possible_groups[current_state.group_selected_idx].secondary_text + )) + .draw(&mut oled.fbuf)?; } Scene::WaitingForCompetitor => { - lcd_driver - .print( - 0, - &get_translation(TranslationKey::SCAN_COMPETITOR_CARD_HEADER), - PrintAlign::Center, - true, - ) - .ok()?; - if let Some(solve_time) = current_state.solve_time { let time_str = ms_to_time_str(solve_time); - lcd_driver - .print( - 1, - &get_translation_params( - TranslationKey::SCAN_COMPETITOR_CARD_WITH_TIME_FOOTER, - &[time_str], - ), - PrintAlign::Center, - true, + center_text_layout(&format!( + "{}\n{}", + get_translation(TranslationKey::SCAN_COMPETITOR_CARD_HEADER), + &get_translation_params( + TranslationKey::SCAN_COMPETITOR_CARD_WITH_TIME_FOOTER, + &[time_str], ) - .ok()?; + )) + .draw(&mut oled.fbuf)?; } else { - lcd_driver - .print( - 1, - &get_translation(TranslationKey::SCAN_COMPETITOR_CARD_FOOTER), - PrintAlign::Center, - true, - ) - .ok()?; + center_text_layout(&format!( + "{}\n{}", + get_translation(TranslationKey::SCAN_COMPETITOR_CARD_HEADER), + get_translation(TranslationKey::SCAN_COMPETITOR_CARD_FOOTER), + )) + .draw(&mut oled.fbuf)?; } } Scene::CompetitorInfo => { - lcd_driver - .print( - 0, - ¤t_state - .competitor_display - .unwrap_or("??????".to_string()), - PrintAlign::Center, - true, - ) - .ok()?; - - if let Some(group) = current_state.solve_group { - lcd_driver - .print(1, &group.secondary_text, PrintAlign::Center, true) - .ok()?; + let mut text = current_state + .competitor_display + .clone() + .unwrap_or("------".to_string()) + .to_string(); + + if let Some(ref group) = current_state.solve_group { + text += &format!("\n{}", group.secondary_text); } + + center_text_layout(&text).draw(&mut oled.fbuf)?; } Scene::Inspection => { let inspection_start = global_state @@ -884,45 +568,39 @@ async fn process_lcd( .inspection_start .unwrap_or(Instant::now()); - lcd_driver - .print(1, "Inspection", PrintAlign::Center, true) - .ok()?; + _ = Text::with_text_style("Inspection", Point::new(64, 44), NORMAL_FONT, TEXT_CENTER) + .draw(&mut oled.disp); + let text_rect = Rectangle::new(Point::new(0, 28), Size::new(128, 17)); loop { let elapsed = (Instant::now() - inspection_start).as_millis(); let time_str = ms_to_time_str(elapsed); - lcd_driver - .print(0, &time_str, PrintAlign::Center, true) - .ok()?; + _ = oled.disp.fill_solid(&text_rect, BinaryColor::Off); + _ = Text::with_text_style(&time_str, Point::new(64, 36), TIMER_FONT, TEXT_CENTER) + .draw(&mut oled.disp); + oled.flush().await?; - fbuf.clear(BinaryColor::Off); - lcd_driver.display_on_oled(fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(disp, fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); Timer::after_millis(LCD_INSPECTION_FRAME_TIME).await; } } - Scene::Timer => loop { - let time = global_state.timer_signal.wait().await; - let time_str = ms_to_time_str(time); - lcd_driver - .print(0, &time_str, PrintAlign::Center, true) - .ok()?; - - //display.set_data_raw(&crate::utils::stackmat::time_str_to_display(&time_str)); - fbuf.clear(BinaryColor::Off); - lcd_driver.display_on_oled(fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(disp, fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); - }, + Scene::Timer => { + oled.clear_main()?; + let text_rect = Rectangle::new(Point::new(0, 28), Size::new(128, 17)); + + loop { + let time = global_state.timer_signal.wait().await; + let time_str = ms_to_time_str(time); + + _ = oled.disp.fill_solid(&text_rect, BinaryColor::Off); + _ = Text::with_text_style(&time_str, Point::new(64, 36), TIMER_FONT, TEXT_CENTER) + .draw(&mut oled.disp); + oled.flush().await?; + } + } Scene::Finished => { let solve_time = current_state.solve_time.unwrap_or(0); - let time_str = if solve_time > 0 { - ms_to_time_str(solve_time) - } else { - heapless::String::new() - }; + let time_str = ms_to_time_str(solve_time); let inspection_time = match (current_state.inspection_start, current_state.inspection_end) { @@ -932,81 +610,149 @@ async fn process_lcd( _ => None, }; - if current_state.use_inspection() + let time_display = if current_state.use_inspection() && inspection_time.unwrap_or(0) > INSPECTION_TIME_PLUS2 { - let inspections_seconds = inspection_time.unwrap_or(0) / 1000; - lcd_driver - .print( - 0, - &alloc::format!("{time_str} ({inspections_seconds}s)"), - PrintAlign::Left, - true, - ) - .ok()?; + let inspection_seconds = inspection_time.unwrap_or(0) / 1000; + alloc::format!("{time_str}+{inspection_seconds}s") } else { - lcd_driver - .print(0, &time_str, PrintAlign::Left, true) - .ok()?; - } + alloc::format!("{time_str}") + }; let penalty = current_state.penalty.unwrap_or(0); - let penalty_str = match penalty { - -2 => "DNS", - -1 => "DNF", - 1.. => &alloc::format!("+{penalty}"), - _ => "", + let penalty_str: alloc::string::String = match penalty { + -2 => "DNS".into(), + -1 => "DNF".into(), + 1.. => alloc::format!("+{penalty}"), + _ => alloc::string::String::new(), }; - lcd_driver - .print(0, penalty_str, PrintAlign::Right, false) - .ok()?; - - if !current_state.time_confirmed { - lcd_driver - .print( - 1, - &get_translation(TranslationKey::CONFIRM_TIME), - PrintAlign::Right, - true, + // Time + penalty centered in MAIN_RECT + // If there's a penalty, show them as a horizontal chain; otherwise just time + if penalty_str.is_empty() { + let time_text = + Text::with_text_style(&time_display, Point::zero(), TIMER_FONT, TEXT_CENTER); + LinearLayout::horizontal(Chain::new(time_text)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Center, + embedded_layout::align::vertical::Center, ) - .ok()?; - } else if current_state.current_judge.is_none() { - lcd_driver - .print( - 1, - &get_translation(TranslationKey::SCAN_JUDGE_CARD), - PrintAlign::Right, - true, + .draw(&mut oled.fbuf)?; + } else { + let time_text = + Text::with_text_style(&time_display, Point::zero(), TIMER_FONT, TEXT_CENTER); + let penalty_text = + Text::with_text_style(&penalty_str, Point::zero(), TIMER_FONT, TEXT_CENTER); + LinearLayout::horizontal(Chain::new(time_text).append(penalty_text)) + .with_alignment(embedded_layout::align::vertical::Center) + .with_spacing(embedded_layout::layout::linear::spacing::FixedMargin(4)) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Center, + embedded_layout::align::vertical::Center, ) - .ok()?; + .draw(&mut oled.fbuf)?; + } + + // Status prompt pinned to bottom center + let status = if !current_state.time_confirmed { + Some(get_translation(TranslationKey::CONFIRM_TIME)) + } else if current_state.current_judge.is_none() { + Some(get_translation(TranslationKey::SCAN_JUDGE_CARD)) } else if current_state.current_competitor.is_some() && current_state.current_judge.is_some() { - lcd_driver - .print( - 1, - &get_translation(TranslationKey::SCAN_COMPETITOR_CARD), - PrintAlign::Right, - true, + Some(get_translation(TranslationKey::SCAN_COMPETITOR_CARD)) + } else { + None + }; + + if let Some(status_str) = status { + let status_text = + Text::with_text_style(&status_str, Point::zero(), NORMAL_FONT, TEXT_CENTER); + LinearLayout::horizontal(Chain::new(status_text)) + .with_alignment(embedded_layout::align::vertical::Center) + .arrange() + .align_to( + &MAIN_RECT, + embedded_layout::align::horizontal::Center, + embedded_layout::align::vertical::Bottom, ) - .ok()?; + .draw(&mut oled.fbuf)?; } } - Scene::Update => { - _ = lcd_driver.print(0, "Updating...", PrintAlign::Center, true); - loop { - let progress = global_state.update_progress.wait().await; - _ = lcd_driver.print(1, &alloc::format!("{progress}%"), PrintAlign::Center, true); + Scene::Update => loop { + let progress = global_state.update_progress.wait().await; + oled.clear_main()?; + center_text_layout(&format!("Updating\n{progress}%")).draw(&mut oled.fbuf)?; + oled.flush().await?; + }, + } - fbuf.clear(BinaryColor::Off); - lcd_driver.display_on_oled(fbuf).await; - embedded_graphics::prelude::DrawTarget::draw_iter(disp, fbuf.into_iter()).unwrap(); - disp.flush().await.unwrap(); - } + /* + match current_state.scene { } + */ + + Ok(()) +} + +async fn process_main_overwrite( + current_state: &SignaledGlobalStateInner, + _global_state: &GlobalState, + oled: &mut OledData<'_>, +) -> bool { + if !current_state.scene.can_be_lcd_overwritten() { + return false; } - Some(()) + if current_state.server_connected == Some(false) { + if current_state.wifi_connected == Some(false) { + let text = Text::with_text_style( + "Wi-Fi\nConnection lost", + Point::zero(), + NORMAL_FONT, + TEXT_CENTER, + ); + + center_layout(Chain::new(text)).draw(&mut oled.fbuf); + } else { + let text = format!( + "{}\n{}", + get_translation(TranslationKey::SERVER_DISCONNECTED_HEADER), + get_translation(TranslationKey::SERVER_DISCONNECTED_FOOTER) + ); + let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); + center_layout(Chain::new(text)).draw(&mut oled.fbuf); + } + } else if current_state.device_added == Some(false) { + #[cfg(not(feature = "e2e"))] + let lines = ( + &get_translation(TranslationKey::DEVICE_NOT_ADDED_HEADER), + &get_translation(TranslationKey::DEVICE_NOT_ADDED_FOOTER), + ); + + #[cfg(feature = "e2e")] + let lines = ("Press submit", "To start HIL"); + + let text = format!("{}\n{}", lines.0, lines.1); + let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); + center_layout(Chain::new(text)).draw(&mut oled.fbuf); + } else if current_state.stackmat_connected == Some(false) { + let text = format!( + "{}\n{}", + get_translation(TranslationKey::STACKMAT_DISCONNECTED_HEADER), + get_translation(TranslationKey::STACKMAT_DISCONNECTED_FOOTER) + ); + let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); + center_layout(Chain::new(text)).draw(&mut oled.fbuf); + } else { + return false; + } + + true } -*/ diff --git a/src/utils/buttons.rs b/src/utils/buttons.rs index 5439d0b..329e8c0 100644 --- a/src/utils/buttons.rs +++ b/src/utils/buttons.rs @@ -107,8 +107,8 @@ impl ButtonsHandler { #[cfg(feature = "v4")] { - for i in 0..4 { - if button_inputs[i].is_high() { + for (i, btn) in button_inputs.iter().enumerate().take(4) { + if btn.is_high() { out_val |= 1 << i; } } diff --git a/src/utils/lcd_resourcese.rs b/src/utils/lcd_resourcese.rs index 2f586ff..c42dc04 100644 --- a/src/utils/lcd_resourcese.rs +++ b/src/utils/lcd_resourcese.rs @@ -25,7 +25,7 @@ impl PixelArt { } fn bytes_per_row(&self) -> u32 { - (self.width + 7) / 8 + self.width.div_ceil(8) } } diff --git a/src/utils/shared_i2c.rs b/src/utils/shared_i2c.rs index 61fdcb3..0cd9a6b 100644 --- a/src/utils/shared_i2c.rs +++ b/src/utils/shared_i2c.rs @@ -4,7 +4,7 @@ use esp_hal::{Async, i2c::master::I2c}; #[derive(Clone)] pub struct SharedI2C { - inner: Rc>>, + inner: Option>>>, } impl embedded_hal::i2c::ErrorType for SharedI2C { @@ -17,7 +17,11 @@ impl embedded_hal_async::i2c::I2c for SharedI2C { address: u8, operations: &mut [embedded_hal::i2c::Operation<'_>], ) -> Result<(), Self::Error> { - self.inner.lock().await.transaction( + let Some(ref i2c) = self.inner else { + return Err(esp_hal::i2c::master::Error::Timeout); + }; + + i2c.lock().await.transaction( address, operations .iter_mut() @@ -29,9 +33,12 @@ impl embedded_hal_async::i2c::I2c for SharedI2C { } impl SharedI2C { - pub fn new(i2c: I2c<'static, Async>) -> Self { - SharedI2C { - inner: Rc::new(Mutex::new(i2c)), + pub fn new(i2c: Option>) -> Self { + match i2c { + Some(i2c) => SharedI2C { + inner: Some(Rc::new(Mutex::new(i2c))), + }, + None => SharedI2C { inner: None }, } } } From b24264a89254e51e9329ea1f04163c3a2303a924 Mon Sep 17 00:00:00 2001 From: filipton Date: Fri, 13 Mar 2026 23:45:20 +0100 Subject: [PATCH 22/31] explore: better deep sleep --- src/battery_v4.rs | 2 ++ src/lcd_v4.rs | 43 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/battery_v4.rs b/src/battery_v4.rs index afc31eb..944c080 100644 --- a/src/battery_v4.rs +++ b/src/battery_v4.rs @@ -28,6 +28,8 @@ pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) let mut last_sent = Instant::now(); loop { if sleep_state() { + let ma = gauge.average_current().await.unwrap_or(0); + log::info!("avg current: {ma}mA"); Timer::after_millis(500).await; continue; } diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index 2f47318..8ece347 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -192,12 +192,8 @@ pub async fn lcd_task( { oled.fbuf.clear(BinaryColor::Off); - let text = Text::with_text_style( - "Sleep\nPress any key...", - Point::zero(), - NORMAL_FONT, - TEXT_CENTER, - ); + let text = + Text::with_text_style("Sleep", Point::zero(), NORMAL_FONT, TEXT_CENTER); center_layout(Chain::new(text)).draw(&mut oled.fbuf); _ = oled.flush().await; @@ -219,9 +215,14 @@ pub async fn lcd_task( && !deeper_sleep_state() && (Instant::now() - last_update).as_millis() > DEEPER_SLEEP_AFTER_MS { + use esp_hal::rtc_cntl::{ + Rtc, + sleep::{RtcioWakeupSource, WakeSource}, + }; + oled.fbuf.clear(BinaryColor::Off); let text = Text::with_text_style( - "Deep Sleep\nPress any key...", + "Deep Sleep", Point::zero(), NORMAL_FONT, TEXT_CENTER, @@ -230,7 +231,33 @@ pub async fn lcd_task( center_layout(Chain::new(text)).draw(&mut oled.fbuf); _ = oled.flush().await; - crate::utils::deeper_sleep(); + unsafe { + let wakeup_pins: &mut [( + &mut dyn esp_hal::gpio::RtcPinWithResistors, + esp_hal::rtc_cntl::sleep::WakeupLevel, + )] = &mut [ + ( + &mut esp_hal::peripherals::GPIO0::steal(), + esp_hal::rtc_cntl::sleep::WakeupLevel::High, + ), + ( + &mut esp_hal::peripherals::GPIO1::steal(), + esp_hal::rtc_cntl::sleep::WakeupLevel::High, + ), + ( + &mut esp_hal::peripherals::GPIO2::steal(), + esp_hal::rtc_cntl::sleep::WakeupLevel::High, + ), + ( + &mut esp_hal::peripherals::GPIO3::steal(), + esp_hal::rtc_cntl::sleep::WakeupLevel::High, + ), + ]; + let rtcio = RtcioWakeupSource::new(wakeup_pins); + Rtc::new(esp_hal::peripherals::LPWR::steal()).sleep_deep(&[&rtcio]); + } + + //crate::utils::deeper_sleep(); } } }; From c0d47610f8a01cc5904c212f61fbcf9cb4c9f4fe Mon Sep 17 00:00:00 2001 From: filipton Date: Sun, 15 Mar 2026 14:21:28 +0100 Subject: [PATCH 23/31] feat: enable rfid soft power down --- src/rfid.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rfid.rs b/src/rfid.rs index 58f9c6b..8eb5b0a 100644 --- a/src/rfid.rs +++ b/src/rfid.rs @@ -99,11 +99,11 @@ pub async fn rfid_task( if sleep_state() != rfid_sleep { rfid_sleep = sleep_state(); - /* match rfid_sleep { true => _ = mfrc522.pcd_soft_power_down().await, false => { _ = mfrc522.pcd_soft_power_up().await; + Timer::after_millis(100).await; loop { _ = mfrc522.pcd_init().await; if mfrc522.pcd_is_init().await { @@ -117,7 +117,6 @@ pub async fn rfid_task( } } } - */ } if rfid_sleep { From 7f58dbc15b5c23f48718c8e7bfdfd79cf608aa09 Mon Sep 17 00:00:00 2001 From: filipton Date: Sun, 15 Mar 2026 16:25:50 +0100 Subject: [PATCH 24/31] chore: remove avg current battery consumption during sleep --- src/battery_v4.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/battery_v4.rs b/src/battery_v4.rs index 944c080..afc31eb 100644 --- a/src/battery_v4.rs +++ b/src/battery_v4.rs @@ -28,8 +28,6 @@ pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) let mut last_sent = Instant::now(); loop { if sleep_state() { - let ma = gauge.average_current().await.unwrap_or(0); - log::info!("avg current: {ma}mA"); Timer::after_millis(500).await; continue; } From 7811d2b9c47acb1ebf54fcfb0fbb8ab537159717 Mon Sep 17 00:00:00 2001 From: filipton Date: Wed, 18 Mar 2026 23:46:54 +0100 Subject: [PATCH 25/31] chore: log parsed saved state --- src/state.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/state.rs b/src/state.rs index b122195..cf3c60e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -374,6 +374,8 @@ impl SignaledGlobalStateInner { } pub fn parse_saved_state(&mut self, saved: SavedGlobalState) { + log::warn!("Parsed saved state: {saved:?}"); + self.session_id = Some(saved.session_id); self.penalty = Some(saved.penalty); self.solve_time = Some(saved.solve_time); From 285a08a40768b0b5279ac8f8f7127b458eb37f44 Mon Sep 17 00:00:00 2001 From: filipton Date: Thu, 19 Mar 2026 19:03:34 +0100 Subject: [PATCH 26/31] feat: add debug power loss, sleep wakeup's prints --- src/battery_v4.rs | 6 ++++++ src/lcd_v4.rs | 30 ++++++++---------------------- src/main.rs | 4 ++++ src/utils/mod.rs | 3 +++ 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/battery_v4.rs b/src/battery_v4.rs index afc31eb..a34e235 100644 --- a/src/battery_v4.rs +++ b/src/battery_v4.rs @@ -24,6 +24,12 @@ pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) return; }; + if let Ok(soc) = gauge.state_of_charge().await + && soc == 0 + { + log::warn!("Battery was removed before boot!"); + } + let mut startup_sent = false; let mut last_sent = Instant::now(); loop { diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index 8ece347..edf6598 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -173,6 +173,7 @@ pub async fn lcd_task( unsafe { crate::state::SLEEP_STATE = false; } + log::warn!("Sleep wakeup!"); } let current_scene = current_state.scene.clone(); @@ -215,10 +216,7 @@ pub async fn lcd_task( && !deeper_sleep_state() && (Instant::now() - last_update).as_millis() > DEEPER_SLEEP_AFTER_MS { - use esp_hal::rtc_cntl::{ - Rtc, - sleep::{RtcioWakeupSource, WakeSource}, - }; + use esp_hal::rtc_cntl::{Rtc, sleep::RtcioWakeupSource}; oled.fbuf.clear(BinaryColor::Off); let text = Text::with_text_style( @@ -232,32 +230,20 @@ pub async fn lcd_task( _ = oled.flush().await; unsafe { + use esp_hal::rtc_cntl::sleep::WakeupLevel; + let wakeup_pins: &mut [( &mut dyn esp_hal::gpio::RtcPinWithResistors, esp_hal::rtc_cntl::sleep::WakeupLevel, )] = &mut [ - ( - &mut esp_hal::peripherals::GPIO0::steal(), - esp_hal::rtc_cntl::sleep::WakeupLevel::High, - ), - ( - &mut esp_hal::peripherals::GPIO1::steal(), - esp_hal::rtc_cntl::sleep::WakeupLevel::High, - ), - ( - &mut esp_hal::peripherals::GPIO2::steal(), - esp_hal::rtc_cntl::sleep::WakeupLevel::High, - ), - ( - &mut esp_hal::peripherals::GPIO3::steal(), - esp_hal::rtc_cntl::sleep::WakeupLevel::High, - ), + (&mut esp_hal::peripherals::GPIO0::steal(), WakeupLevel::High), + (&mut esp_hal::peripherals::GPIO1::steal(), WakeupLevel::High), + (&mut esp_hal::peripherals::GPIO2::steal(), WakeupLevel::High), + (&mut esp_hal::peripherals::GPIO3::steal(), WakeupLevel::High), ]; let rtcio = RtcioWakeupSource::new(wakeup_pins); Rtc::new(esp_hal::peripherals::LPWR::steal()).sleep_deep(&[&rtcio]); } - - //crate::utils::deeper_sleep(); } } }; diff --git a/src/main.rs b/src/main.rs index f51df23..17e43af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -113,6 +113,10 @@ async fn main(spawner: Spawner) { } }; + let reason = esp_hal::rtc_cntl::reset_reason(esp_hal::system::Cpu::ProCpu); + let wake_reason = esp_hal::rtc_cntl::wakeup_cause(); + log::info!("Wake reason: {:?} {:?}", reason, wake_reason); + let global_state = Rc::new(GlobalStateInner::new(&nvs, board.aes)); let wifi_setup_sig = Rc::new(Signal::new()); let wifi_conn_sig = Rc::new(Signal::new()); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 057940e..8bfe1cd 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -43,6 +43,7 @@ pub fn get_efuse_u32() -> u32 { mac as u32 } +#[cfg(feature = "v3")] /// Sets cpu clock to 10mHz (not reversable) pub fn deeper_sleep() { esp32c3_set_cpu_freq_10mhz(); @@ -50,6 +51,7 @@ pub fn deeper_sleep() { unsafe { crate::state::DEEPER_SLEEP = true }; } +#[cfg(feature = "v3")] #[allow(unused)] #[inline(always)] fn ets_update_cpu_frequency_rom(ticks_per_us: u32) { @@ -60,6 +62,7 @@ fn ets_update_cpu_frequency_rom(ticks_per_us: u32) { unsafe { ets_update_cpu_frequency(ticks_per_us) }; } +#[cfg(feature = "v3")] fn esp32c3_set_cpu_freq_10mhz() { use esp32c3::{RTC_CNTL, SYSTEM}; From b482b33e36de2895bf92a7a5dd63036060b16e06 Mon Sep 17 00:00:00 2001 From: filipton Date: Thu, 19 Mar 2026 19:45:52 +0100 Subject: [PATCH 27/31] feat: text wrap --- Cargo.lock | 18 +++++++++++++ Cargo.toml | 3 ++- src/lcd_v4.rs | 74 ++++++++++++++++++++++----------------------------- 3 files changed, 52 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de65fcc..defaba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1246,6 +1246,17 @@ dependencies = [ "embedded-storage", ] +[[package]] +name = "embedded-text" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf5c72c52db2f7dbe4a9c1ed81cd21301e8d66311b194fa41c04fb4f71843ba" +dependencies = [ + "az", + "embedded-graphics", + "object-chain", +] + [[package]] name = "embedded-tls" version = "0.19.0" @@ -1862,6 +1873,7 @@ dependencies = [ "embedded-io-async 0.7.0", "embedded-layout", "embedded-storage", + "embedded-text", "embedded-tls", "esp-alloc", "esp-backtrace", @@ -2590,6 +2602,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "object-chain" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41af26158b0f5530f7b79955006c2727cd23d0d8e7c3109dc316db0a919784dd" + [[package]] name = "oled_async" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index ca4034e..c8c9da8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ oled_async = { version = "0.2.0", features = ["i2c"], optional = true } bq27441 = { version = "0.1.0", features = ["async", "embassy"], optional = true } profont = { version = "0.7.0", optional = true } embedded-layout = { version = "0.4.2", optional = true } +embedded-text = { version = "0.7.3", optional = true } #v3 only dyn-smooth = { version = "0.2.0", optional = true } @@ -74,7 +75,7 @@ qa = [] sleep = [] auto_add = [] v3 = ["dep:dyn-smooth", "dep:ag-lcd-async"] -v4 = ["dep:display-interface", "dep:display-interface-i2c", "dep:embedded-graphics", "dep:embedded-graphics-framebuf", "dep:oled_async", "dep:bq27441", "dep:profont", "dep:embedded-layout"] +v4 = ["dep:display-interface", "dep:display-interface-i2c", "dep:embedded-graphics", "dep:embedded-graphics-framebuf", "dep:oled_async", "dep:bq27441", "dep:profont", "dep:embedded-layout", "dep:embedded-text"] [profile.dev] opt-level = 3 diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index edf6598..e7d496c 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -17,6 +17,11 @@ use embedded_layout::{ prelude::*, view_group::ViewGroup, }; +use embedded_text::{ + TextBox, + alignment::{HorizontalAlignment, VerticalAlignment}, + style::TextBoxStyleBuilder, +}; use esp_hal::gpio::Output; use oled_async::{displays::ssd1309::Ssd1309_128_64, mode::GraphicsMode}; @@ -93,16 +98,13 @@ fn center_layout( ) } -fn center_text_layout( - text: &str, -) -> LinearLayout< - Horizontal, - embedded_layout::object_chain::Chain< - embedded_graphics::text::Text<'_, MonoTextStyle<'_, BinaryColor>>, - >, -> { - let text = Text::with_text_style(text, Point::zero(), NORMAL_FONT, TEXT_CENTER); - center_layout(Chain::new(text)) +fn center_text_layout(text: &str) -> TextBox<'_, MonoTextStyle<'_, BinaryColor>> { + let textbox_style = TextBoxStyleBuilder::new() + .alignment(HorizontalAlignment::Center) + .vertical_alignment(VerticalAlignment::Middle) + .build(); + + TextBox::with_textbox_style(text, MAIN_RECT, NORMAL_FONT, textbox_style) } #[embassy_executor::task] @@ -574,6 +576,8 @@ async fn process_main( center_text_layout(&text).draw(&mut oled.fbuf)?; } Scene::Inspection => { + oled.clear_main()?; + oled.flush().await?; let inspection_start = global_state .state .value() @@ -581,7 +585,7 @@ async fn process_main( .inspection_start .unwrap_or(Instant::now()); - _ = Text::with_text_style("Inspection", Point::new(64, 44), NORMAL_FONT, TEXT_CENTER) + _ = Text::with_text_style("Inspection", Point::new(64, 50), NORMAL_FONT, TEXT_CENTER) .draw(&mut oled.disp); let text_rect = Rectangle::new(Point::new(0, 28), Size::new(128, 17)); @@ -592,13 +596,14 @@ async fn process_main( _ = oled.disp.fill_solid(&text_rect, BinaryColor::Off); _ = Text::with_text_style(&time_str, Point::new(64, 36), TIMER_FONT, TEXT_CENTER) .draw(&mut oled.disp); - oled.flush().await?; + _ = oled.disp.flush().await; Timer::after_millis(LCD_INSPECTION_FRAME_TIME).await; } } Scene::Timer => { oled.clear_main()?; + oled.flush().await?; let text_rect = Rectangle::new(Point::new(0, 28), Size::new(128, 17)); loop { @@ -608,7 +613,7 @@ async fn process_main( _ = oled.disp.fill_solid(&text_rect, BinaryColor::Off); _ = Text::with_text_style(&time_str, Point::new(64, 36), TIMER_FONT, TEXT_CENTER) .draw(&mut oled.disp); - oled.flush().await?; + _ = oled.disp.flush().await; } } Scene::Finished => { @@ -640,8 +645,6 @@ async fn process_main( _ => alloc::string::String::new(), }; - // Time + penalty centered in MAIN_RECT - // If there's a penalty, show them as a horizontal chain; otherwise just time if penalty_str.is_empty() { let time_text = Text::with_text_style(&time_display, Point::zero(), TIMER_FONT, TEXT_CENTER); @@ -651,8 +654,9 @@ async fn process_main( .align_to( &MAIN_RECT, embedded_layout::align::horizontal::Center, - embedded_layout::align::vertical::Center, + embedded_layout::align::vertical::Top, ) + .translate(Point::new(0, 10)) .draw(&mut oled.fbuf)?; } else { let time_text = @@ -666,12 +670,12 @@ async fn process_main( .align_to( &MAIN_RECT, embedded_layout::align::horizontal::Center, - embedded_layout::align::vertical::Center, + embedded_layout::align::vertical::Top, ) + .translate(Point::new(0, 10)) .draw(&mut oled.fbuf)?; } - // Status prompt pinned to bottom center let status = if !current_state.time_confirmed { Some(get_translation(TranslationKey::CONFIRM_TIME)) } else if current_state.current_judge.is_none() { @@ -685,16 +689,12 @@ async fn process_main( }; if let Some(status_str) = status { - let status_text = - Text::with_text_style(&status_str, Point::zero(), NORMAL_FONT, TEXT_CENTER); - LinearLayout::horizontal(Chain::new(status_text)) - .with_alignment(embedded_layout::align::vertical::Center) - .arrange() - .align_to( - &MAIN_RECT, - embedded_layout::align::horizontal::Center, - embedded_layout::align::vertical::Bottom, - ) + let textbox_style = TextBoxStyleBuilder::new() + .alignment(HorizontalAlignment::Center) + .vertical_alignment(VerticalAlignment::Bottom) + .build(); + + TextBox::with_textbox_style(&status_str, MAIN_RECT, NORMAL_FONT, textbox_style) .draw(&mut oled.fbuf)?; } } @@ -725,22 +725,14 @@ async fn process_main_overwrite( if current_state.server_connected == Some(false) { if current_state.wifi_connected == Some(false) { - let text = Text::with_text_style( - "Wi-Fi\nConnection lost", - Point::zero(), - NORMAL_FONT, - TEXT_CENTER, - ); - - center_layout(Chain::new(text)).draw(&mut oled.fbuf); + _ = center_text_layout("Wi-Fi\nConnection lost").draw(&mut oled.fbuf); } else { let text = format!( "{}\n{}", get_translation(TranslationKey::SERVER_DISCONNECTED_HEADER), get_translation(TranslationKey::SERVER_DISCONNECTED_FOOTER) ); - let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); - center_layout(Chain::new(text)).draw(&mut oled.fbuf); + _ = center_text_layout(&text).draw(&mut oled.fbuf); } } else if current_state.device_added == Some(false) { #[cfg(not(feature = "e2e"))] @@ -753,16 +745,14 @@ async fn process_main_overwrite( let lines = ("Press submit", "To start HIL"); let text = format!("{}\n{}", lines.0, lines.1); - let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); - center_layout(Chain::new(text)).draw(&mut oled.fbuf); + _ = center_text_layout(&text).draw(&mut oled.fbuf); } else if current_state.stackmat_connected == Some(false) { let text = format!( "{}\n{}", get_translation(TranslationKey::STACKMAT_DISCONNECTED_HEADER), get_translation(TranslationKey::STACKMAT_DISCONNECTED_FOOTER) ); - let text = Text::with_text_style(&text, Point::zero(), NORMAL_FONT, TEXT_CENTER); - center_layout(Chain::new(text)).draw(&mut oled.fbuf); + _ = center_text_layout(&text).draw(&mut oled.fbuf); } else { return false; } From b8e30cf1abceb3868d34e7f1e3916d6db1069f62 Mon Sep 17 00:00:00 2001 From: filipton Date: Thu, 19 Mar 2026 20:11:17 +0100 Subject: [PATCH 28/31] feat: better scollable config menus --- src/buttons.rs | 2 + src/lcd_v4.rs | 215 ++++++++++++++++++++++++++++++------------------- 2 files changed, 133 insertions(+), 84 deletions(-) diff --git a/src/buttons.rs b/src/buttons.rs index 42bbbb3..48e2937 100644 --- a/src/buttons.rs +++ b/src/buttons.rs @@ -178,6 +178,7 @@ async fn submit_up( match state_val.menu_scene { Some(MenuScene::Signing) | Some(MenuScene::Unsigning) => { state_val.menu_scene = None; + state_val.selected_config_menu = Some(0); state.state.signal(); return Ok(true); } @@ -210,6 +211,7 @@ async fn submit_up( state_val.menu_scene = None; state_val.selected_bluetooth_item = 0; + state_val.selected_config_menu = Some(0); state.state.signal(); return Ok(true); } diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index e7d496c..2279627 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -1,3 +1,17 @@ +use crate::{ + consts::{ + DEEPER_SLEEP_AFTER_MS, INSPECTION_TIME_PLUS2, LCD_INSPECTION_FRAME_TIME, SLEEP_AFTER_MS, + }, + state::{ + GlobalState, MenuScene, Scene, SignaledGlobalStateInner, deeper_sleep_state, sleep_state, + }, + translations::{TranslationKey, get_translation, get_translation_params}, + utils::{ + lcd_resourcese::{CrossedIcon, Resources}, + shared_i2c::SharedI2C, + stackmat::ms_to_time_str, + }, +}; use alloc::{format, rc::Rc, string::ToString}; use anyhow::{Result, anyhow}; use display_interface_i2c::I2CInterface; @@ -25,21 +39,6 @@ use embedded_text::{ use esp_hal::gpio::Output; use oled_async::{displays::ssd1309::Ssd1309_128_64, mode::GraphicsMode}; -use crate::{ - consts::{ - DEEPER_SLEEP_AFTER_MS, INSPECTION_TIME_PLUS2, LCD_INSPECTION_FRAME_TIME, SLEEP_AFTER_MS, - }, - state::{ - GlobalState, MenuScene, Scene, SignaledGlobalStateInner, deeper_sleep_state, sleep_state, - }, - translations::{TranslationKey, get_translation, get_translation_params}, - utils::{ - lcd_resourcese::{CrossedIcon, Resources}, - shared_i2c::SharedI2C, - stackmat::ms_to_time_str, - }, -}; - pub const FBUF_WIDTH: usize = 128; pub const FBUF_HEIGHT: usize = 64; pub const FBUF_SIZE: usize = FBUF_WIDTH * FBUF_HEIGHT; @@ -66,12 +65,14 @@ impl OledData<'_> { } pub const MAIN_RECT: Rectangle = Rectangle::new(Point::new(0, 11), Size::new(128, 53)); - pub const NORMAL_FONT: MonoTextStyle<'_, BinaryColor> = MonoTextStyle::new( &embedded_graphics::mono_font::ascii::FONT_7X13, BinaryColor::On, ); - +pub const SMALL_FONT: MonoTextStyle<'_, BinaryColor> = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_6X9, + BinaryColor::On, +); pub const TIMER_FONT: MonoTextStyle<'_, BinaryColor> = MonoTextStyle::new(&profont::PROFONT_14_POINT, BinaryColor::On); @@ -79,7 +80,6 @@ pub const TEXT_CENTER: TextStyle = TextStyleBuilder::new() .alignment(Alignment::Center) .baseline(Baseline::Middle) .build(); - pub const TEXT_TOPBAR: TextStyle = TextStyleBuilder::new() .alignment(Alignment::Right) .baseline(Baseline::Top) @@ -107,6 +107,80 @@ fn center_text_layout(text: &str) -> TextBox<'_, MonoTextStyle<'_, BinaryColor>> TextBox::with_textbox_style(text, MAIN_RECT, NORMAL_FONT, textbox_style) } +fn draw_scrollable_menu(target: &mut D, items: &[&str], selected: usize) +where + D: DrawTarget, +{ + const LINE_HEIGHT: i32 = 10; + const VISIBLE: usize = 5; + const PADDING_X: i32 = 4; + + let menu_font = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_6X9, + BinaryColor::On, + ); + let menu_font_inv = MonoTextStyle::new( + &embedded_graphics::mono_font::ascii::FONT_6X9, + BinaryColor::Off, + ); + + let total = items.len(); + let scroll_start = if selected + 1 >= VISIBLE { + (selected + 1 - VISIBLE).min(total.saturating_sub(VISIBLE)) + } else { + 0 + }; + + let start_y = MAIN_RECT.top_left.y; + + for (row, item) in items[scroll_start..].iter().take(VISIBLE).enumerate() { + let item_idx = scroll_start + row; + let y = start_y + row as i32 * LINE_HEIGHT; + let text_y = y + LINE_HEIGHT / 2; + + if item_idx == selected { + let bar = Rectangle::new(Point::new(0, y), Size::new(128, LINE_HEIGHT as u32)); + bar.into_styled(PrimitiveStyle::with_fill(BinaryColor::On)) + .draw(target) + .ok(); + Text::with_text_style( + item, + Point::new(PADDING_X, text_y), + menu_font_inv, + TextStyleBuilder::new() + .alignment(Alignment::Left) + .baseline(Baseline::Middle) + .build(), + ) + .draw(target) + .ok(); + } else { + Text::with_text_style( + item, + Point::new(PADDING_X, text_y), + menu_font, + TextStyleBuilder::new() + .alignment(Alignment::Left) + .baseline(Baseline::Middle) + .build(), + ) + .draw(target) + .ok(); + } + + if scroll_start > 0 && row == 0 { + Text::with_text_style("^", Point::new(122, text_y), menu_font, TEXT_CENTER) + .draw(target) + .ok(); + } + if scroll_start + VISIBLE < total && row == VISIBLE - 1 { + Text::with_text_style("v", Point::new(122, text_y), menu_font, TEXT_CENTER) + .draw(target) + .ok(); + } + } +} + #[embassy_executor::task] pub async fn lcd_task( i2c: SharedI2C, @@ -320,6 +394,21 @@ async fn process_top_bar( ) .draw(&mut oled.fbuf)?; + let text = if current_state.selected_config_menu.is_some() { + Some("CONFIG") + } else { + match current_state.menu_scene { + Some(MenuScene::Signing) => Some("SIGN"), + Some(MenuScene::Unsigning) => Some("UNSIGN"), + Some(MenuScene::BtDisplay) => Some("BTDISP"), + None => None, + } + }; + if let Some(text) = text { + Text::with_text_style(text, Point::new(64, 5), SMALL_FONT, TEXT_CENTER) + .draw(&mut oled.fbuf)?; + } + Line::new(Point::new(0, 10), Point::new(128, 10)) .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) .draw(&mut oled.fbuf)?; @@ -351,36 +440,13 @@ async fn process_main( } if let Some(sel) = current_state.selected_config_menu { - let lt = Text::with_text_style("<", Point::zero(), TIMER_FONT, TEXT_CENTER); - let gt = Text::with_text_style(">", Point::zero(), TIMER_FONT, TEXT_CENTER); - LinearLayout::horizontal(Chain::new(lt)) - .with_alignment(embedded_layout::align::vertical::Center) - .arrange() - .align_to( - &MAIN_RECT, - embedded_layout::align::horizontal::Left, - embedded_layout::align::vertical::Center, - ) - .draw(&mut oled.fbuf)?; - - LinearLayout::horizontal(Chain::new(gt)) - .with_alignment(embedded_layout::align::vertical::Center) - .arrange() - .align_to( - &MAIN_RECT, - embedded_layout::align::horizontal::Right, - embedded_layout::align::vertical::Center, - ) - .draw(&mut oled.fbuf)?; - - // TODO: this can be "scrollable" list btw - center_text_layout(&format!( - "Config Menu\n{}. {}", - sel + 1, - crate::structs::CONFIG_MENU_ITEMS[sel] - )) - .draw(&mut oled.fbuf)?; - + let items: alloc::vec::Vec = crate::structs::CONFIG_MENU_ITEMS + .iter() + .enumerate() + .map(|(i, name)| alloc::format!("{}. {}", i + 1, name)) + .collect(); + let item_refs: alloc::vec::Vec<&str> = items.iter().map(|s| s.as_str()).collect(); + draw_scrollable_menu(&mut oled.fbuf, &item_refs, sel); return Ok(()); } @@ -407,45 +473,31 @@ async fn process_main( Timer::after_millis(300).await; oled.clear_main()?; - center_text_layout(&format!( - "{main_text}\nScan the card\n\nPress Submit to exit" - )) - .draw(&mut oled.fbuf)?; + center_text_layout(&format!("{main_text}\nScan the card\n\nSubmit to exit")) + .draw(&mut oled.fbuf)?; } else { - center_text_layout(&format!( - "{main_text}\nScan the card\n\nPress Submit to exit" - )) - .draw(&mut oled.fbuf)?; + center_text_layout(&format!("{main_text}\nScan the card\n\nSubmit to exit")) + .draw(&mut oled.fbuf)?; } return Ok(()); } Some(crate::state::MenuScene::BtDisplay) => { - if current_state.selected_bluetooth_item - == current_state.discovered_bluetooth_devices.len() - { - center_text_layout("BT Display:\nUnpair").draw(&mut oled.fbuf)?; - } else if current_state.selected_bluetooth_item - == current_state.discovered_bluetooth_devices.len() + 1 - { - center_text_layout("BT Display:\nExit").draw(&mut oled.fbuf)?; - } else if current_state.selected_bluetooth_item - < current_state.discovered_bluetooth_devices.len() - { - if let Some(display_dev) = current_state - .discovered_bluetooth_devices - .get(current_state.selected_bluetooth_item) - { - center_text_layout(&format!( - "BT Display:\n{} [{:X?}]", - display_dev.name, display_dev.addr - )) - .draw(&mut oled.fbuf)?; - } - } else { + let mut items: alloc::vec::Vec = current_state + .discovered_bluetooth_devices + .iter() + .map(|dev| alloc::format!("{} [{:X?}]", dev.name, dev.addr)) + .collect(); + items.push("Unpair".into()); + items.push("Exit".into()); + + let sel = current_state.selected_bluetooth_item; + if sel >= items.len() { global_state.state.lock().await.selected_bluetooth_item = 0; + } else { + let item_refs: alloc::vec::Vec<&str> = items.iter().map(|s| s.as_str()).collect(); + draw_scrollable_menu(&mut oled.fbuf, &item_refs, sel); } - return Ok(()); } None => {} @@ -706,11 +758,6 @@ async fn process_main( }, } - /* - match current_state.scene { - } - */ - Ok(()) } From 2a8d6f7e1ed60972b32c52c5208a11233fffa4dc Mon Sep 17 00:00:00 2001 From: filipton Date: Thu, 19 Mar 2026 20:19:21 +0100 Subject: [PATCH 29/31] fix: delegate call translation --- src/lcd_v4.rs | 2 +- src/stackmat.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index 2279627..7adc5d5 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -521,7 +521,7 @@ async fn process_main( } else { center_text_layout(&format!( "{}\n{}", - get_translation(TranslationKey::WAITING_FOR_DELEGATE_HEADER), + get_translation(TranslationKey::CALLING_FOR_DELEGATE_HEADER), get_translation_params( TranslationKey::CALLING_FOR_DELEGATE_FOOTER, &[delegate_remaining], diff --git a/src/stackmat.rs b/src/stackmat.rs index 2cb913d..32faf9b 100644 --- a/src/stackmat.rs +++ b/src/stackmat.rs @@ -156,6 +156,7 @@ pub async fn stackmat_task( state.scene = Scene::Timer; } } else if parsed.0 == StackmatTimerState::Stopped { + log::info!("Timer stopped: {}ms", parsed.1); let mut state = global_state.state.lock().await; let last_solve_diff = if cfg!(not(any(feature = "e2e", feature = "qa"))) { state.last_solve_time.unwrap_or(0).abs_diff(parsed.1) From 6ed38f84c587d133473fe112ce67a1c3e1f3ab6b Mon Sep 17 00:00:00 2001 From: filipton Date: Thu, 19 Mar 2026 23:59:23 +0100 Subject: [PATCH 30/31] fix: sleep timer reset when battery percentage changes --- build.rs | 11 ++++++++--- src/battery_v4.rs | 17 ++++++++++------- src/lcd_v4.rs | 9 +++++++++ src/state.rs | 41 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/build.rs b/build.rs index 2237f2a..ffa7c55 100644 --- a/build.rs +++ b/build.rs @@ -34,9 +34,14 @@ fn main() { format!("D{epoch}") }; - // NOTE: change this if something changes in schematic, (but not MCU) - // This will enable firmware to be built for multiple hw revisions for example - let hw = "v3"; + let hw = if cfg!(feature = "v4") { + "v4" + } else if cfg!(feature = "v3") { + "v3" + } else { + "unknown" + }; + let generated = VERSION_TEMPLATE .replace("{version}", &version_str) .replace("{hw}", hw) diff --git a/src/battery_v4.rs b/src/battery_v4.rs index a34e235..f5dbe0a 100644 --- a/src/battery_v4.rs +++ b/src/battery_v4.rs @@ -30,7 +30,8 @@ pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) log::warn!("Battery was removed before boot!"); } - let mut startup_sent = false; + let mut last_soc = 101; + let mut last_charging = true; let mut last_sent = Instant::now(); loop { if sleep_state() { @@ -44,15 +45,17 @@ pub async fn battery_read_task(i2c: SharedI2C, state: crate::state::GlobalState) soc = bat_percentage(calculate(mv)); } let ma = gauge.average_current().await.unwrap_or(0); + let charging = ma >= 0; - { - let mut state = state.state.lock().await; - state.battery_status = (soc, ma >= 0) - } + if last_soc != soc || last_charging != charging { + { + let mut state = state.state.lock().await; + state.battery_status = (soc, charging) + } - if !startup_sent { state.show_battery.signal(soc); - startup_sent = true; + last_soc = soc; + last_charging = charging; } if last_sent.elapsed().as_millis() >= BATTERY_SEND_INTERVAL_MS { diff --git a/src/lcd_v4.rs b/src/lcd_v4.rs index 7adc5d5..b6fe145 100644 --- a/src/lcd_v4.rs +++ b/src/lcd_v4.rs @@ -65,6 +65,7 @@ impl OledData<'_> { } pub const MAIN_RECT: Rectangle = Rectangle::new(Point::new(0, 11), Size::new(128, 53)); +pub const TOPBAR_RECT: Rectangle = Rectangle::new(Point::new(0, 0), Size::new(128, 10)); pub const NORMAL_FONT: MonoTextStyle<'_, BinaryColor> = MonoTextStyle::new( &embedded_graphics::mono_font::ascii::FONT_7X13, BinaryColor::On, @@ -261,6 +262,14 @@ pub async fn lcd_task( loop { Timer::after_millis(1000).await; + if global_state.show_battery.signaled() { + oled.fbuf.fill_solid(&TOPBAR_RECT, BinaryColor::Off); + let current_state = global_state.state.value().await; + _ = process_top_bar(¤t_state, &global_state, &mut oled).await; + _ = oled.flush().await; + + global_state.show_battery.reset(); + } #[cfg(not(any(feature = "e2e", feature = "qa")))] if !sleep_state() diff --git a/src/state.rs b/src/state.rs index cf3c60e..cb55ba1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -180,7 +180,7 @@ impl GlobalStateInner { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct SignaledGlobalStateInner { pub scene: Scene, pub menu_scene: Option, @@ -469,3 +469,42 @@ impl SavedGlobalState { pub async fn clear_saved_global_state(_nvs: &Nvs) {} } + +impl PartialEq for SignaledGlobalStateInner { + fn eq(&self, other: &Self) -> bool { + let result = self.scene == other.scene + && self.menu_scene == other.menu_scene + && self.inspection_start == other.inspection_start + && self.inspection_end == other.inspection_end + && self.solve_time == other.solve_time + && self.last_solve_time == other.last_solve_time + && self.penalty == other.penalty + && self.session_id == other.session_id + && self.time_confirmed == other.time_confirmed + && self.solve_group == other.solve_group + && self.error_text == other.error_text + && self.possible_groups == other.possible_groups + && self.group_selected_idx == other.group_selected_idx + && self.selected_config_menu == other.selected_config_menu + && self.discovered_bluetooth_devices == other.discovered_bluetooth_devices + && self.selected_bluetooth_item == other.selected_bluetooth_item + && self.device_added == other.device_added + && self.server_connected == other.server_connected + && self.wifi_connected == other.wifi_connected + && self.stackmat_connected == other.stackmat_connected + && self.current_competitor == other.current_competitor + && self.current_judge == other.current_judge + && self.competitor_display == other.competitor_display + && self.delegate_used == other.delegate_used + && self.delegate_hold == other.delegate_hold + // battery_status intentionally excluded (v4 hw) + && self.custom_message == other.custom_message; + + #[cfg(feature = "bat_dev_lcd")] + let result = result + && self.current_bat_read == other.current_bat_read + && self.avg_bat_read == other.avg_bat_read; + + result + } +} From 266906b105fa46bc5f2a52e8aa428110b7fad0a9 Mon Sep 17 00:00:00 2001 From: filipton Date: Sat, 21 Mar 2026 00:18:32 +0100 Subject: [PATCH 31/31] chore: update release script for v3 and v4 --- release.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/release.sh b/release.sh index e0a6751..3f213d6 100755 --- a/release.sh +++ b/release.sh @@ -24,14 +24,17 @@ while [ -z "$RELEASE_VERSION" ]; do done source ~/export-esp.sh -RELEASE_BUILD="$RELEASE_VERSION" cargo build -r - EPOCH=$(date +%s) +RELEASE_BUILD="$RELEASE_VERSION" cargo build -r --no-default-features --features v3,sleep mkdir -p /tmp/fkm-build &> /dev/null espflash save-image --chip esp32c3 ./target/riscv32imc-unknown-none-elf/release/fkm-firmware "/tmp/fkm-build/v3_STATION_${RELEASE_VERSION}.bin" ./append_metadata.sh "/tmp/fkm-build/v3_STATION_${RELEASE_VERSION}.bin" "$RELEASE_VERSION" "STATION" "v3" "$EPOCH" +RELEASE_BUILD="$RELEASE_VERSION" cargo build -r --no-default-features --features v4,sleep +espflash save-image --chip esp32c3 ./target/riscv32imc-unknown-none-elf/release/fkm-firmware "/tmp/fkm-build/v4_STATION_${RELEASE_VERSION}.bin" +./append_metadata.sh "/tmp/fkm-build/v4_STATION_${RELEASE_VERSION}.bin" "$RELEASE_VERSION" "STATION" "v4" "$EPOCH" + cd $SCRIPT_DIR echo "Version: $RELEASE_VERSION"