From d0b0de550b8c5ab687fe5f3de3f6879339942c5f Mon Sep 17 00:00:00 2001 From: Yandrik Date: Tue, 19 Nov 2024 23:00:08 +0100 Subject: [PATCH] feat: light-control and microwave ui using slint and lvgl --- lvgl-based/.cargo/config.toml | 7 +- lvgl-based/Cargo.toml | 28 +- lvgl-based/lvgl-configs/lv_conf.h | 8 +- lvgl-based/src/bin/light-control.rs | 543 +++++++++++++++++++++++++++ lvgl-based/src/bin/microwave-ui.rs | 522 +++++++++++++++++++++++++ lvgl-based/src/bin/timer.rs | 50 ++- slint-based/.cargo/config.toml | 1 - slint-based/Cargo.toml | 11 +- slint-based/build.rs | 18 + slint-based/img/pause.png | Bin 0 -> 373 bytes slint-based/img/play.png | Bin 0 -> 401 bytes slint-based/img/restart.png | Bin 0 -> 1362 bytes slint-based/img/xmark-square.png | Bin 0 -> 542 bytes slint-based/src/bin/light-control.rs | 347 +++++++++++++++++ slint-based/src/bin/microwave-ui.rs | 490 ++++++++++++++++++++++++ slint-based/src/bin/timer.rs | 490 ++++++++++++++++++++++++ slint-based/ui/app-window.slint | 18 +- slint-based/ui/lights-app.slint | 164 ++++++++ slint-based/ui/microwave-ui.slint | 146 +++++++ slint-based/ui/timer-app.slint | 114 ++++++ 20 files changed, 2910 insertions(+), 47 deletions(-) create mode 100644 lvgl-based/src/bin/light-control.rs create mode 100644 lvgl-based/src/bin/microwave-ui.rs create mode 100644 slint-based/img/pause.png create mode 100644 slint-based/img/play.png create mode 100644 slint-based/img/restart.png create mode 100644 slint-based/img/xmark-square.png create mode 100644 slint-based/src/bin/light-control.rs create mode 100644 slint-based/src/bin/microwave-ui.rs create mode 100644 slint-based/src/bin/timer.rs create mode 100644 slint-based/ui/lights-app.slint create mode 100644 slint-based/ui/microwave-ui.slint create mode 100644 slint-based/ui/timer-app.slint diff --git a/lvgl-based/.cargo/config.toml b/lvgl-based/.cargo/config.toml index 6e3b72b..1d6343d 100644 --- a/lvgl-based/.cargo/config.toml +++ b/lvgl-based/.cargo/config.toml @@ -9,7 +9,6 @@ rustflags = [ # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110 "--cfg", "espidf_time64", - # Added the following 2 entries so lvgl will build without getting string.h file not found "--sysroot", "/home/ed/.rustup/toolchains/esp/xtensa-esp32s3-elf/esp-13.2.0_20230928/xtensa-esp-elf/xtensa-esp-elf/include", @@ -30,7 +29,7 @@ DEP_LV_CONFIG_PATH = { relative = true, value = "lvgl-configs" } CROSS_COMPILE = "xtensa-esp32-elf" # Directory for custom fonts (written in C) that Lvgl can use -LVGL_FONTS_DIR = {relative = true, value = "custom-fonts"} +LVGL_FONTS_DIR = { relative = true, value = "custom-fonts" } -PATH="/usr/lib64/ccache:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/var/lib/snapd/snap/bin:/home/yannik/.rustup/toolchains/esp/xtensa-esp32s2-elf/esp-2021r2-patch5-8_4_0/xtensa-esp32s2-elf/bin:/home/yannik/.rustup/toolchains/esp/xtensa-esp32s3-elf/esp-2021r2-patch5-8_4_0/xtensa-esp32s3-elf/bin:/home/yannik/.rustup/toolchains/esp/xtensa-esp32-elf/esp-2021r2-patch5-8_4_0/xtensa-esp32-elf/bin:/home/yannik/.rustup/toolchains/esp/riscv32-esp-elf/esp-2021r2-patch5-8_4_0/riscv32-esp-elf/bin:" -LIBCLANG_PATH="/home/yannik/.rustup/toolchains/esp/xtensa-esp32-elf-clang/esp-15.0.0-20221201/esp-clang/lib" +PATH = "/home/yannik/.rustup/toolchains/esp/xtensa-esp-elf/esp-14.2.0_20240906/xtensa-esp-elf/bin:/home/yannik/.local/share/pnpm:/home/yannik/.local/share/zinit/polaris/bin:/home/yannik/.rustup/toolchains/esp/xtensa-esp-elf/esp-14.2.0_20240906/xtensa-esp-elf/bin:/home/yannik/.surrealdb:/home/yannik/.pyenv/shims:/home/yannik/.pyenv/bin:/home/yannik/.cargo/bin:/home/yannik/.local/bin:/home/yannik/bin:/usr/local/bin:/usr/lib64/ccache:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/var/lib/snapd/snap/bin:/home/yannik/development/flutter/bin" +LIBCLANG_PATH = "/home/yannik/.rustup/toolchains/esp/xtensa-esp32-elf-clang/esp-15.0.0-20221201/esp-clang/lib" diff --git a/lvgl-based/Cargo.toml b/lvgl-based/Cargo.toml index 9c38d0e..66c4a51 100644 --- a/lvgl-based/Cargo.toml +++ b/lvgl-based/Cargo.toml @@ -1,18 +1,18 @@ [workspace] [package] -name = "rust-m5stack-lvgl-demo" -version = "0.1.0" -authors = ["enelson1001 "] -edition = "2021" -resolver = "2" +name = "rust-m5stack-lvgl-demo" +version = "0.1.0" +authors = ["enelson1001 "] +edition = "2021" +resolver = "2" rust-version = "1.71" [profile.release] opt-level = "s" [profile.dev] -debug = true # Symbols are nice and they don't increase the size on Flash +debug = true # Symbols are nice and they don't increase the size on Flash opt-level = "z" [[bin]] @@ -21,6 +21,12 @@ name = "starter" [[bin]] name = "timer" +[[bin]] +name = "light-control" + +[[bin]] +name = "microwave-ui" + [features] default = ["embassy", "esp-idf-svc/native", "std"] @@ -36,13 +42,13 @@ embassy = [ ] [dependencies] -log = { version = "0.4", default-features = false } +log = { version = "0.4", default-features = false } esp-idf-svc = { version = "0.49.1", default-features = false } esp-idf-hal = { version = "0.44.1" } esp-idf-sys = { version = "0.35.0" } -cstr_core = "0.2.1" +cstr_core = "0.2.1" embedded-graphics-core = "0.4.0" lvgl = { version = "0.6.2", default-features = false, features = [ @@ -53,19 +59,19 @@ lvgl = { version = "0.6.2", default-features = false, features = [ lvgl-sys = { version = "0.6.2" } display-interface-spi = "0.5.0" -mipidsi = "0.8.0" +mipidsi = "0.8.0" static_cell = "2.1.0" xpt2046 = { git = "https://github.com/Yandrik/xpt2046.git", version = "0.3.1" } embedded-graphics-profiler-display = { version = "0.1.0", path = "../embedded-graphics-profiler-display", features = ["std"] } - +heapless = "0.8.0" [build-dependencies] embuild = "0.32.0" [patch.crates-io] -lvgl = { git = "https://github.com/enelson1001/lv_binding_rust" } +lvgl = { git = "https://github.com/enelson1001/lv_binding_rust" } lvgl-sys = { git = "https://github.com/enelson1001/lv_binding_rust" } diff --git a/lvgl-based/lvgl-configs/lv_conf.h b/lvgl-based/lvgl-configs/lv_conf.h index bae8bfb..fb0f586 100644 --- a/lvgl-based/lvgl-configs/lv_conf.h +++ b/lvgl-based/lvgl-configs/lv_conf.h @@ -328,17 +328,17 @@ *https://fonts.google.com/specimen/Montserrat*/ #define LV_FONT_MONTSERRAT_8 0 #define LV_FONT_MONTSERRAT_10 0 -#define LV_FONT_MONTSERRAT_12 0 +#define LV_FONT_MONTSERRAT_12 1 #define LV_FONT_MONTSERRAT_14 1 -#define LV_FONT_MONTSERRAT_16 0 +#define LV_FONT_MONTSERRAT_16 1 #define LV_FONT_MONTSERRAT_18 0 #define LV_FONT_MONTSERRAT_20 0 #define LV_FONT_MONTSERRAT_22 0 -#define LV_FONT_MONTSERRAT_24 0 +#define LV_FONT_MONTSERRAT_24 1 #define LV_FONT_MONTSERRAT_26 0 #define LV_FONT_MONTSERRAT_28 0 #define LV_FONT_MONTSERRAT_30 0 -#define LV_FONT_MONTSERRAT_32 0 +#define LV_FONT_MONTSERRAT_32 1 #define LV_FONT_MONTSERRAT_34 0 #define LV_FONT_MONTSERRAT_36 0 #define LV_FONT_MONTSERRAT_38 0 diff --git a/lvgl-based/src/bin/light-control.rs b/lvgl-based/src/bin/light-control.rs new file mode 100644 index 0000000..b470881 --- /dev/null +++ b/lvgl-based/src/bin/light-control.rs @@ -0,0 +1,543 @@ +use std::{ + cell::RefCell, + cmp::min, + str::FromStr, + sync::mpsc::channel, + thread, + time::{Duration, Instant}, +}; + +use cstr_core::CString; +use display_interface_spi::SPIInterface; +use embedded_graphics_core::{draw_target::DrawTarget, prelude::Point}; +use embedded_graphics_profiler_display::ProfilerDisplay; +use esp_idf_hal::spi::SpiSingleDeviceDriver; +use esp_idf_hal::{ + delay::{self, Delay}, + gpio::*, + peripherals::Peripherals, + spi::{config::DriverConfig, Dma, SpiConfig, SpiDeviceDriver}, + units::FromValueType, // for converting 26MHz to value +}; +use lvgl::{ + font::Font, + input_device::{ + pointer::{Pointer, PointerInputData}, + InputDriver, + }, + style::{FlexAlign, FlexFlow, Layout, Style}, + widgets::{Btn, Label, Slider, Switch}, + Align, + AnimationState, + Color, + Display, + DrawBuffer, + Event, + LvError, + NativeObject, + Obj, + Part, + Screen, + TextAlign, + Widget, +}; +use mipidsi::{ + models::ILI9486Rgb565, + options::{ColorInversion, ColorOrder, Orientation, Rotation}, + Builder, +}; +use xpt2046::Xpt2046; + +fn lerp_fixed(start: u8, end: u8, t: u8, max_t: u8) -> u8 { + let (start, end, t, max_t) = (start as u16, end as u16, t as u16, max_t as u16); + let t = t.min(max_t); + let result = start + ((end - start.min(end)) * t + (max_t / 2)) / max_t; + result as u8 +} + +#[derive(Debug, Clone)] +struct Lamp { + pub name: heapless::String<64>, + pub on: bool, + pub brightness: u8, +} + +impl Lamp { + pub fn new(name: &str) -> Self { + Self { + name: heapless::String::from(heapless::String::from_str(name).unwrap()), + on: false, + brightness: 255, + } + } +} + +enum Page<'a> { + Home, + LampCtrl(&'a Lamp), +} +struct AppData { + lamps: heapless::Vec, +} + +impl AppData { + fn new() -> Self { + Self { + lamps: heapless::Vec::new(), + } + } + + fn add_lamp(&mut self, name: &str) { + self.lamps.push(Lamp::new(name)).unwrap(); + } +} + +fn main() -> Result<(), LvError> { + const HOR_RES: u32 = 320; + const VER_RES: u32 = 240; + const LINES: u32 = 20; + + // It is necessary to call this function once. Otherwise some patches to the + // runtime implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71 + esp_idf_svc::sys::link_patches(); + + // Initialize lvgl + lvgl::init(); + + // Bind the log crate to the ESP Logging facilities + esp_idf_svc::log::EspLogger::initialize_default(); + + log::info!("================= Starting App ======================="); + + let peripherals = Peripherals::take().unwrap(); + + // #[allow(unused)] + let pins = peripherals.pins; + + let sclk = pins.gpio14; + let miso = pins.gpio12; + let mosi = pins.gpio13; + let cs = pins.gpio15; + let dc = pins.gpio2; + + let spi = SpiDeviceDriver::new_single( + peripherals.spi2, + sclk, // sclk + mosi, // sdo + Some(miso), // sdi + Some(cs), // cs + &DriverConfig::new().dma(Dma::Channel1(4096)), + &SpiConfig::new().write_only(true).baudrate(10.MHz().into()), + ) + .unwrap(); + + // let rst = PinDriver::output(pins.gpio33).unwrap(); + let dc = PinDriver::output(dc).unwrap(); + let di = SPIInterface::new(spi, dc); + + // Turn backlight on + let mut bklt = PinDriver::output(pins.gpio21).unwrap(); + bklt.set_high().unwrap(); + + // Configuration for M5Stack Core Development Kit V1.0 + // Puts display in landscape mode with the three buttons at the bottom of screen + // let mut m5stack_display = Builder::ili9342c_rgb565(di) + // .with_display_size(320, 240) + // .with_color_order(ColorOrder::Bgr) + // .with_orientation(Orientation::Portrait(false)) + // .with_invert_colors(mipidsi::ColorInversion::Inverted) + // .init(&mut delay::Ets, Some(rst)) + // .unwrap(); + + // 0.6.0 + // let mut raw_display = Builder::ili9342c_rgb565(di) + // .with_orientation(Orientation::Portrait(false)) + // .with_color_order(ColorOrder::Bgr) + // .with_invert_colors(true) + // .init(&mut Delay::new_default(), None::>) + // .unwrap(); + + let raw_display = Builder::new(ILI9486Rgb565, di) + .orientation(Orientation { + rotation: Rotation::Deg90, + mirrored: true, + }) + .color_order(ColorOrder::Bgr) + .invert_colors(ColorInversion::Inverted) + .init(&mut Delay::new_default()) + .unwrap(); + + let mut raw_display = ProfilerDisplay::new(raw_display); + + // Stack size value - 20,000 for 10 lines, 40,000 for 20 lines + // let (touch_send, touch_recv) = channel(); + let touch_irq = pins.gpio36; + let touch_mosi = pins.gpio32; + let touch_miso = pins.gpio39; + let touch_clk = pins.gpio25; + let touch_cs = pins.gpio33; + + let mut touch_driver = Xpt2046::new( + SpiDeviceDriver::new_single( + peripherals.spi3, + touch_clk, + touch_mosi, + Some(touch_miso), + Some(touch_cs), + &DriverConfig::new(), + &SpiConfig::new().write_only(true).baudrate(2.MHz().into()), + ) + .unwrap(), + PinDriver::input(touch_irq).unwrap(), + xpt2046::Orientation::LandscapeFlipped, + ); + touch_driver.set_num_samples(1); + touch_driver.init(&mut Delay::new_default()).unwrap(); + + let touch_driver = RefCell::new(touch_driver); + + let _lvgl_thread = thread::Builder::new() + .stack_size(40_000) + .spawn(move || { + println!("thread started"); + + let mut appdata = AppData::new(); + + let buffer = DrawBuffer::<{ (HOR_RES * LINES) as usize }>::default(); + let display = Display::register(buffer, HOR_RES, VER_RES, |refresh| { + raw_display.draw_iter(refresh.as_pixels()).unwrap(); + }) + .unwrap(); + + // Register a new input device that's capable of reading the current state of + // the input + // let mut last_touch = RefCell::new(None); + let _touch_screen = Pointer::register( + || { + let mut td_ref = touch_driver.borrow_mut(); + td_ref.run().expect("Running Touch driver failed"); + if td_ref.is_touched() { + let point = td_ref.get_touch_point(); + // println!("touched {:?}", point); + PointerInputData::Touch(Point::new(point.x + 20, 240 - point.y)).pressed().once() + } else { + // println!("untouched"); + PointerInputData::Touch(Point::new(0, 0)).released().once() + } + }, + &display, + ); + + // Create screen and widgets + let mut screen = display.get_scr_act().unwrap(); + let mut screen_style = Style::default(); + screen_style.set_bg_color(Color::from_rgb((0, 0, 0))); + screen_style.set_radius(0); + screen_style.set_layout(Layout::flex()); + screen_style.set_flex_flow(FlexFlow::ROW_WRAP); + screen_style.set_flex_main_place(FlexAlign::CENTER); + screen_style.set_flex_cross_place(FlexAlign::CENTER); + screen.add_style(Part::Main, &mut screen_style); + + + appdata.add_lamp("Front Door"); + appdata.add_lamp("Living Room"); + appdata.add_lamp("Bedroom"); + appdata.add_lamp("Bathroom"); + appdata.add_lamp("Porch"); + + let mut light_page = Screen::blank().unwrap(); + light_page.set_size(320, 240); + let mut light_page_style = Style::default(); + light_page_style.set_bg_color(Color::from_rgb((0, 0, 0))); + light_page_style.set_radius(0); + light_page.add_style(Part::Main, &mut light_page_style); + + let mut light_page_title_label = Label::create(&mut light_page).unwrap(); + let mut light_page_title_style = Style::default(); + light_page_title_style.set_text_color(Color::from_rgb((255, 255, 255))); + unsafe { light_page_title_style.set_text_font(Font::new_raw(lvgl_sys::lv_font_montserrat_24)) }; + light_page_title_label.add_style(Part::Main, &mut light_page_title_style); + light_page_title_label.set_align(Align::TopMid, 0, 6); + + + let mut brightness_slider = Slider::create(&mut light_page).unwrap(); + brightness_slider.set_size(250, 10); + // brightness_slider.range(0..255); + brightness_slider.set_value(0, AnimationState::OFF); + brightness_slider.set_align(Align::Center, 0, -20); + + let mut brightness_slider_label = Label::create(&mut light_page).unwrap(); + // no text for now + let mut white_text_style = Style::default(); + white_text_style.set_text_color(Color::from_rgb((255, 255, 255))); + unsafe { white_text_style.set_text_font(Font::new_raw(lvgl_sys::lv_font_montserrat_14)) }; + brightness_slider_label.add_style(Part::Main, &mut white_text_style); + brightness_slider_label.set_text(CString::new("Brightness").unwrap().as_c_str()); + brightness_slider_label.set_align(Align::Center, 0, -50); + + + // Add switch + let mut light_switch = Switch::create(&mut light_page).unwrap(); + light_switch.set_align(Align::Center, 0, 40); + + let mut light_switch_label = Label::create(&mut light_page).unwrap(); + // no text for now + let mut lslabel_style = Style::default(); + lslabel_style.set_text_color(Color::from_rgb((255, 255, 255))); + unsafe { lslabel_style.set_text_font(Font::new_raw(lvgl_sys::lv_font_montserrat_14)) }; + light_switch_label.add_style(Part::Main, &mut lslabel_style); + light_switch_label.set_text(CString::new("On/Off").unwrap().as_c_str()); + light_switch_label.set_align(Align::Center, 0, 10); + + // Add back button + let mut back_btn = Btn::create(&mut light_page).unwrap(); + back_btn.set_size(40, 40); + back_btn.set_align(Align::TopLeft, 10, 10); + back_btn.on_event(|_btn, event| { + if let Event::Pressed = event { + display.set_scr_act(&mut screen); + } + }); + + let mut back_label = Label::create(&mut back_btn).unwrap(); + back_label.set_text(CString::new(b"\xef\x81\x93").unwrap().as_c_str()); // Font Awesome left arrow + let mut back_style = Style::default(); + unsafe { back_style.set_text_font(Font::new_raw(lvgl_sys::lv_font_montserrat_24)) }; + back_label.add_style(Part::Main, &mut back_style); + + let mut light_ctrls = heapless::Vec::<(Btn, Label, Label), 8>::new(); + + let mut page = Page::Home; + + for lamp in appdata.lamps.iter_mut() { + let mut cont = Btn::new().unwrap(); + cont.set_size(100, 110); + // cont.set_align(Align::TopLeft, 0, 0); + + cont.on_event(|_btn, event| { + if let Event::Pressed = event { + println!("lamp {:?}", lamp.name); + // page = Page::LampCtrl(lamp); + + brightness_slider.set_value(lerp_fixed(0, 100, lamp.brightness, 255).into(), AnimationState::OFF); + unsafe { + if lamp.on { + lvgl_sys::lv_obj_add_state(light_switch.raw().as_ptr(), lvgl_sys::LV_STATE_CHECKED as u16); + } else { + lvgl_sys::lv_obj_clear_state(light_switch.raw().as_ptr(), lvgl_sys::LV_STATE_CHECKED as u16); + } + } + light_switch.on_event(|_ls, event| { + if let Event::ValueChanged | Event::Released = event { + let on = unsafe { lvgl_sys::lv_obj_has_state(_ls.raw().as_ptr(), lvgl_sys::LV_STATE_CHECKED as u16) }; + lamp.on = on; + } + }); + brightness_slider.on_event(|_sldr, event| { + // println!("event: {:?}", event); + if let Event::ValueChanged | Event::Released = event { + let brightness = _sldr.get_value(); + println!("brightness: {}", brightness); + lamp.brightness = lerp_fixed(0, 255, brightness as u8, 100); + println!("brightness: {}", lamp.brightness); + } + }); + light_page_title_label.set_text(CString::new(lamp.name.as_str()).unwrap().as_c_str()); + + display.set_scr_act(&mut light_page); + } + }); + + let mut style_name = Style::default(); + style_name.set_text_align(TextAlign::Center); + style_name.set_text_color(Color::from_rgb((255, 255, 255))); + unsafe { style_name.set_text_font(Font::new_raw(lvgl_sys::lv_font_montserrat_14)) }; + + let mut style_icon = Style::default(); + style_icon.set_text_color(Color::from_rgb((255, 255, 255))); + unsafe { style_icon.set_text_font(Font::new_raw(lvgl_sys::lv_font_montserrat_32)) }; + style_icon.set_text_align(TextAlign::Center); + + + let mut icon = Label::create(&mut cont).unwrap(); + icon.set_text(CString::new(b"\xef\x84\xa4").unwrap().as_c_str()); + icon.add_style(Part::Main, Box::leak(Box::new(style_icon))); + icon.set_align(Align::Center, 0, 0); + + let mut name = Label::create(&mut cont).unwrap(); + name.set_text(CString::new(lamp.name.as_str()).unwrap().as_c_str()); + // style_name.set_text_color(Color::from_rgb((255, 255, 255))); // white + name.add_style(Part::Main, Box::leak(Box::new(style_name))); + name.set_align(Align::BottomMid, 0, -4); + + light_ctrls.push((cont, icon, name)) + .unwrap(); + } + + + // let mut time = Label::new().unwrap(); + // time.set_text(CString::new("00:10:000").unwrap().as_c_str()); + // let mut style_time = Style::default(); + // style_time.set_text_color(Color::from_rgb((255, 255, 255))); // white + // style_time.set_text_align(TextAlign::Center); + // unsafe { style_time.set_text_font(Font::new_raw(lvgl_sys::lv_font_montserrat_24)) }; + // time.add_style(Part::Main, &mut style_time); + // time.set_align(Align::Center, 0, 0); + + /* + + + // let mut cont = Obj::new().unwrap(); + // cont.set_size(320, 50); + // let mut style = Style::default(); + // style.set_flex_flow(FlexFlow::ROW); + // style.set_flex_main_place(FlexAlign::SPACE_EVENLY); + // style.set_flex_cross_place(FlexAlign::CENTER); + // // style.set_bg_opa(0); + // style.set_border_width(0); + // cont.add_style(Part::Any, &mut style); + // cont.set_align(Align::Center, 0, 40); + + let mut button_add = Btn::create(&mut screen).unwrap(); + button_add.set_align(Align::Center, -50, 32); + // button_add.set_pos(130, 150); + button_add.set_size(30, 30); + // button_add.set_align(Align::Center, 0, 0); + let mut btn_lbl1 = Label::create(&mut button_add).unwrap(); + btn_lbl1.set_text(CString::new(b"+").unwrap().as_c_str()); + + button_add.on_event(|_btn, event| { + if let Event::Pressed = event { + println!("pressed"); + if appdata.timer_stopped() { + appdata.add_secs(10); + } + } + // println!("Button received event: {:?}", event); + }); + + let mut button_sub = Btn::create(&mut screen).unwrap(); + // button_sub.set_pos(170, 150); + button_sub.set_size(30, 30); + button_sub.set_align(Align::Center, 50, 35); + // button_sub.set_align(Align::Center, 0, 0); + let mut btn_lbl2 = Label::create(&mut button_sub).unwrap(); + btn_lbl2.set_text(CString::new(b"-").unwrap().as_c_str()); + + button_sub.on_event(|_btn, event| { + if let Event::Pressed = event { + if appdata.timer_stopped() { + appdata.sub_secs(10); + } + } + }); + + let mut center_label = Label::new().unwrap(); + center_label.set_text(CString::new(b"+/- 10s").unwrap().as_c_str()); + center_label.set_align(Align::Center, 0, 35); + let mut cl_style = Style::default(); + cl_style.set_text_color(Color::from_rgb((255, 255, 255))); + center_label.add_style(Part::Main, &mut cl_style); + + let mut button_reset = Btn::create(&mut screen).unwrap(); + //button_reset.set_pos(123, 190); + button_reset.set_align(Align::Center, -22, 70); + button_reset.set_size(35, 35); + let mut btn_lbl3 = Label::create(&mut button_reset).unwrap(); + btn_lbl3.set_text(CString::new(b"\xEF\x80\xA1").unwrap().as_c_str()); + + + const PLAY: &'static [u8; 3] = b"\xEF\x81\x8B"; + const PAUSE: &'static [u8; 3] = b"\xEF\x81\x8C"; + const STOP: &'static [u8; 3] = b"\xEF\x81\x8D"; + + let mut button_start_stop = Btn::create(&mut screen).unwrap(); + // button_start_stop.set_pos(173, 190); + button_start_stop.set_align(Align::Center, 22, 70); + button_start_stop.set_size(35, 35); + let mut btn_lbl4 = Label::create(&mut button_start_stop).unwrap(); + btn_lbl4.set_text(CString::new(b"\xEF\x81\x8B").unwrap().as_c_str()); + + button_reset.on_event(|_btn, event| { + if let Event::Pressed = event { + appdata.reset_timer(); + btn_lbl4.set_text(CString::new(PLAY).unwrap().as_c_str()); + } + }); + + button_start_stop.on_event(|_btn, event| { + if let Event::Pressed = event { + if appdata.timer_finished() { + // println!("Resetting finished timer"); + appdata.reset_timer(); + btn_lbl4.set_text(CString::new(PLAY).unwrap().as_c_str()); + } else if appdata.timer_running() { + // println!("Pausing timer"); + appdata.pause_timer(); + btn_lbl4.set_text(CString::new(PLAY).unwrap().as_c_str()); + } else { + // println!("Starting timer"); + appdata.start_timer(); + if appdata.timer_finished() { + // println!("Timer already finished"); + btn_lbl4.set_text(CString::new(STOP).unwrap().as_c_str()); + } else { + // println!("Timer running"); + btn_lbl4.set_text(CString::new(PAUSE).unwrap().as_c_str()); + } + } + } + }); + + let mut was_finished = false; + let mut last_rem_time: Duration = appdata.remaining() + Duration::from_millis(10); + + */ + + let mut last_time = Instant::now(); + loop { + let start_time = Instant::now(); + + let start_draw_time = Instant::now(); + lvgl::task_handler(); + + // Simulate clock - so sleep for one second so time text is incremented in + // seconds + // delay::FreeRtos::delay_ms(1); + + let now_time = Instant::now(); + lvgl::tick_inc(now_time.duration_since(last_time)); + last_time = now_time; + + + let end_time = Instant::now(); + let draw_time = raw_display.get_time(); + let prep_time = start_draw_time - start_time; + let proc_time = end_time - start_draw_time; + let proc_time = proc_time - min(draw_time, proc_time); + + if draw_time.as_micros() > 0 { + println!( + "draw time: {}.{:03}ms | prep time: {}.{:03}ms | proc time: {}.{:03}ms | total time: {}.{:03}ms", + draw_time.as_millis(), + draw_time.as_micros() % 100, + prep_time.as_millis(), + prep_time.as_micros() % 100, + proc_time.as_millis(), + proc_time.as_micros() % 100, + (draw_time + prep_time + proc_time).as_millis(), + (draw_time + prep_time + proc_time).as_micros() % 100, ); + } + raw_display.reset_time(); + + delay::FreeRtos::delay_ms(2); + } + }) + .unwrap(); + + loop { + // Don't exit application + delay::FreeRtos::delay_ms(1_000_000); + } +} diff --git a/lvgl-based/src/bin/microwave-ui.rs b/lvgl-based/src/bin/microwave-ui.rs new file mode 100644 index 0000000..aac343f --- /dev/null +++ b/lvgl-based/src/bin/microwave-ui.rs @@ -0,0 +1,522 @@ +use std::{ + cell::RefCell, + cmp::min, + str::FromStr, + sync::mpsc::channel, + thread, + time::{Duration, Instant}, +}; + +use cstr_core::CString; +use display_interface_spi::SPIInterface; +use embedded_graphics_core::{draw_target::DrawTarget, prelude::Point}; +use embedded_graphics_profiler_display::ProfilerDisplay; +use esp_idf_hal::spi::SpiSingleDeviceDriver; +use esp_idf_hal::{ + delay::{self, Delay}, + gpio::*, + peripherals::Peripherals, + spi::{config::DriverConfig, Dma, SpiConfig, SpiDeviceDriver}, + units::FromValueType, // for converting 26MHz to value +}; +use lvgl::{ + font::Font, + input_device::{ + pointer::{Pointer, PointerInputData}, + InputDriver, + }, + style::{FlexAlign, FlexFlow, Layout, Style}, + widgets::{Arc, Btn, Label, Slider, Switch}, + Align, + AnimationState, + Color, + Display, + DrawBuffer, + Event, + LvError, + NativeObject, + Obj, + Part, + Screen, + TextAlign, + Widget, +}; +use mipidsi::{ + models::ILI9486Rgb565, + options::{ColorInversion, ColorOrder, Orientation, Rotation}, + Builder, +}; +use xpt2046::Xpt2046; + +fn lerp_fixed(start: u8, end: u8, t: u8, max_t: u8) -> u8 { + let (start, end, t, max_t) = (start as u16, end as u16, t as u16, max_t as u16); + let t = t.min(max_t); + let result = start + ((end - start.min(end)) * t + (max_t / 2)) / max_t; + result as u8 +} + +#[derive(Debug, Clone)] +struct Lamp { + pub name: heapless::String<64>, + pub on: bool, + pub brightness: u8, +} +struct AppData { + timer_start: Instant, + timer_set_duration: Duration, + timer_remaining_duration: Duration, + timer_running: bool, + timer_paused: bool, + wattage_level: u8, +} + +impl AppData { + fn new() -> Self { + Self { + timer_start: Instant::now(), + timer_set_duration: Duration::from_secs(10), + timer_remaining_duration: Duration::from_secs(10), + timer_running: false, + timer_paused: false, + wattage_level: 5, + } + } + + fn set_timer_duration(&mut self, duration: Duration) { + self.timer_set_duration = duration; + } + + fn add_secs(&mut self, secs: u64) { + self.set_timer_duration( + self.timer_set_duration + .checked_add(Duration::from_secs(secs)) + .unwrap_or(Duration::from_secs(5999)) + .min(Duration::from_secs(5999)), + ); + } + + fn sub_secs(&mut self, secs: u64) { + self.set_timer_duration( + self.timer_set_duration + .checked_sub(Duration::from_secs(secs)) + .unwrap_or(Duration::from_secs(10)) + .max(Duration::from_secs(10)), + ); + } + + fn start_timer(&mut self) { + if !self.timer_paused { + self.timer_remaining_duration = self.timer_set_duration; + } + + self.timer_start = Instant::now(); + self.timer_running = true; + self.timer_paused = false; + } + + fn pause_timer(&mut self) { + self.timer_paused = true; + self.timer_remaining_duration = self + .timer_remaining_duration + .checked_sub(self.timer_start.elapsed()) + .unwrap_or(Duration::from_secs(0)); + } + + fn reset_timer(&mut self) { + self.timer_start = Instant::now(); + self.timer_paused = false; + self.timer_running = false; + self.timer_remaining_duration = self.timer_set_duration; + } + + fn remaining(&self) -> Duration { + if self.timer_stopped() { + self.timer_set_duration + } else if self.timer_paused() { + self.timer_remaining_duration + } else { + self.timer_remaining_duration + .checked_sub(self.timer_start.elapsed()) + .unwrap_or(Duration::from_secs(0)) + } + } + + fn timer_stopped(&self) -> bool { + !self.timer_running + } + + fn timer_paused(&self) -> bool { + self.timer_running && self.timer_paused + } + + fn timer_running(&self) -> bool { + self.timer_running && !self.timer_paused + } + + fn timer_finished(&self) -> bool { + self.timer_running && self.remaining() == Duration::from_secs(0) + } + + fn set_wattage_level(&mut self, level: u8) { + assert!(level < 6, "wattage level cannot be over 6"); + self.wattage_level = level; + } + + fn get_wattage_level_str(&self) -> &'static str { + match self.wattage_level { + 0 => "180W", + 1 => "220W", + 2 => "360W", + 3 => "480W", + 4 => "620W", + 5 => "800W", + _ => unreachable!(), + } + } +} + +fn main() -> Result<(), LvError> { + const HOR_RES: u32 = 320; + const VER_RES: u32 = 240; + const LINES: u32 = 20; + + // It is necessary to call this function once. Otherwise some patches to the + // runtime implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71 + esp_idf_svc::sys::link_patches(); + + // Initialize lvgl + lvgl::init(); + + // Bind the log crate to the ESP Logging facilities + esp_idf_svc::log::EspLogger::initialize_default(); + + log::info!("================= Starting App ======================="); + + let peripherals = Peripherals::take().unwrap(); + + // #[allow(unused)] + let pins = peripherals.pins; + + let sclk = pins.gpio14; + let miso = pins.gpio12; + let mosi = pins.gpio13; + let cs = pins.gpio15; + let dc = pins.gpio2; + + let spi = SpiDeviceDriver::new_single( + peripherals.spi2, + sclk, // sclk + mosi, // sdo + Some(miso), // sdi + Some(cs), // cs + &DriverConfig::new().dma(Dma::Channel1(4096)), + &SpiConfig::new().write_only(true).baudrate(10.MHz().into()), + ) + .unwrap(); + + // let rst = PinDriver::output(pins.gpio33).unwrap(); + let dc = PinDriver::output(dc).unwrap(); + let di = SPIInterface::new(spi, dc); + + // Turn backlight on + let mut bklt = PinDriver::output(pins.gpio21).unwrap(); + bklt.set_high().unwrap(); + + // Configuration for M5Stack Core Development Kit V1.0 + // Puts display in landscape mode with the three buttons at the bottom of screen + // let mut m5stack_display = Builder::ili9342c_rgb565(di) + // .with_display_size(320, 240) + // .with_color_order(ColorOrder::Bgr) + // .with_orientation(Orientation::Portrait(false)) + // .with_invert_colors(mipidsi::ColorInversion::Inverted) + // .init(&mut delay::Ets, Some(rst)) + // .unwrap(); + + // 0.6.0 + // let mut raw_display = Builder::ili9342c_rgb565(di) + // .with_orientation(Orientation::Portrait(false)) + // .with_color_order(ColorOrder::Bgr) + // .with_invert_colors(true) + // .init(&mut Delay::new_default(), None::>) + // .unwrap(); + + let raw_display = Builder::new(ILI9486Rgb565, di) + .orientation(Orientation { + rotation: Rotation::Deg90, + mirrored: true, + }) + .color_order(ColorOrder::Bgr) + .invert_colors(ColorInversion::Inverted) + .init(&mut Delay::new_default()) + .unwrap(); + + let mut raw_display = ProfilerDisplay::new(raw_display); + + // Stack size value - 20,000 for 10 lines, 40,000 for 20 lines + // let (touch_send, touch_recv) = channel(); + let touch_irq = pins.gpio36; + let touch_mosi = pins.gpio32; + let touch_miso = pins.gpio39; + let touch_clk = pins.gpio25; + let touch_cs = pins.gpio33; + + let mut touch_driver = Xpt2046::new( + SpiDeviceDriver::new_single( + peripherals.spi3, + touch_clk, + touch_mosi, + Some(touch_miso), + Some(touch_cs), + &DriverConfig::new(), + &SpiConfig::new().write_only(true).baudrate(2.MHz().into()), + ) + .unwrap(), + PinDriver::input(touch_irq).unwrap(), + xpt2046::Orientation::LandscapeFlipped, + ); + touch_driver.set_num_samples(1); + touch_driver.init(&mut Delay::new_default()).unwrap(); + + let touch_driver = RefCell::new(touch_driver); + + let _lvgl_thread = thread::Builder::new() + .stack_size(40_000) + .spawn(move || { + println!("thread started"); + + let mut appdata = AppData::new(); + + let buffer = DrawBuffer::<{ (HOR_RES * LINES) as usize }>::default(); + let display = Display::register(buffer, HOR_RES, VER_RES, |refresh| { + raw_display.draw_iter(refresh.as_pixels()).unwrap(); + }) + .unwrap(); + + // Register a new input device that's capable of reading the current state of + // the input + // let mut last_touch = RefCell::new(None); + let _touch_screen = Pointer::register( + || { + let mut td_ref = touch_driver.borrow_mut(); + td_ref.run().expect("Running Touch driver failed"); + if td_ref.is_touched() { + let point = td_ref.get_touch_point(); + // println!("touched {:?}", point); + PointerInputData::Touch(Point::new(point.x + 20, 240 - point.y)).pressed().once() + } else { + // println!("untouched"); + PointerInputData::Touch(Point::new(0, 0)).released().once() + } + }, + &display, + ); + + // Create screen and widgets + let mut screen = display.get_scr_act().unwrap(); + let mut screen_style = Style::default(); + screen_style.set_bg_color(Color::from_rgb((0, 0, 0))); + screen_style.set_radius(0); + // screen_style.set_layout(Layout::flex()); + // screen_style.set_flex_flow(FlexFlow::ROW_WRAP); + // screen_style.set_flex_main_place(FlexAlign::CENTER); + // screen_style.set_flex_cross_place(FlexAlign::CENTER); + screen.add_style(Part::Main, &mut screen_style); + + + let mut wattage_label = Label::create(&mut screen).unwrap(); + wattage_label.set_align(Align::Center, -70, 0); + let mut wattage_style = Style::default(); + wattage_style.set_text_color(Color::from_rgb((255, 255, 255))); + unsafe { wattage_style.set_text_font(Font::new_raw(lvgl_sys::lv_font_montserrat_32)) }; + wattage_label.add_style(Part::Main, &mut wattage_style); + wattage_label.set_text(CString::new(appdata.get_wattage_level_str()).unwrap().as_c_str()); + + + let mut power_arc = Arc::create(&mut screen).unwrap(); + power_arc.set_size(150, 150); + power_arc.set_align(Align::Center, -70, 0); + power_arc.set_angles(135, 45); // Creates a 270-degree arc + power_arc.set_rotation(0); + power_arc.set_bg_angles(135, 45); + unsafe { + lvgl_sys::lv_arc_set_range(power_arc.raw().as_mut(), 0, 5); + } + + power_arc.on_event(|arc, event| { + if let Event::ValueChanged = event { + let mut value = unsafe { + lvgl_sys::lv_arc_get_value(arc.raw().as_mut()) + }; + appdata.set_wattage_level((value as u8).min(6)); + wattage_label.set_text(CString::new(appdata.get_wattage_level_str()).unwrap().as_c_str()); + } + }); + + // Add timer display on the right + let mut time_label = Label::new().unwrap(); + time_label.set_text(CString::new("00:10:000").unwrap().as_c_str()); + let mut style_time = Style::default(); + style_time.set_text_color(Color::from_rgb((255, 255, 255))); + style_time.set_text_align(TextAlign::Center); + unsafe { style_time.set_text_font(Font::new_raw(lvgl_sys::lv_font_montserrat_24)) }; + time_label.add_style(Part::Main, &mut style_time); + time_label.set_align(Align::Center, 80, 0); + + // Add + and - buttons at bottom + let mut button_add = Btn::create(&mut screen).unwrap(); + button_add.set_align(Align::Center, 10, 70); + button_add.set_size(30, 30); + let mut btn_lbl1 = Label::create(&mut button_add).unwrap(); + btn_lbl1.set_text(CString::new(b"+").unwrap().as_c_str()); + + let mut button_sub = Btn::create(&mut screen).unwrap(); + button_sub.set_size(30, 30); + button_sub.set_align(Align::Center, 130, 70); + let mut btn_lbl2 = Label::create(&mut button_sub).unwrap(); + btn_lbl2.set_text(CString::new(b"-").unwrap().as_c_str()); + + // Add timer control logic from timer.rs + button_add.on_event(|_btn, event| { + if let Event::Pressed = event { + if appdata.timer_stopped() { + appdata.add_secs(10); + } + } + }); + + button_sub.on_event(|_btn, event| { + if let Event::Pressed = event { + if appdata.timer_stopped() { + appdata.sub_secs(10); + } + } + }); + + // Add reset button + let mut button_reset = Btn::create(&mut screen).unwrap(); + button_reset.set_align(Align::Center, 50, 70); + button_reset.set_size(35, 35); + let mut btn_lbl3 = Label::create(&mut button_reset).unwrap(); + btn_lbl3.set_text(CString::new(b"\xEF\x80\xA1").unwrap().as_c_str()); + + // Add start/stop/pause button + const PLAY: &'static [u8; 3] = b"\xEF\x81\x8B"; + const PAUSE: &'static [u8; 3] = b"\xEF\x81\x8C"; + const STOP: &'static [u8; 3] = b"\xEF\x81\x8D"; + + let mut button_start_stop = Btn::create(&mut screen).unwrap(); + button_start_stop.set_align(Align::Center, 90, 70); + button_start_stop.set_size(35, 35); + let mut btn_lbl4 = Label::create(&mut button_start_stop).unwrap(); + btn_lbl4.set_text(CString::new(PLAY).unwrap().as_c_str()); + + // Add event handlers + button_reset.on_event(|_btn, event| { + if let Event::Pressed = event { + appdata.reset_timer(); + btn_lbl4.set_text(CString::new(PLAY).unwrap().as_c_str()); + unsafe { + lvgl_sys::lv_obj_clear_state(button_add.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + lvgl_sys::lv_obj_clear_state(button_sub.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + lvgl_sys::lv_obj_clear_state(power_arc.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + } + } + }); + + button_start_stop.on_event(|_btn, event| { + if let Event::Pressed = event { + if appdata.timer_finished() { + appdata.reset_timer(); + btn_lbl4.set_text(CString::new(PLAY).unwrap().as_c_str()); + // Enable all buttons + unsafe { + lvgl_sys::lv_obj_clear_state(button_add.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + lvgl_sys::lv_obj_clear_state(button_sub.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + lvgl_sys::lv_obj_clear_state(power_arc.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + } + } else if appdata.timer_running() { + appdata.pause_timer(); + btn_lbl4.set_text(CString::new(PLAY).unwrap().as_c_str()); + // Enable all buttons + unsafe { + lvgl_sys::lv_obj_clear_state(button_add.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + lvgl_sys::lv_obj_clear_state(button_sub.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + lvgl_sys::lv_obj_clear_state(power_arc.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + } + } else { + appdata.start_timer(); + if appdata.timer_finished() { + btn_lbl4.set_text(CString::new(STOP).unwrap().as_c_str()); + } else { + btn_lbl4.set_text(CString::new(PAUSE).unwrap().as_c_str()); + // Disable buttons while running + unsafe { + lvgl_sys::lv_obj_add_state(button_add.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + lvgl_sys::lv_obj_add_state(button_sub.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + lvgl_sys::lv_obj_add_state(power_arc.raw().as_mut(), lvgl_sys::LV_STATE_DISABLED as u16); + } + } + } + } + }); + + let mut was_finished = false; + let mut last_rem_time: Duration = appdata.remaining() + Duration::from_millis(10); + + let mut last_time = Instant::now(); + loop { + let start_time = Instant::now(); + + let dur = appdata.remaining(); + if last_rem_time != dur { + let val = CString::new(format!("{:02}:{:02}:{:03}", + dur.as_secs() / 60, + dur.as_secs() % 60, + dur.as_millis() % 1000)).unwrap(); + + time_label.set_text(&val).unwrap(); + last_rem_time = dur; + } + + if !was_finished && appdata.timer_finished() { + btn_lbl4.set_text(CString::new(STOP).unwrap().as_c_str()); + } + was_finished = appdata.timer_finished(); + + let start_draw_time = Instant::now(); + lvgl::task_handler(); + + let now_time = Instant::now(); + lvgl::tick_inc(now_time.duration_since(last_time)); + last_time = now_time; + + let end_time = Instant::now(); + let draw_time = raw_display.get_time(); + let prep_time = start_draw_time - start_time; + let proc_time = end_time - start_draw_time; + let proc_time = proc_time - min(draw_time, proc_time); + + if draw_time.as_micros() > 0 { + println!( + "draw time: {}.{:03}ms | prep time: {}.{:03}ms | proc time: {}.{:03}ms | total time: {}.{:03}ms", + draw_time.as_millis(), + draw_time.as_micros() % 100, + prep_time.as_millis(), + prep_time.as_micros() % 100, + proc_time.as_millis(), + proc_time.as_micros() % 100, + (draw_time + prep_time + proc_time).as_millis(), + (draw_time + prep_time + proc_time).as_micros() % 100, ); + } + raw_display.reset_time(); + + delay::FreeRtos::delay_ms(2); + } + }) + .unwrap(); + + loop { + // Don't exit application + delay::FreeRtos::delay_ms(1_000_000); + } +} diff --git a/lvgl-based/src/bin/timer.rs b/lvgl-based/src/bin/timer.rs index 845fa33..5e89642 100644 --- a/lvgl-based/src/bin/timer.rs +++ b/lvgl-based/src/bin/timer.rs @@ -24,7 +24,7 @@ use lvgl::{ pointer::{Pointer, PointerInputData}, InputDriver, }, - style::Style, + style::{FlexAlign, FlexFlow, Style}, widgets::{Btn, Label}, Align, Color, @@ -32,6 +32,7 @@ use lvgl::{ DrawBuffer, Event, LvError, + Obj, Part, TextAlign, Widget, @@ -282,9 +283,11 @@ fn main() -> Result<(), LvError> { screen.add_style(Part::Main, &mut screen_style); let mut time = Label::new().unwrap(); + time.set_text(CString::new("00:10:000").unwrap().as_c_str()); let mut style_time = Style::default(); style_time.set_text_color(Color::from_rgb((255, 255, 255))); // white style_time.set_text_align(TextAlign::Center); + unsafe { style_time.set_text_font(Font::new_raw(lvgl_sys::lv_font_montserrat_24)) }; // Custom font requires lvgl-sys in Cargo.toml and 'use lvgl_sys' in this file @@ -293,10 +296,22 @@ fn main() -> Result<(), LvError> { // Time text will be centered in screen time.set_align(Align::Center, 0, 0); + // let mut cont = Obj::new().unwrap(); + // cont.set_size(320, 50); + // let mut style = Style::default(); + // style.set_flex_flow(FlexFlow::ROW); + // style.set_flex_main_place(FlexAlign::SPACE_EVENLY); + // style.set_flex_cross_place(FlexAlign::CENTER); + // // style.set_bg_opa(0); + // style.set_border_width(0); + // cont.add_style(Part::Any, &mut style); + // cont.set_align(Align::Center, 0, 40); + let mut button_add = Btn::create(&mut screen).unwrap(); - // button_add.set_align(Align::Center, -50, -100); - button_add.set_pos(130, 150); + button_add.set_align(Align::Center, -50, 32); + // button_add.set_pos(130, 150); button_add.set_size(30, 30); + // button_add.set_align(Align::Center, 0, 0); let mut btn_lbl1 = Label::create(&mut button_add).unwrap(); btn_lbl1.set_text(CString::new(b"+").unwrap().as_c_str()); @@ -311,8 +326,10 @@ fn main() -> Result<(), LvError> { }); let mut button_sub = Btn::create(&mut screen).unwrap(); - button_sub.set_pos(170, 150); + // button_sub.set_pos(170, 150); button_sub.set_size(30, 30); + button_sub.set_align(Align::Center, 50, 35); + // button_sub.set_align(Align::Center, 0, 0); let mut btn_lbl2 = Label::create(&mut button_sub).unwrap(); btn_lbl2.set_text(CString::new(b"-").unwrap().as_c_str()); @@ -324,8 +341,16 @@ fn main() -> Result<(), LvError> { } }); + let mut center_label = Label::new().unwrap(); + center_label.set_text(CString::new(b"+/- 10s").unwrap().as_c_str()); + center_label.set_align(Align::Center, 0, 35); + let mut cl_style = Style::default(); + cl_style.set_text_color(Color::from_rgb((255, 255, 255))); + center_label.add_style(Part::Main, &mut cl_style); + let mut button_reset = Btn::create(&mut screen).unwrap(); - button_reset.set_pos(123, 190); + //button_reset.set_pos(123, 190); + button_reset.set_align(Align::Center, -22, 70); button_reset.set_size(35, 35); let mut btn_lbl3 = Label::create(&mut button_reset).unwrap(); btn_lbl3.set_text(CString::new(b"\xEF\x80\xA1").unwrap().as_c_str()); @@ -336,7 +361,8 @@ fn main() -> Result<(), LvError> { const STOP: &'static [u8; 3] = b"\xEF\x81\x8D"; let mut button_start_stop = Btn::create(&mut screen).unwrap(); - button_start_stop.set_pos(173, 190); + // button_start_stop.set_pos(173, 190); + button_start_stop.set_align(Align::Center, 22, 70); button_start_stop.set_size(35, 35); let mut btn_lbl4 = Label::create(&mut button_start_stop).unwrap(); btn_lbl4.set_text(CString::new(b"\xEF\x81\x8B").unwrap().as_c_str()); @@ -373,15 +399,19 @@ fn main() -> Result<(), LvError> { }); let mut was_finished = false; + let mut last_rem_time: Duration = appdata.remaining() + Duration::from_millis(10); loop { let start_time = Instant::now(); let rem_time = appdata.remaining(); let val = CString::new(format!("{:02}:{:02}:{:03}", - rem_time.as_secs() / 60, - rem_time.as_secs() % 60, - rem_time.as_millis() % 1000)).unwrap(); + rem_time.as_secs() / 60, + rem_time.as_secs() % 60, + rem_time.as_millis() % 1000)).unwrap(); + time.set_text(&val).unwrap(); + last_rem_time = rem_time; + if !was_finished && appdata.timer_finished() { was_finished = true; @@ -419,7 +449,7 @@ fn main() -> Result<(), LvError> { } raw_display.reset_time(); - delay::FreeRtos::delay_ms(1); + delay::FreeRtos::delay_ms(2); } }) diff --git a/slint-based/.cargo/config.toml b/slint-based/.cargo/config.toml index c01d173..6027dce 100644 --- a/slint-based/.cargo/config.toml +++ b/slint-based/.cargo/config.toml @@ -9,7 +9,6 @@ # This file is MIT or Apache-2.0 as per the repository README.md file # -[target.xtensa-esp32-none] # runner = "espflash --monitor" # Select this runner for espflash v1.x.x runner = "espflash flash --monitor --baud 921600" # Select this runner for espflash v2.x.x diff --git a/slint-based/Cargo.toml b/slint-based/Cargo.toml index 2f8a3b4..23cc17e 100644 --- a/slint-based/Cargo.toml +++ b/slint-based/Cargo.toml @@ -15,6 +15,15 @@ build = "build.rs" [[bin]] name = "starter" +[[bin]] +name = "timer" + +[[bin]] +name = "light-control" + +[[bin]] +name = "microwave-ui" + [dependencies] slint = { version = "1.8", default-features = false, features = ["compat-1-2", "renderer-software", "libm", "unsafe-single-threaded"] } @@ -42,7 +51,7 @@ embedded-graphics-profiler-display = { version = "0.1.0", path = "../embedded-gr xpt2046 = { git = "https://github.com/Yandrik/xpt2046.git", version = "0.3.1" } -esp-backtrace = { version = "0.14.2", features = ["esp32", "println"] } +esp-backtrace = { version = "0.14.2", features = ["esp32", "println", "panic-handler", "exception-handler"] } static_cell = { version = "2.1.0", features = ["nightly"] } embedded-hal-bus = "0.2.0" diff --git a/slint-based/build.rs b/slint-based/build.rs index a15234b..1f0828e 100644 --- a/slint-based/build.rs +++ b/slint-based/build.rs @@ -5,4 +5,22 @@ fn main() { .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer), ) .unwrap(); + slint_build::compile_with_config( + "ui/timer-app.slint", + slint_build::CompilerConfiguration::new() + .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer), + ) + .unwrap(); + slint_build::compile_with_config( + "ui/lights-app.slint", + slint_build::CompilerConfiguration::new() + .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer), + ) + .unwrap(); + slint_build::compile_with_config( + "ui/microwave-ui.slint", + slint_build::CompilerConfiguration::new() + .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer), + ) + .unwrap(); } diff --git a/slint-based/img/pause.png b/slint-based/img/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..cb4e12c47eccefb1fe3c8af1b32373ea369fb5e6 GIT binary patch literal 373 zcmV-*0gC>KP)6L;3>4%6If}j7tqeyc@RNfB5NF02~J=U-|kE@ zGq2i8Qu3!#^6yV1;MWN%0VO~>@OM|J3d8!S)(hi~IA5b~FZwJ#Lt7+)8qj-G(@6b< zeg}1l=2(30Yl{dNf?@IIl^tMi70t4ETEG|#laytL-)+R$!~br27G`HrpT*Mx!pDb? z2+#CM%!AmwF9F^TTl`AEa>%={1iT%#_?3X=kau4Rcsp$ID*?+P@4gc7cG%)q0+vJG zeI?-Su*I(gEQh@NO2FG;i(d(_izyaQColrTJeMVV7}ki(=$OUR0_tE0%iV?{;*5S9 zb&cj&JS`v)SnKfvW;Uo#*XtHOBcjN5`pw((-7yLHuHV_WDFG!wI-n~=8c4(M8^Q1~ Tavzw200000NkvXXu0mjf0BNPI literal 0 HcmV?d00001 diff --git a/slint-based/img/play.png b/slint-based/img/play.png new file mode 100644 index 0000000000000000000000000000000000000000..a4dea0380aa908212b1e53b6b1683d868a479c96 GIT binary patch literal 401 zcmV;C0dD?@P)5@cUslq{yzDon5lL&9v=CS%hpn?VhvH3B&}gfVEq7QFT|3S!{sOc0#F3jDwYl%U;fWMkmy z%oAKe5{`^X*CPyrIn>lM890SvSQ(5XbPTIzi_RI#(6Kd8k=LLSMUhj8>ima(Ox0*- zuncN6-Bs#p>qc8dM9rik>Z;J{cOaCjVj7`O(6xOBYNn^&0{V&Fj%kg_>+oWPZ~ch% v`^W1=Q$bts9aLQNhWcdMEWA56@(lg}nR7%LRB25600000NkvXXu0mjf@&c>& literal 0 HcmV?d00001 diff --git a/slint-based/img/restart.png b/slint-based/img/restart.png new file mode 100644 index 0000000000000000000000000000000000000000..3f977e5adb1daea29d40b4bc9e4d61544cd9ae9a GIT binary patch literal 1362 zcmV-Y1+DstP)ZF$`LIpv&bhlsnI}UgB4g(iCC6} z17##?f|}xhC^({0DW#^(Vc+ku_GQ2G&N=tobMJZYz6Brq3~TSTzPUbxNghot1y8JSrTs^4C&!S}6!Ll|uuB`-S_2tMWK_Nbgq(Ukcv|{}e;0 z7y=z+c(yQFXqM863##{#P$is9(?%KsZDnYt@Mwg=je7o*@QZLtxKg-5=qy|xH8iWN%WAsKj0xGdnvR^5Lfd?K8Vaguudg@MAoLf3#U#D@!;l8#1F0+*`6cLk=4 zFZA{dVOvsz0lmJue_fz-!Z-1l@J5n0lMv{rhCUZ=@fzH#Yo^e;Bt{bG43W;uLKjc> zV_i=Wevhl4kH8hG{DaWRYlM&PLE*0ijuoQPLdwg8L0+Y=b$yp`F{+_B0+(11vqU1S z=hKA6g?Lq)S6%`P z*ILv1LnW)wnR+0A$)Un$X8Wv4;w#FWgpB~K`rYlu)OszqH@O6qQu#LZx zjUTC7-JqJcTkYJ3?2iBk61VD%*Y*3UO=VN@Q#I=%0uDXg*BAk&&BJDUlFB!;@gsGs zgft`$kWd0l8BSNobb*snISFfjKXd3-m0N-%@SGkzZ3=|Z^UKM5`EppOG@pox*t2R( zp>eu*^PZCLpq%7yIYSN#q5L;Ufb-}9Q}|hB`!e#rw|v=Y{_IxS%lQ@zB}i)!EFm`u zM1tiYgb$ntkmrSGq6n~Evuz`M)b3c9HHmx7z?uj{{`}8paV)FMfzqYifrbXpX2hUpYcv=DmzbRH=P(tVmb}!e2h$k&fG|sw;#MAf@4! z&=a~Q{y3i@i8umeLdaqx{|S|XiC?lPw_AL>U`R^o0| z@INSmDDt3LNYn`Up#xd&ZbGfhbzEl751u&E^%t^OR>KG^k}W@+r$a;YuyF)hsuJ-A z%n{h%DYy-#RHG3jz>${Rs}q7sg3D0Jm)s>ML~_^d%KT9w;45W*)F*IIZSyn43DcMe zlrSO{0VZ3=^hxZh=WkfQFh2{2SnB1VRFUUq!_VXOCA4Y$Ug17ec_JK+<0FbDCpSiKIcvB})QL@z2kB52Y5yA86pxj(b7|hQbj2gS~|7wOJ?GjEIWH2 zGqZ6vIhonHXLf$yoU^lg@7!vo4Ki*eFW=x_! z2wQ`_d8Ssd6uJb^sDau?S#tpQaK2>P&QXr1CSV>whXQH`^;L5&;n_srqtpbDhtSIc zY!`KYIhqn}6uIaLPq$zxWSLwMl9hs13cyDZz(JWFa}H?HnYZ18N5Bs3il}GyqeOt9 z?g}K+%ih?&nG`ev2cTNWAuAe*U;^A_R6Yw>mn%<_%V^$%Op3UGpfS%J3KoGL0P*LT zdl+98vW{&u4!~w$LUm!)1VP4y;bxu&6Zu(b) gIP?BJYUv~J1tEgHG3)nj-2eap07*qoM6N<$f@N;)PXGV_ literal 0 HcmV?d00001 diff --git a/slint-based/src/bin/light-control.rs b/slint-based/src/bin/light-control.rs new file mode 100644 index 0000000..8e2ded3 --- /dev/null +++ b/slint-based/src/bin/light-control.rs @@ -0,0 +1,347 @@ +#![no_std] +#![no_main] +extern crate alloc; + +use alloc::boxed::Box; +use alloc::rc::Rc; +use core::mem::MaybeUninit; +use core::{cell::RefCell, cmp::min, fmt}; +use display_interface_spi::SPIInterface; +use embassy_embedded_hal::shared_bus::blocking::spi::SpiDevice; +use embassy_executor::Spawner; +use embassy_sync::{ + blocking_mutex::{raw::NoopRawMutex, NoopMutex}, + signal::Signal, +}; +use embassy_time::Delay; +use embassy_time::{Duration, Instant, Timer}; +use embedded_graphics_core::pixelcolor::Rgb565; +use embedded_graphics_core::prelude::*; +use embedded_graphics_core::primitives::Rectangle; +use embedded_graphics_profiler_display::ProfilerDisplay; +use embedded_hal::digital::OutputPin; +use embedded_hal_bus::spi::ExclusiveDevice; +use esp_backtrace as _; +use esp_hal::interrupt::Priority; +use esp_hal::{self, prelude::*}; +use esp_hal::{ + clock::ClockControl, + gpio::{GpioPin, Input, Io, Level, Output, Pull, NO_PIN}, + peripherals::{Peripherals, SPI2, SPI3}, + prelude::*, + rtc_cntl::Rtc, + spi::{master::Spi, FullDuplexMode, SpiMode}, + system::SystemControl, + timer::timg::TimerGroup, +}; +use esp_hal_embassy::InterruptExecutor; +use esp_println::println; +use mipidsi::{ + models::ILI9486Rgb565, + options::{ColorInversion, ColorOrder, Orientation, Rotation}, + Builder, Display, +}; +use slint::platform::software_renderer::MinimalSoftwareWindow; +use slint::platform::{Platform, PointerEventButton, WindowEvent}; +use slint::private_unstable_api::re_exports::LogicalPoint; +use slint::LogicalPosition; +use static_cell::StaticCell; +use xpt2046::Xpt2046; + +use esp_alloc as _; + +// slint::slint!{ export MyUI := Window {} } +/* +slint::include_modules!(); +# */ + +slint::include_modules!(); + +fn init_heap() { + const HEAP_SIZE: usize = 64 * 1024; + static mut HEAP: MaybeUninit<[u8; HEAP_SIZE]> = MaybeUninit::uninit(); + + unsafe { + esp_alloc::HEAP.add_region(esp_alloc::HeapRegion::new( + HEAP.as_mut_ptr() as *mut u8, + HEAP_SIZE, + esp_alloc::MemoryCapability::Internal.into(), + )); + } +} + +#[embassy_executor::task] +async fn touch_task( + touch_irq: GpioPin<36>, + spi: ExclusiveDevice< + Spi<'static, SPI3, FullDuplexMode>, + Output<'static, GpioPin<33>>, + &'static mut Delay, + >, + touch_signal: &'static Signal>, +) -> ! { + let mut touch_driver = + Xpt2046::new(spi, Input::new(touch_irq, Pull::Up), xpt2046::Orientation::LandscapeFlipped); + touch_driver.set_num_samples(16); + touch_driver.init(&mut embassy_time::Delay).unwrap(); + + esp_println::println!("touch task"); + + loop { + touch_driver.run().expect("Running Touch driver failed"); + if touch_driver.is_touched() { + let point = touch_driver.get_touch_point(); + touch_signal.signal(Some(Point::new(point.x + 25, 240 - point.y))); + } else { + touch_signal.signal(None); + } + Timer::after(Duration::from_millis(1)).await; // 100 a second + + // Your touch handling logic here + } +} + +fn point_to_logical_pos(point: Point) -> LogicalPosition { + LogicalPosition::new(point.x as f32, point.y as f32) +} + +struct CYDPlatform { + window: Rc, +} + +impl Platform for CYDPlatform { + fn create_window_adapter( + &self, + ) -> Result, slint::PlatformError> { + Ok(self.window.clone()) + } + + fn duration_since_start(&self) -> core::time::Duration { + embassy_time::Instant::from_millis(0).elapsed().into() + } + + //noinspection DuplicatedCode + fn run_event_loop(&self) -> Result<(), slint::PlatformError> { + todo!(); + } +} + +#[main] +async fn main(spawner: Spawner) { + init_heap(); + let peripherals = Peripherals::take(); + let system = SystemControl::new(peripherals.SYSTEM); + let mut clocks = ClockControl::boot_defaults(system.clock_control).freeze(); + + let mut rtc = Rtc::new(peripherals.LPWR); + rtc.rwdt.disable(); + let mut timer_group0 = TimerGroup::new(peripherals.TIMG0, &clocks); + timer_group0.wdt.disable(); + let mut timer_group1 = TimerGroup::new(peripherals.TIMG1, &clocks); + timer_group1.wdt.disable(); + + esp_hal_embassy::init(&clocks, timer_group0.timer0); + + let io = Io::new(peripherals.GPIO, peripherals.IO_MUX); + + let touch_irq = io.pins.gpio36; + let touch_mosi = io.pins.gpio32; + let touch_miso = io.pins.gpio39; + let touch_clk = io.pins.gpio25; + let touch_cs = io.pins.gpio33; + + // 2MHz is the MAX! DO NOT DECREASE! This is really important. + let mut touch_spi = Spi::new(peripherals.SPI3, 2.MHz(), SpiMode::Mode0, &mut clocks).with_pins( + Some(touch_clk), + Some(touch_mosi), + Some(touch_miso), + NO_PIN, + ); + + static TOUCH_DELAY_STATICCELL: StaticCell = StaticCell::new(); + let mut delay = TOUCH_DELAY_STATICCELL.init(Delay); + + let touch_spi = + ExclusiveDevice::new(touch_spi, Output::new(touch_cs, Level::Low), delay).unwrap(); + + let touch_signal = Signal::new(); + static TOUCH_SIGNAL: StaticCell>> = StaticCell::new(); + let touch_signal = &*TOUCH_SIGNAL.init(touch_signal); + + // let sw_int = system.software_interrupt_control.software_interrupt2; + + // static EXECUTOR: StaticCell> = StaticCell::new(); + // let executor = InterruptExecutor::<2>::new(sw_int); + // let executor = EXECUTOR.init(executor); + + spawner.spawn(touch_task(touch_irq, touch_spi, touch_signal)).unwrap(); + + // executor.start(Priority::Priority1); + + // Display setup + let sclk = io.pins.gpio14; + let miso = io.pins.gpio12; + let mosi = io.pins.gpio13; + let cs = io.pins.gpio15; + let dc = io.pins.gpio2; + let mut backlight = Output::new(io.pins.gpio21, Level::Low); + + let mut spi = Spi::new(peripherals.SPI2, 10u32.MHz(), SpiMode::Mode0, &clocks).with_pins( + Some(sclk), + Some(mosi), + Some(miso), + esp_hal::gpio::NO_PIN, + ); + + // static DISP_SPI_BUS: StaticCell>>> = StaticCell::new(); + // let spi_bus = NoopMutex::new(RefCell::new(spi)); + // let spi_bus = DISP_SPI_BUS.init(spi_bus); + + static spi_delay_staticcell: StaticCell = StaticCell::new(); + let mut delay = spi_delay_staticcell.init(Delay); + let spi = ExclusiveDevice::new(spi, Output::new(cs, Level::Low), delay).unwrap(); + + let di = SPIInterface::new(spi, Output::new(dc, Level::Low)); + + let display = Builder::new(ILI9486Rgb565, di) + .orientation(Orientation { rotation: Rotation::Deg90, mirrored: true }) + .color_order(ColorOrder::Bgr) + .invert_colors(ColorInversion::Inverted) + .init(&mut embassy_time::Delay) + .unwrap(); + + let mut display = ProfilerDisplay::new(display); + + backlight.set_high(); + + let size = display.bounding_box().size; + let size = slint::PhysicalSize::new(size.width as u32, size.height as u32); + + let window = MinimalSoftwareWindow::new(Default::default()); + slint::platform::set_platform(Box::new(CYDPlatform { window: window.clone() })).unwrap(); + + let ui = create_slint_app(); + + window.set_size(slint::PhysicalSize::new(320, 240)); + + let mut buffer_provider = DrawBuffer { + display, + buffer: &mut [slint::platform::software_renderer::Rgb565Pixel(0); 320], + }; + + let mut last_touch = None; + + loop { + let start_time = Instant::now(); + if let Some(touch) = touch_signal.try_take() { + // println!("touch: {:?}, last_touch: {:?}", touch, last_touch); + let button = PointerEventButton::Left; + let interact = match (touch, last_touch) { + (Some(point), Some(_)) => { + Some(WindowEvent::PointerMoved { position: point_to_logical_pos(point) }) + } + (Some(point), None) => Some(WindowEvent::PointerPressed { + position: point_to_logical_pos(point), + button, + }), + (None, Some(point)) => Some(WindowEvent::PointerReleased { + position: point_to_logical_pos(point), + button, + }), + (None, None) => None, + }; + if let Some(event) = interact { + // println!("event: {:?}", event); + window.dispatch_event(event); + } + + last_touch = touch; + } + + slint::platform::update_timers_and_animations(); + + // let window = window.clone(); + let start_draw_time = Instant::now(); + window.draw_if_needed(|renderer| { + renderer.render_by_line(&mut buffer_provider); + }); + + let button = PointerEventButton::Left; + + if window.has_active_animations() { + continue; + } + + let display = &mut buffer_provider.display; + + let end_time = embassy_time::Instant::now(); + let draw_time = display.get_time(); + let prep_time = start_draw_time - start_time; + let proc_time = end_time - start_draw_time; + let proc_time = proc_time - min(draw_time, proc_time); + rtc.rwdt.feed(); + + if draw_time.as_micros() > 0 { + println!( + "draw time: {}.{:03}ms | prep time: {}.{:03}ms | proc time: {}.{:03}ms | total time: {}.{:03}ms", + draw_time.as_millis(), + draw_time.as_micros() % 100, + prep_time.as_millis(), + prep_time.as_micros() % 100, + proc_time.as_millis(), + proc_time.as_micros() % 100, + (draw_time + prep_time + proc_time).as_millis(), + (draw_time + prep_time + proc_time).as_micros() % 100, ); + } + display.reset_time(); + Timer::after(Duration::from_millis(1)).await; // 60 a second + } +} + +fn create_slint_app() -> LightsApp { + let ui = LightsApp::new().unwrap(); + ui +} + +struct DrawBuffer<'a, DT> { + display: DT, + buffer: &'a mut [slint::platform::software_renderer::Rgb565Pixel], +} + +impl< + // DI: display_interface_spi::WriteOnlyDataCommand, + E: core::fmt::Debug, + DT: DrawTarget, + // RST: OutputPin, + > slint::platform::software_renderer::LineBufferProvider for &mut DrawBuffer<'_, DT> +{ + type TargetPixel = slint::platform::software_renderer::Rgb565Pixel; + + fn process_line( + &mut self, + line: usize, + range: core::ops::Range, + render_fn: impl FnOnce(&mut [slint::platform::software_renderer::Rgb565Pixel]), + ) { + let buffer = &mut self.buffer[range.clone()]; + + render_fn(buffer); + + // We send empty data just to get the device in the right window + self.display + .fill_contiguous( + &Rectangle::new( + Point::new(range.start as i32, line as i32), + Size::new((range.end - range.start) as u32, 1), + ), + // range.start as u16, + // line as _, + // range.end as u16, + // line as u16, + buffer + .iter() + .map(|x| embedded_graphics_core::pixelcolor::raw::RawU16::new(x.0).into()), + ) + .unwrap(); + } +} diff --git a/slint-based/src/bin/microwave-ui.rs b/slint-based/src/bin/microwave-ui.rs new file mode 100644 index 0000000..c750ff2 --- /dev/null +++ b/slint-based/src/bin/microwave-ui.rs @@ -0,0 +1,490 @@ +#![no_std] +#![no_main] +extern crate alloc; + +use alloc::boxed::Box; +use alloc::rc::Rc; +use alloc::sync::Arc; +use core::mem::MaybeUninit; +use core::{cell::RefCell, cmp::min, fmt}; +use display_interface_spi::SPIInterface; +use embassy_embedded_hal::shared_bus::blocking::spi::SpiDevice; +use embassy_executor::Spawner; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::{ + blocking_mutex::{raw::NoopRawMutex, NoopMutex}, + signal::Signal, +}; +use embassy_time::Delay; +use embassy_time::{Duration, Instant, Timer}; +use embedded_graphics_core::pixelcolor::Rgb565; +use embedded_graphics_core::prelude::*; +use embedded_graphics_core::primitives::Rectangle; +use embedded_graphics_profiler_display::ProfilerDisplay; +use embedded_hal::digital::OutputPin; +use embedded_hal_bus::spi::ExclusiveDevice; +use esp_backtrace as _; +use esp_hal::interrupt::Priority; +use esp_hal::{self, prelude::*}; +use esp_hal::{ + clock::ClockControl, + gpio::{GpioPin, Input, Io, Level, Output, Pull, NO_PIN}, + peripherals::{Peripherals, SPI2, SPI3}, + prelude::*, + rtc_cntl::Rtc, + spi::{master::Spi, FullDuplexMode, SpiMode}, + system::SystemControl, + timer::timg::TimerGroup, +}; +use esp_hal_embassy::InterruptExecutor; +use esp_println::println; +use mipidsi::{ + models::ILI9486Rgb565, + options::{ColorInversion, ColorOrder, Orientation, Rotation}, + Builder, Display, +}; +use slint::platform::software_renderer::MinimalSoftwareWindow; +use slint::platform::{Platform, PointerEventButton, WindowEvent}; +use slint::private_unstable_api::re_exports::LogicalPoint; +use slint::{format, LogicalPosition}; +use static_cell::StaticCell; +use xpt2046::Xpt2046; + +use esp_alloc as _; + +// slint::slint!{ export MyUI := Window {} } +/* +slint::include_modules!(); +# */ + +slint::include_modules!(); + +fn init_heap() { + const HEAP_SIZE: usize = 64 * 1024; + static mut HEAP: MaybeUninit<[u8; HEAP_SIZE]> = MaybeUninit::uninit(); + + unsafe { + esp_alloc::HEAP.add_region(esp_alloc::HeapRegion::new( + HEAP.as_mut_ptr() as *mut u8, + HEAP_SIZE, + esp_alloc::MemoryCapability::Internal.into(), + )); + } +} + +#[embassy_executor::task] +async fn touch_task( + touch_irq: GpioPin<36>, + spi: ExclusiveDevice< + Spi<'static, SPI3, FullDuplexMode>, + Output<'static, GpioPin<33>>, + &'static mut Delay, + >, + touch_signal: &'static Signal>, +) -> ! { + let mut touch_driver = + Xpt2046::new(spi, Input::new(touch_irq, Pull::Up), xpt2046::Orientation::LandscapeFlipped); + touch_driver.set_num_samples(1); + touch_driver.init(&mut embassy_time::Delay).unwrap(); + + esp_println::println!("touch task"); + + loop { + touch_driver.run().expect("Running Touch driver failed"); + if touch_driver.is_touched() { + let point = touch_driver.get_touch_point(); + touch_signal.signal(Some(Point::new(point.x + 25, 240 - point.y))); + } else { + touch_signal.signal(None); + } + Timer::after(Duration::from_millis(1)).await; // 100 a second + + // Your touch handling logic here + } +} + +fn point_to_logical_pos(point: Point) -> LogicalPosition { + LogicalPosition::new(point.x as f32, point.y as f32) +} + +struct CYDPlatform { + window: Rc, +} + +impl Platform for CYDPlatform { + fn create_window_adapter( + &self, + ) -> Result, slint::PlatformError> { + Ok(self.window.clone()) + } + + fn duration_since_start(&self) -> core::time::Duration { + embassy_time::Instant::from_millis(0).elapsed().into() + } + + //noinspection DuplicatedCode + fn run_event_loop(&self) -> Result<(), slint::PlatformError> { + todo!(); + } +} + +struct AppData { + timer_start: Instant, + timer_set_duration: Duration, + timer_remaining_duration: Duration, + timer_running: bool, + timer_paused: bool, +} + +impl AppData { + fn new() -> Self { + Self { + timer_start: Instant::now(), + timer_set_duration: Duration::from_secs(10), + timer_remaining_duration: Duration::from_secs(10), + timer_running: false, + timer_paused: false, + } + } + + fn set_timer_duration(&mut self, duration: Duration) { + self.timer_set_duration = duration; + } + + fn add_secs(&mut self, secs: u64) { + self.set_timer_duration( + self.timer_set_duration + .checked_add(Duration::from_secs(secs)) + .unwrap_or(Duration::from_secs(5999)) + .min(Duration::from_secs(5999)), + ); + } + + fn sub_secs(&mut self, secs: u64) { + self.set_timer_duration( + self.timer_set_duration + .checked_sub(Duration::from_secs(secs)) + .unwrap_or(Duration::from_secs(10)) + .max(Duration::from_secs(10)), + ); + } + + fn start_timer(&mut self) { + if !self.timer_paused { + self.timer_remaining_duration = self.timer_set_duration; + } + + self.timer_start = Instant::now(); + self.timer_running = true; + self.timer_paused = false; + } + + fn pause_timer(&mut self) { + self.timer_paused = true; + self.timer_remaining_duration = self + .timer_remaining_duration + .checked_sub(self.timer_start.elapsed()) + .unwrap_or(Duration::from_secs(0)); + } + + fn reset_timer(&mut self) { + self.timer_start = Instant::now(); + self.timer_paused = false; + self.timer_running = false; + self.timer_remaining_duration = self.timer_set_duration; + } + + fn remaining(&self) -> Duration { + if self.timer_stopped() { + self.timer_set_duration + } else if self.timer_paused() { + self.timer_remaining_duration + } else { + self.timer_remaining_duration + .checked_sub(self.timer_start.elapsed()) + .unwrap_or(Duration::from_secs(0)) + } + } + + fn timer_stopped(&self) -> bool { + !self.timer_running + } + + fn timer_paused(&self) -> bool { + self.timer_running && self.timer_paused + } + + fn timer_running(&self) -> bool { + self.timer_running && !self.timer_paused + } + + fn timer_finished(&self) -> bool { + self.timer_running && self.remaining() == Duration::from_secs(0) + } +} + +#[main] +async fn main(spawner: Spawner) { + init_heap(); + let peripherals = Peripherals::take(); + let system = SystemControl::new(peripherals.SYSTEM); + let mut clocks = ClockControl::boot_defaults(system.clock_control).freeze(); + + let mut rtc = Rtc::new(peripherals.LPWR); + rtc.rwdt.disable(); + let mut timer_group0 = TimerGroup::new(peripherals.TIMG0, &clocks); + timer_group0.wdt.disable(); + let mut timer_group1 = TimerGroup::new(peripherals.TIMG1, &clocks); + timer_group1.wdt.disable(); + + esp_hal_embassy::init(&clocks, timer_group0.timer0); + + let io = Io::new(peripherals.GPIO, peripherals.IO_MUX); + + let touch_irq = io.pins.gpio36; + let touch_mosi = io.pins.gpio32; + let touch_miso = io.pins.gpio39; + let touch_clk = io.pins.gpio25; + let touch_cs = io.pins.gpio33; + + // 2MHz is the MAX! DO NOT DECREASE! This is really important. + let mut touch_spi = Spi::new(peripherals.SPI3, 2.MHz(), SpiMode::Mode0, &mut clocks).with_pins( + Some(touch_clk), + Some(touch_mosi), + Some(touch_miso), + NO_PIN, + ); + + static TOUCH_DELAY_STATICCELL: StaticCell = StaticCell::new(); + let mut delay = TOUCH_DELAY_STATICCELL.init(Delay); + + let touch_spi = + ExclusiveDevice::new(touch_spi, Output::new(touch_cs, Level::Low), delay).unwrap(); + + let touch_signal = Signal::new(); + static TOUCH_SIGNAL: StaticCell>> = + StaticCell::new(); + let touch_signal = &*TOUCH_SIGNAL.init(touch_signal); + + // let sw_int = system.software_interrupt_control.software_interrupt2; + + // static EXECUTOR: StaticCell> = StaticCell::new(); + // let executor = InterruptExecutor::<2>::new(sw_int); + // let executor = EXECUTOR.init(executor); + + // static EXECUTOR: StaticCell> = StaticCell::new(); + // let executor = + // InterruptExecutor::<2>::new(system.software_interrupt_control.software_interrupt2); + // let mut executor = EXECUTOR.init(executor); + + // executor + // .start(Priority::Priority2) + spawner.spawn(touch_task(touch_irq, touch_spi, touch_signal)).unwrap(); + + // Display setup + let sclk = io.pins.gpio14; + let miso = io.pins.gpio12; + let mosi = io.pins.gpio13; + let cs = io.pins.gpio15; + let dc = io.pins.gpio2; + let mut backlight = Output::new(io.pins.gpio21, Level::Low); + + let mut spi = Spi::new(peripherals.SPI2, 10u32.MHz(), SpiMode::Mode0, &clocks).with_pins( + Some(sclk), + Some(mosi), + Some(miso), + esp_hal::gpio::NO_PIN, + ); + + // static DISP_SPI_BUS: StaticCell>>> = StaticCell::new(); + // let spi_bus = NoopMutex::new(RefCell::new(spi)); + // let spi_bus = DISP_SPI_BUS.init(spi_bus); + + static SPI_DELAY_STATICCELL: StaticCell = StaticCell::new(); + let mut delay = SPI_DELAY_STATICCELL.init(Delay); + let spi = ExclusiveDevice::new(spi, Output::new(cs, Level::Low), delay).unwrap(); + + let di = SPIInterface::new(spi, Output::new(dc, Level::Low)); + + let display = Builder::new(ILI9486Rgb565, di) + .orientation(Orientation { rotation: Rotation::Deg90, mirrored: true }) + .color_order(ColorOrder::Bgr) + .invert_colors(ColorInversion::Inverted) + .init(&mut embassy_time::Delay) + .unwrap(); + + let mut display = ProfilerDisplay::new(display); + + backlight.set_high(); + + let size = display.bounding_box().size; + let size = slint::PhysicalSize::new(size.width as u32, size.height as u32); + + let window = MinimalSoftwareWindow::new(Default::default()); + slint::platform::set_platform(Box::new(CYDPlatform { window: window.clone() })).unwrap(); + + let ui = create_slint_app(); + + window.set_size(slint::PhysicalSize::new(320, 240)); + + let mut buffer_provider = DrawBuffer { + display, + buffer: &mut [slint::platform::software_renderer::Rgb565Pixel(0); 320], + }; + + let mut last_touch = None; + + let mut appdata = Rc::new(RefCell::new(AppData::new())); + + let mut cl_appdata = appdata.clone(); + ui.on_add_10s(move || cl_appdata.borrow_mut().add_secs(10)); + let mut cl_appdata = appdata.clone(); + ui.on_sub_10s(move || cl_appdata.borrow_mut().sub_secs(10)); + let mut cl_appdata = appdata.clone(); + ui.on_start_timer(move || cl_appdata.borrow_mut().start_timer()); + let mut cl_appdata = appdata.clone(); + ui.on_stop_timer(move || cl_appdata.borrow_mut().pause_timer()); + let mut cl_appdata = appdata.clone(); + ui.on_reset_timer(move || cl_appdata.borrow_mut().reset_timer()); + + loop { + let start_time = Instant::now(); + if let Some(touch) = touch_signal.try_take() { + // println!("touch: {:?}, last_touch: {:?}", touch, last_touch); + let button = PointerEventButton::Left; + let interact = match (touch, last_touch) { + (Some(point), Some(_)) => { + Some(WindowEvent::PointerMoved { position: point_to_logical_pos(point) }) + } + (Some(point), None) => Some(WindowEvent::PointerPressed { + position: point_to_logical_pos(point), + button, + }), + (None, Some(point)) => Some(WindowEvent::PointerReleased { + position: point_to_logical_pos(point), + button, + }), + (None, None) => None, + }; + if let Some(event) = interact { + // println!("event: {:?}", event); + window.dispatch_event(event); + } + + last_touch = touch; + } + + { + let appdata = appdata.borrow(); + let remaining = appdata.remaining(); + ui.set_show_reset_timer(appdata.timer_finished()); + ui.set_show_start_timer( + !appdata.timer_running() || appdata.timer_paused() && !appdata.timer_finished(), + ); + ui.set_show_stop_timer(appdata.timer_running() && !appdata.timer_finished()); + + ui.set_timer_text(format!( + "{:02}:{:02}:{:03}", + remaining.as_secs() / 60, + remaining.as_secs() % 60, + remaining.as_millis() % 1000 + )); + } + + slint::platform::update_timers_and_animations(); + + // let window = window.clone(); + let start_draw_time = Instant::now(); + window.draw_if_needed(|renderer| { + // println!("dirty reg: {:?}", renderer.render_by_line(&mut buffer_provider)); + renderer.render_by_line(&mut buffer_provider); + }); + + let button = PointerEventButton::Left; + + if window.has_active_animations() { + continue; + } + + let display = &mut buffer_provider.display; + + let end_time = embassy_time::Instant::now(); + let draw_time = display.get_time(); + let prep_time = start_draw_time - start_time; + let proc_time = end_time - start_draw_time; + let proc_time = proc_time - min(draw_time, proc_time); + rtc.rwdt.feed(); + + if draw_time.as_micros() > 0 { + println!( + "draw time: {}.{:03}ms | prep time: {}.{:03}ms | proc time: {}.{:03}ms | total time: {}.{:03}ms", + draw_time.as_millis(), + draw_time.as_micros() % 100, + prep_time.as_millis(), + prep_time.as_micros() % 100, + proc_time.as_millis(), + proc_time.as_micros() % 100, + (draw_time + prep_time + proc_time).as_millis(), + (draw_time + prep_time + proc_time).as_micros() % 100, ); + } + display.reset_time(); + Timer::after(Duration::from_millis(1)).await; // 60 a second + } +} + +fn create_slint_app() -> MicrowaveUI { + let ui = MicrowaveUI::new().unwrap(); + + /* + let ui_handle = ui.as_weak(); + ui.on_request_increase_value(move || { + let ui = ui_handle.unwrap(); + ui.set_counter(ui.get_counter() + 1); + }); + */ + + ui +} + +struct DrawBuffer<'a, DT> { + display: DT, + buffer: &'a mut [slint::platform::software_renderer::Rgb565Pixel], +} + +impl< + // DI: display_interface_spi::WriteOnlyDataCommand, + E: core::fmt::Debug, + DT: DrawTarget, + // RST: OutputPin, + > slint::platform::software_renderer::LineBufferProvider for &mut DrawBuffer<'_, DT> +{ + type TargetPixel = slint::platform::software_renderer::Rgb565Pixel; + + fn process_line( + &mut self, + line: usize, + range: core::ops::Range, + render_fn: impl FnOnce(&mut [slint::platform::software_renderer::Rgb565Pixel]), + ) { + let buffer = &mut self.buffer[range.clone()]; + + render_fn(buffer); + + // We send empty data just to get the device in the right window + self.display + .fill_contiguous( + &Rectangle::new( + Point::new(range.start as i32, line as i32), + Size::new((range.end - range.start) as u32, 1), + ), + // range.start as u16, + // line as _, + // range.end as u16, + // line as u16, + buffer + .iter() + .map(|x| embedded_graphics_core::pixelcolor::raw::RawU16::new(x.0).into()), + ) + .unwrap(); + } +} diff --git a/slint-based/src/bin/timer.rs b/slint-based/src/bin/timer.rs new file mode 100644 index 0000000..89d49d4 --- /dev/null +++ b/slint-based/src/bin/timer.rs @@ -0,0 +1,490 @@ +#![no_std] +#![no_main] +extern crate alloc; + +use alloc::boxed::Box; +use alloc::rc::Rc; +use alloc::sync::Arc; +use core::mem::MaybeUninit; +use core::{cell::RefCell, cmp::min, fmt}; +use display_interface_spi::SPIInterface; +use embassy_embedded_hal::shared_bus::blocking::spi::SpiDevice; +use embassy_executor::Spawner; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::{ + blocking_mutex::{raw::NoopRawMutex, NoopMutex}, + signal::Signal, +}; +use embassy_time::Delay; +use embassy_time::{Duration, Instant, Timer}; +use embedded_graphics_core::pixelcolor::Rgb565; +use embedded_graphics_core::prelude::*; +use embedded_graphics_core::primitives::Rectangle; +use embedded_graphics_profiler_display::ProfilerDisplay; +use embedded_hal::digital::OutputPin; +use embedded_hal_bus::spi::ExclusiveDevice; +use esp_backtrace as _; +use esp_hal::interrupt::Priority; +use esp_hal::{self, prelude::*}; +use esp_hal::{ + clock::ClockControl, + gpio::{GpioPin, Input, Io, Level, Output, Pull, NO_PIN}, + peripherals::{Peripherals, SPI2, SPI3}, + prelude::*, + rtc_cntl::Rtc, + spi::{master::Spi, FullDuplexMode, SpiMode}, + system::SystemControl, + timer::timg::TimerGroup, +}; +use esp_hal_embassy::InterruptExecutor; +use esp_println::println; +use mipidsi::{ + models::ILI9486Rgb565, + options::{ColorInversion, ColorOrder, Orientation, Rotation}, + Builder, Display, +}; +use slint::platform::software_renderer::MinimalSoftwareWindow; +use slint::platform::{Platform, PointerEventButton, WindowEvent}; +use slint::private_unstable_api::re_exports::LogicalPoint; +use slint::{format, LogicalPosition}; +use static_cell::StaticCell; +use xpt2046::Xpt2046; + +use esp_alloc as _; + +// slint::slint!{ export MyUI := Window {} } +/* +slint::include_modules!(); +# */ + +slint::include_modules!(); + +fn init_heap() { + const HEAP_SIZE: usize = 32 * 1024; + static mut HEAP: MaybeUninit<[u8; HEAP_SIZE]> = MaybeUninit::uninit(); + + unsafe { + esp_alloc::HEAP.add_region(esp_alloc::HeapRegion::new( + HEAP.as_mut_ptr() as *mut u8, + HEAP_SIZE, + esp_alloc::MemoryCapability::Internal.into(), + )); + } +} + +#[embassy_executor::task] +async fn touch_task( + touch_irq: GpioPin<36>, + spi: ExclusiveDevice< + Spi<'static, SPI3, FullDuplexMode>, + Output<'static, GpioPin<33>>, + &'static mut Delay, + >, + touch_signal: &'static Signal>, +) -> ! { + let mut touch_driver = + Xpt2046::new(spi, Input::new(touch_irq, Pull::Up), xpt2046::Orientation::LandscapeFlipped); + touch_driver.set_num_samples(1); + touch_driver.init(&mut embassy_time::Delay).unwrap(); + + esp_println::println!("touch task"); + + loop { + touch_driver.run().expect("Running Touch driver failed"); + if touch_driver.is_touched() { + let point = touch_driver.get_touch_point(); + touch_signal.signal(Some(Point::new(point.x + 25, 240 - point.y))); + } else { + touch_signal.signal(None); + } + Timer::after(Duration::from_millis(1)).await; // 100 a second + + // Your touch handling logic here + } +} + +fn point_to_logical_pos(point: Point) -> LogicalPosition { + LogicalPosition::new(point.x as f32, point.y as f32) +} + +struct CYDPlatform { + window: Rc, +} + +impl Platform for CYDPlatform { + fn create_window_adapter( + &self, + ) -> Result, slint::PlatformError> { + Ok(self.window.clone()) + } + + fn duration_since_start(&self) -> core::time::Duration { + embassy_time::Instant::from_millis(0).elapsed().into() + } + + //noinspection DuplicatedCode + fn run_event_loop(&self) -> Result<(), slint::PlatformError> { + todo!(); + } +} + +struct AppData { + timer_start: Instant, + timer_set_duration: Duration, + timer_remaining_duration: Duration, + timer_running: bool, + timer_paused: bool, +} + +impl AppData { + fn new() -> Self { + Self { + timer_start: Instant::now(), + timer_set_duration: Duration::from_secs(10), + timer_remaining_duration: Duration::from_secs(10), + timer_running: false, + timer_paused: false, + } + } + + fn set_timer_duration(&mut self, duration: Duration) { + self.timer_set_duration = duration; + } + + fn add_secs(&mut self, secs: u64) { + self.set_timer_duration( + self.timer_set_duration + .checked_add(Duration::from_secs(secs)) + .unwrap_or(Duration::from_secs(5999)) + .min(Duration::from_secs(5999)), + ); + } + + fn sub_secs(&mut self, secs: u64) { + self.set_timer_duration( + self.timer_set_duration + .checked_sub(Duration::from_secs(secs)) + .unwrap_or(Duration::from_secs(10)) + .max(Duration::from_secs(10)), + ); + } + + fn start_timer(&mut self) { + if !self.timer_paused { + self.timer_remaining_duration = self.timer_set_duration; + } + + self.timer_start = Instant::now(); + self.timer_running = true; + self.timer_paused = false; + } + + fn pause_timer(&mut self) { + self.timer_paused = true; + self.timer_remaining_duration = self + .timer_remaining_duration + .checked_sub(self.timer_start.elapsed()) + .unwrap_or(Duration::from_secs(0)); + } + + fn reset_timer(&mut self) { + self.timer_start = Instant::now(); + self.timer_paused = false; + self.timer_running = false; + self.timer_remaining_duration = self.timer_set_duration; + } + + fn remaining(&self) -> Duration { + if self.timer_stopped() { + self.timer_set_duration + } else if self.timer_paused() { + self.timer_remaining_duration + } else { + self.timer_remaining_duration + .checked_sub(self.timer_start.elapsed()) + .unwrap_or(Duration::from_secs(0)) + } + } + + fn timer_stopped(&self) -> bool { + !self.timer_running + } + + fn timer_paused(&self) -> bool { + self.timer_running && self.timer_paused + } + + fn timer_running(&self) -> bool { + self.timer_running && !self.timer_paused + } + + fn timer_finished(&self) -> bool { + self.timer_running && self.remaining() == Duration::from_secs(0) + } +} + +#[main] +async fn main(spawner: Spawner) { + init_heap(); + let peripherals = Peripherals::take(); + let system = SystemControl::new(peripherals.SYSTEM); + let mut clocks = ClockControl::boot_defaults(system.clock_control).freeze(); + + let mut rtc = Rtc::new(peripherals.LPWR); + rtc.rwdt.disable(); + let mut timer_group0 = TimerGroup::new(peripherals.TIMG0, &clocks); + timer_group0.wdt.disable(); + let mut timer_group1 = TimerGroup::new(peripherals.TIMG1, &clocks); + timer_group1.wdt.disable(); + + esp_hal_embassy::init(&clocks, timer_group0.timer0); + + let io = Io::new(peripherals.GPIO, peripherals.IO_MUX); + + let touch_irq = io.pins.gpio36; + let touch_mosi = io.pins.gpio32; + let touch_miso = io.pins.gpio39; + let touch_clk = io.pins.gpio25; + let touch_cs = io.pins.gpio33; + + // 2MHz is the MAX! DO NOT DECREASE! This is really important. + let mut touch_spi = Spi::new(peripherals.SPI3, 2.MHz(), SpiMode::Mode0, &mut clocks).with_pins( + Some(touch_clk), + Some(touch_mosi), + Some(touch_miso), + NO_PIN, + ); + + static TOUCH_DELAY_STATICCELL: StaticCell = StaticCell::new(); + let mut delay = TOUCH_DELAY_STATICCELL.init(Delay); + + let touch_spi = + ExclusiveDevice::new(touch_spi, Output::new(touch_cs, Level::Low), delay).unwrap(); + + let touch_signal = Signal::new(); + static TOUCH_SIGNAL: StaticCell>> = + StaticCell::new(); + let touch_signal = &*TOUCH_SIGNAL.init(touch_signal); + + // let sw_int = system.software_interrupt_control.software_interrupt2; + + // static EXECUTOR: StaticCell> = StaticCell::new(); + // let executor = InterruptExecutor::<2>::new(sw_int); + // let executor = EXECUTOR.init(executor); + + // static EXECUTOR: StaticCell> = StaticCell::new(); + // let executor = + // InterruptExecutor::<2>::new(system.software_interrupt_control.software_interrupt2); + // let mut executor = EXECUTOR.init(executor); + + // executor + // .start(Priority::Priority2) + spawner.spawn(touch_task(touch_irq, touch_spi, touch_signal)).unwrap(); + + // Display setup + let sclk = io.pins.gpio14; + let miso = io.pins.gpio12; + let mosi = io.pins.gpio13; + let cs = io.pins.gpio15; + let dc = io.pins.gpio2; + let mut backlight = Output::new(io.pins.gpio21, Level::Low); + + let mut spi = Spi::new(peripherals.SPI2, 10u32.MHz(), SpiMode::Mode0, &clocks).with_pins( + Some(sclk), + Some(mosi), + Some(miso), + esp_hal::gpio::NO_PIN, + ); + + // static DISP_SPI_BUS: StaticCell>>> = StaticCell::new(); + // let spi_bus = NoopMutex::new(RefCell::new(spi)); + // let spi_bus = DISP_SPI_BUS.init(spi_bus); + + static SPI_DELAY_STATICCELL: StaticCell = StaticCell::new(); + let mut delay = SPI_DELAY_STATICCELL.init(Delay); + let spi = ExclusiveDevice::new(spi, Output::new(cs, Level::Low), delay).unwrap(); + + let di = SPIInterface::new(spi, Output::new(dc, Level::Low)); + + let display = Builder::new(ILI9486Rgb565, di) + .orientation(Orientation { rotation: Rotation::Deg90, mirrored: true }) + .color_order(ColorOrder::Bgr) + .invert_colors(ColorInversion::Inverted) + .init(&mut embassy_time::Delay) + .unwrap(); + + let mut display = ProfilerDisplay::new(display); + + backlight.set_high(); + + let size = display.bounding_box().size; + let size = slint::PhysicalSize::new(size.width as u32, size.height as u32); + + let window = MinimalSoftwareWindow::new(Default::default()); + slint::platform::set_platform(Box::new(CYDPlatform { window: window.clone() })).unwrap(); + + let ui = create_slint_app(); + + window.set_size(slint::PhysicalSize::new(320, 240)); + + let mut buffer_provider = DrawBuffer { + display, + buffer: &mut [slint::platform::software_renderer::Rgb565Pixel(0); 320], + }; + + let mut last_touch = None; + + let mut appdata = Rc::new(RefCell::new(AppData::new())); + + let mut cl_appdata = appdata.clone(); + ui.on_add_10s(move || cl_appdata.borrow_mut().add_secs(10)); + let mut cl_appdata = appdata.clone(); + ui.on_sub_10s(move || cl_appdata.borrow_mut().sub_secs(10)); + let mut cl_appdata = appdata.clone(); + ui.on_start_timer(move || cl_appdata.borrow_mut().start_timer()); + let mut cl_appdata = appdata.clone(); + ui.on_stop_timer(move || cl_appdata.borrow_mut().pause_timer()); + let mut cl_appdata = appdata.clone(); + ui.on_reset_timer(move || cl_appdata.borrow_mut().reset_timer()); + + loop { + let start_time = Instant::now(); + if let Some(touch) = touch_signal.try_take() { + // println!("touch: {:?}, last_touch: {:?}", touch, last_touch); + let button = PointerEventButton::Left; + let interact = match (touch, last_touch) { + (Some(point), Some(_)) => { + Some(WindowEvent::PointerMoved { position: point_to_logical_pos(point) }) + } + (Some(point), None) => Some(WindowEvent::PointerPressed { + position: point_to_logical_pos(point), + button, + }), + (None, Some(point)) => Some(WindowEvent::PointerReleased { + position: point_to_logical_pos(point), + button, + }), + (None, None) => None, + }; + if let Some(event) = interact { + // println!("event: {:?}", event); + window.dispatch_event(event); + } + + last_touch = touch; + } + + { + let appdata = appdata.borrow(); + let remaining = appdata.remaining(); + ui.set_show_reset_timer(appdata.timer_finished()); + ui.set_show_start_timer( + !appdata.timer_running() || appdata.timer_paused() && !appdata.timer_finished(), + ); + ui.set_show_stop_timer(appdata.timer_running() && !appdata.timer_finished()); + + ui.set_timer_text(format!( + "{:02}:{:02}:{:03}", + remaining.as_secs() / 60, + remaining.as_secs() % 60, + remaining.as_millis() % 1000 + )); + } + + slint::platform::update_timers_and_animations(); + + // let window = window.clone(); + let start_draw_time = Instant::now(); + window.draw_if_needed(|renderer| { + // println!("dirty reg: {:?}", renderer.render_by_line(&mut buffer_provider)); + renderer.render_by_line(&mut buffer_provider); + }); + + let button = PointerEventButton::Left; + + if window.has_active_animations() { + continue; + } + + let display = &mut buffer_provider.display; + + let end_time = embassy_time::Instant::now(); + let draw_time = display.get_time(); + let prep_time = start_draw_time - start_time; + let proc_time = end_time - start_draw_time; + let proc_time = proc_time - min(draw_time, proc_time); + rtc.rwdt.feed(); + + if draw_time.as_micros() > 0 { + println!( + "draw time: {}.{:03}ms | prep time: {}.{:03}ms | proc time: {}.{:03}ms | total time: {}.{:03}ms", + draw_time.as_millis(), + draw_time.as_micros() % 100, + prep_time.as_millis(), + prep_time.as_micros() % 100, + proc_time.as_millis(), + proc_time.as_micros() % 100, + (draw_time + prep_time + proc_time).as_millis(), + (draw_time + prep_time + proc_time).as_micros() % 100, ); + } + display.reset_time(); + Timer::after(Duration::from_millis(1)).await; // 60 a second + } +} + +fn create_slint_app() -> TimerApp { + let ui = TimerApp::new().unwrap(); + + /* + let ui_handle = ui.as_weak(); + ui.on_request_increase_value(move || { + let ui = ui_handle.unwrap(); + ui.set_counter(ui.get_counter() + 1); + }); + */ + + ui +} + +struct DrawBuffer<'a, DT> { + display: DT, + buffer: &'a mut [slint::platform::software_renderer::Rgb565Pixel], +} + +impl< + // DI: display_interface_spi::WriteOnlyDataCommand, + E: core::fmt::Debug, + DT: DrawTarget, + // RST: OutputPin, + > slint::platform::software_renderer::LineBufferProvider for &mut DrawBuffer<'_, DT> +{ + type TargetPixel = slint::platform::software_renderer::Rgb565Pixel; + + fn process_line( + &mut self, + line: usize, + range: core::ops::Range, + render_fn: impl FnOnce(&mut [slint::platform::software_renderer::Rgb565Pixel]), + ) { + let buffer = &mut self.buffer[range.clone()]; + + render_fn(buffer); + + // We send empty data just to get the device in the right window + self.display + .fill_contiguous( + &Rectangle::new( + Point::new(range.start as i32, line as i32), + Size::new((range.end - range.start) as u32, 1), + ), + // range.start as u16, + // line as _, + // range.end as u16, + // line as u16, + buffer + .iter() + .map(|x| embedded_graphics_core::pixelcolor::raw::RawU16::new(x.0).into()), + ) + .unwrap(); + } +} diff --git a/slint-based/ui/app-window.slint b/slint-based/ui/app-window.slint index 2676e4b..064e6f7 100644 --- a/slint-based/ui/app-window.slint +++ b/slint-based/ui/app-window.slint @@ -5,20 +5,6 @@ export component AppWindow inherits Window { height: 240px; in-out property counter: 42; callback request-increase-value(); - - VerticalBox { - alignment: start; - Text { - text: "Hello World!"; - font-size: 24px; - horizontal-alignment: center; - } - } - - Text { - text: "Counter App (Slint)"; - } - VerticalBox { Text { text: "Counter: \{root.counter}"; @@ -31,6 +17,6 @@ export component AppWindow inherits Window { } } - // AboutSlint { } - } + AboutSlint { } + } } diff --git a/slint-based/ui/lights-app.slint b/slint-based/ui/lights-app.slint new file mode 100644 index 0000000..9fb8872 --- /dev/null +++ b/slint-based/ui/lights-app.slint @@ -0,0 +1,164 @@ +import { Button, VerticalBox , AboutSlint, Slider, Switch } from "std-widgets.slint"; + +export struct Light { + name: string, + brightness: int, + on: bool, +} + +export component LightsApp inherits Window { + width: 320px; + height: 240px; + + in property show-start-timer: true; + in property show-stop-timer: false; + in property show-reset-timer: false; + + property <[Light]> lights: [ + { + name: "Bathroom", + brightness: 100, + on: true, + }, + { + name: "Bedroom", + brightness: 200, + on: true, + }, + { + name: "Living Room", + brightness: 50, + on: false, + }, + { + name: "Front Door", + brightness: 250, + on: false, + }, + { + name: "Porch", + brightness: 1, + on: true, + }, + // {name: "Bathroom", brightness: 4, on: false, }, + ]; + + property selected-light: 0; + property show-light-page: false; + + in property timer-text: "00:00:000"; + + property start-timer-icon: @image-url("../img/play.png"); + property stop-timer-icon: @image-url("../img/pause.png"); + property reset-timer-icon: @image-url("../img/xmark-square.png"); + + property light-icon: @image-url("../img/play.png"); + + VerticalBox { + alignment: start; + Text { + text: show-light-page ? lights[selected-light].name : "Light Control App (Slint)"; + font-size: 14px; + horizontal-alignment: center; + } + } + + if !show-light-page: VerticalLayout { + alignment: center; + spacing: 5px; + padding-top: 25px; + // height: 200px; + HorizontalLayout { + alignment: LayoutAlignment.center; + spacing: 6px; + for i in 3: HorizontalLayout { + alignment: center; + height: 100px; + if i < lights.length: + Button { + width: 96px; + height: 96px; + // icon: light-icon; + text: lights[i].name; + clicked => { + selected-light = i; + show-light-page = true; + } + } + } + } + + HorizontalLayout { + alignment: LayoutAlignment.center; + spacing: 6px; + for i in 3: HorizontalLayout { + alignment: center; + height: 100px; + if i + 3 < lights.length: + Button { + width: 96px; + height: 96px; + // icon: light-icon; + text: lights[i].name; + clicked => { + selected-light = i + 3; + show-light-page = true; + } + } + } + } + } + + if show-light-page: Rectangle { + VerticalLayout { + alignment: LayoutAlignment.center; + width: 320px; + spacing: 4px; + + HorizontalLayout { + alignment: LayoutAlignment.center; + Slider { + width: 250px; + maximum: 255; + value: lights[selected-light].brightness; + changed(f) => { + lights[selected-light].brightness = f + } + } + } + + Text { + text: "Brightness"; + horizontal-alignment: center; + } + + Rectangle { + height: 16px; + } + + HorizontalLayout { + alignment: LayoutAlignment.center; + Switch { + checked: lights[selected-light].on; + toggled => { + lights[selected-light].on = !lights[selected-light].on + } + } + } + + Text { + text: "On / Off"; + horizontal-alignment: center; + } + } + + Button { + x: 10px; + y: 10px; + text: "<"; + clicked => { + show-light-page = false; + } + } + } + } diff --git a/slint-based/ui/microwave-ui.slint b/slint-based/ui/microwave-ui.slint new file mode 100644 index 0000000..161dfcf --- /dev/null +++ b/slint-based/ui/microwave-ui.slint @@ -0,0 +1,146 @@ +import { Button, VerticalBox, HorizontalBox, AboutSlint } from "std-widgets.slint"; + +export component MicrowaveUI inherits Window { + width: 320px; + height: 240px; + + property <[string]> wattages: ["180W", "220W", "360W", "480W", "620W", "800W"]; + property selected-wattage: 0; + + callback add-10s(); + callback sub-10s(); + callback start-timer(); + callback stop-timer(); + callback reset-timer(); + + in property show-start-timer: true; + in property show-stop-timer: false; + in property show-reset-timer: false; + + in property timer-text: "00:00:000"; + + property start-timer-icon: @image-url("../img/play.png"); + property stop-timer-icon: @image-url("../img/pause.png"); + property reset-timer-icon: @image-url("../img/xmark-square.png"); + + function multifunction-button-pressed() { + if (show-start-timer) { + start-timer(); + } + if (show-stop-timer) { + stop-timer(); + } + if (show-reset-timer) { + reset-timer(); + } + } + + function multifunction-button-icon() -> image { + if (show-start-timer) { + return start-timer-icon; + } + if (show-stop-timer) { + return stop-timer-icon; + } + if (show-reset-timer) { + return reset-timer-icon; + } + return start-timer-icon; + } + + VerticalBox { + alignment: start; + Text { + text: " Microwave UI (Slint)"; + font-size: 14px; + horizontal-alignment: center; + } + } + + HorizontalBox { + VerticalBox { + padding-top: 15px; + padding-bottom: 20px; + Button { + enabled: (selected-wattage < wattages.length - 1) && show-start-timer; + text: "+"; + clicked => { + selected-wattage += 1 + } + } + + Text { + font-size: 24px; + text: wattages[selected-wattage]; + horizontal-alignment: center; + vertical-alignment: center; + } + + Button { + enabled: !(selected-wattage <= 0) && show-start-timer; + text: "-"; + clicked => { + selected-wattage -= 1 + } + } + } + + VerticalBox { + alignment: center; + Text { + text: timer-text; + font-size: 24px; + horizontal-alignment: center; + } + + Rectangle { + height: 4px; + } + + HorizontalLayout { + alignment: center; + spacing: 8px; + + Button { + text: "+10s"; + enabled: show-start-timer; + clicked => { + add-10s(); + } + } + + Text { + text: "Set Timer"; + vertical-alignment: center; + } + + Button { + text: "-10s"; + enabled: show-start-timer; + clicked => { + sub-10s(); + } + } + } + + HorizontalLayout { + alignment: center; + spacing: 8px; + Button { + icon: @image-url("../img/restart.png"); + clicked => { + reset-timer(); + } + } + + Button { + icon: multifunction-button-icon(); + // visible: root.show_start_timer; + clicked => { + multifunction-button-pressed(); + } + } + } + } + } +} diff --git a/slint-based/ui/timer-app.slint b/slint-based/ui/timer-app.slint new file mode 100644 index 0000000..920ccc3 --- /dev/null +++ b/slint-based/ui/timer-app.slint @@ -0,0 +1,114 @@ +import { Button, VerticalBox , AboutSlint } from "std-widgets.slint"; + +export component TimerApp inherits Window { + width: 320px; + height: 240px; + + callback add-10s(); + callback sub-10s(); + callback start-timer(); + callback stop-timer(); + callback reset-timer(); + + in property show-start-timer: true; + in property show-stop-timer: false; + in property show-reset-timer: false; + + in property timer-text: "00:00:000"; + + property start-timer-icon: @image-url("../img/play.png"); + property stop-timer-icon: @image-url("../img/pause.png"); + property reset-timer-icon: @image-url("../img/xmark-square.png"); + + function multifunction-button-pressed() { + if (show-start-timer) { + start-timer(); + } + if (show-stop-timer) { + stop-timer(); + } + if (show-reset-timer) { + reset-timer(); + } + } + + function multifunction-button-icon() -> image { + if (show-start-timer) { + return start-timer-icon; + } + if (show-stop-timer) { + return stop-timer-icon; + } + if (show-reset-timer) { + return reset-timer-icon; + } + return start-timer-icon; + } + + VerticalBox { + alignment: start; + Text { + text: "Timer App (Slint)"; + font-size: 14px; + horizontal-alignment: center; + } + } + + VerticalBox { + alignment: center; + Text { + text: timer-text; + font-size: 24px; + horizontal-alignment: center; + } + + Rectangle { + height: 4px; + } + + HorizontalLayout { + alignment: center; + spacing: 8px; + + Button { + text: "+10s"; + enabled: show-start-timer; + clicked => { + add-10s(); + } + } + + Text { + text: "Set Timer"; + vertical-alignment: center; + } + + Button { + text: "-10s"; + enabled: show-start-timer; + clicked => { + sub-10s(); + } + } + } + + HorizontalLayout { + alignment: center; + spacing: 8px; + Button { + icon: @image-url("../img/restart.png"); + clicked => { + reset-timer(); + } + } + + Button { + icon: multifunction-button-icon(); + // visible: root.show_start_timer; + clicked => { + multifunction-button-pressed(); + } + } + } + } +}