slint esp32 tokio

2 篇文章 0 订阅
1 篇文章 0 订阅

更新:使用eso32c2-120M spi-40M DMA,屏幕 132x162,使用全屏缓冲,spi 全屏刷新耗时 15ms,slint 132x162 全屏渲染耗时 105ms,33x33 元素渲染耗时 16ms,渲染的东西越多渲染耗时越高,观感流畅

源码:https://github.com/xiaguangbo/slint_esp32_tokio
cpu 是 esp32c2,屏幕是 ili9341,触摸是 xpt2046,使用 spi 半双工
不使用DMA,单行缓冲,单行刷新,SPI 40M,240*320全屏刷新为1.5秒
这是一个游戏,翻到两个一样的就成功,slint官网有入门示例,就是这个,然后把 .slint 和 控制逻辑拿过来直接用。就是slint平台需要稍微移植下,字体会自动打包
在这里插入图片描述

Cargo.toml

[package]
name = "esp32c2"
version = "0.1.0"
authors = ["xxx"]
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
opt-level = "z"

[features]
default = ["std", "embassy", "esp-idf-svc/native"]

pio = ["esp-idf-svc/pio"]
std = ["alloc", "esp-idf-svc/binstart", "esp-idf-svc/std"]
alloc = ["esp-idf-svc/alloc"]
nightly = ["esp-idf-svc/nightly"]
experimental = ["esp-idf-svc/experimental"]
embassy = [
    "esp-idf-svc/embassy-sync",
    "esp-idf-svc/critical-section",
    "esp-idf-svc/embassy-time-driver",
]

[dependencies]
log = { version = "*", default-features = false }
esp-idf-svc = { version = "*", default-features = false }

tokio = { version = "*", features = ["rt", "time", "sync"] }
num-traits = "*"
chrono = "*"
rand = "*"
slint = { version = "*", default-features = false, features = [
    "compat-1-2",
    "renderer-software",
    "unsafe-single-threaded",
] }

[build-dependencies]
embuild = "*"
slint-build = "*"

appwindow.slint

struct TileData {
    image: image,
    image_visible: bool,
    solved: bool,
}

component MemoryTile inherits Rectangle {
    in property <bool> open_curtain;
    in property <bool> solved;
    in property <image> icon;
    callback clicked;

    height: 50px;
    width: 50px;
    border-radius: self.width / 2;
    background: solved ? #34CE57 : #3960D5;
    clip: true;

    animate background { duration: 800ms; }

    Image {
        source: icon;
        width: parent.width;
        height: parent.height;
    }

    // Left curtain
    Rectangle {
        background: #193076;
        x: 0px;
        width: open_curtain ? 0px : (parent.width / 2);
        height: parent.height;
        clip: true;

        animate width {
            duration: 250ms;
            easing: ease-in;
        }

        Image {
            width: root.width - 25px;
            height: root.height - 25px;
            x: 13px;
            y: 13px;
            source: @image-url("../icons/tile_logo.png");
        }
    }

    // Right curtain
    Rectangle {
        background: #193076;
        x: open_curtain ? parent.width : (parent.width / 2);
        width: open_curtain ? 0px : (parent.width / 2);
        height: parent.height;
        clip: true;

        animate width {
            duration: 250ms;
            easing: ease-in;
        }
        animate x {
            duration: 250ms;
            easing: ease-in;
        }

        Image {
            width: root.width - 25px;
            height: root.height - 25px;
            x: parent.width - self.width - 13px;
            y: 13px;
            source: @image-url("../icons/tile_logo.png");
        }
    }

    TouchArea {
        clicked => {
            // Delegate to the user of this element
            root.clicked();
        }

        width: 100%;
        height: 100%;
    }
}

export component AppWindow inherits Window {
    width: 240px;
    height: 320px;

    callback check_if_pair_solved();
    // Added
    in property <bool> disable_tiles;
    // Added

    in-out property <[TileData]> memory_tiles: [
        { image: @image-url("../icons/at.png") },
        { image: @image-url("../icons/balance-scale.png") },
        { image: @image-url("../icons/bicycle.png") },
        { image: @image-url("../icons/bus.png") },
        { image: @image-url("../icons/cloud.png") },
        { image: @image-url("../icons/cogs.png") },
        { image: @image-url("../icons/motorcycle.png") },
        { image: @image-url("../icons/video.png") },
    ];

    for tile[i] in memory_tiles: MemoryTile {
        x: mod(i, 4) * (root.width / 4);
        y: floor(i / 4) * (root.width / 4);
        width: 50px;
        height: 50px;
        icon: tile.image;
        open_curtain: tile.image_visible || tile.solved; // 任何一个满足都打开帘子
        // propagate the solved status from the model to the tile
        solved: tile.solved;
        clicked => {
            // old: tile.image_visible = !tile.image_visible;
            // new:
            // 可不可以点击
            if (!root.disable_tiles) {
                tile.image_visible = !tile.image_visible;
                root.check_if_pair_solved();
            }
        }
    }
}

ui

use std::{borrow::Borrow, cell::RefCell, rc::Rc};

use slint::platform::{software_renderer::*, PointerEventButton, WindowAdapter, WindowEvent};
use slint::Model;
use tokio::time;

use esp_idf_svc::hal::{gpio::*, peripheral::*, spi::*};

use crate::component::{ili9341, xpt2046};

slint::include_modules!();

pub async fn work<SPI, CS, CS2, DC>(spi1: SPI, spi2: SPI, cs1: CS, cs2: CS2, dc: DC)
where
    SPI: Borrow<SpiDriver<'static>> + 'static,
    CS: Peripheral<P = CS> + OutputPin,
    CS2: Peripheral<P = CS2> + OutputPin,
    DC: Peripheral<P = DC> + OutputPin,
{
    let mut ili9341 = ili9341::ILI9341::new(spi1, cs1, dc);
    let xpt2046 = xpt2046::XPT2046::new(spi2, cs2);

    ili9341.open();

    let buffer_provider = DrawBuffer {
        display: ili9341,
        buffer: vec![Rgb565Pixel::default(); ili9341::ILI9341_WIDTH as usize].leak(),
    };

    slint::platform::set_platform(Box::new(SlintBackend {
        window: Default::default(),
        now: std::time::Instant::now().into(),
        buffer_provider: buffer_provider.into(),
        touch: xpt2046.into(),
        last_touch: None.into(),
    }))
    .unwrap();

    let main_window = AppWindow::new().unwrap();

    // Fetch the tiles from the model
    let mut tiles: Vec<TileData> = main_window.get_memory_tiles().iter().collect();
    // Duplicate them to ensure that we have pairs
    tiles.extend(tiles.clone());

    // Randomly mix the tiles
    use rand::seq::SliceRandom;
    let mut rng = rand::thread_rng();
    tiles.shuffle(&mut rng);

    // Assign the shuffled Vec to the model property
    let tiles_model = std::rc::Rc::new(slint::VecModel::from(tiles));
    main_window.set_memory_tiles(tiles_model.clone().into());

    let main_window_weak = main_window.as_weak();
    // 点击的回调函数
    main_window.on_check_if_pair_solved(move || {
        // 如果元素的(image_visible && !solved)为真,则得到他
        // 就是被打开看的且没有被标记的对象
        let mut flipped_tiles = tiles_model
            .iter()
            .enumerate()
            .filter(|(_, tile)| tile.image_visible && !tile.solved);

        // 当检查出有两个这样的元素就进入判断
        if let (Some((t1_idx, mut t1)), Some((t2_idx, mut t2))) =
            (flipped_tiles.next(), flipped_tiles.next())
        {
            let is_pair_solved = t1 == t2; // 比较两个元素的值是不是一样的,包括图片的 rgba 和元素属性,也就是 TileData 的所有成员

            // 一样
            if is_pair_solved {
                t1.solved = true; // 彻底打开帘子
                tiles_model.set_row_data(t1_idx, t1);
                t2.solved = true;
                tiles_model.set_row_data(t2_idx, t2);
            }
            // 不一样
            else {
                let main_window = main_window_weak.unwrap();
                main_window.set_disable_tiles(true); // 防止继续点击
                let tiles_model = tiles_model.clone();

                // 延时 1s
                slint::Timer::single_shot(std::time::Duration::from_secs(1), move || {
                    main_window.set_disable_tiles(false); // 可继续点击
                    t1.image_visible = false; // 关闭帘子
                    tiles_model.set_row_data(t1_idx, t1);
                    t2.image_visible = false;
                    tiles_model.set_row_data(t2_idx, t2);
                });
            }
        }
    });

    loop {
        slint::run_event_loop().unwrap();
        time::sleep(time::Duration::from_millis(20)).await;
    }
}

pub struct SlintBackend<'a, SPI, DC>
where
    SPI: Borrow<SpiDriver<'a>> + 'a,
    DC: Peripheral<P = DC> + OutputPin,
{
    window: RefCell<Option<Rc<MinimalSoftwareWindow>>>,
    now: RefCell<std::time::Instant>,
    buffer_provider: RefCell<DrawBuffer<'a, SPI, DC>>,
    touch: RefCell<xpt2046::XPT2046<'a, SPI>>,
    last_touch: RefCell<Option<slint::LogicalPosition>>,
}

impl<'a, SPI, DC> slint::platform::Platform for SlintBackend<'a, SPI, DC>
where
    SPI: Borrow<SpiDriver<'a>> + 'a,
    DC: Peripheral<P = DC> + OutputPin,
{
    fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, slint::PlatformError> {
        let window = MinimalSoftwareWindow::new(RepaintBufferType::ReusedBuffer);

        self.window.replace(Some(window.clone()));
        self.window
            .borrow()
            .as_ref()
            .unwrap()
            .set_size(slint::PhysicalSize::new(
                ili9341::ILI9341_WIDTH as u32,
                ili9341::ILI9341_HEIGHT as u32,
            ));

        Ok(window)
    }

    fn duration_since_start(&self) -> std::time::Duration {
        self.now.borrow().elapsed()
    }

    fn run_event_loop(&self) -> Result<(), slint::PlatformError> {
        let while_now = std::time::Instant::now();
        let mut touch_now = std::time::Instant::now();
        let mut touch_ed = false;

        // 连续绘制达到 100ms 就跳过
        while while_now.elapsed().as_millis() < 100 {
            slint::platform::update_timers_and_animations();

            if let Some(window) = self.window.borrow().clone() {
                if !touch_ed {
                    touch_ed = !touch_ed;
                    touch_now = std::time::Instant::now();

                    if let Some(event) = match self.touch.borrow_mut().read() {
                        Some(v) => {
                            let position = slint::PhysicalPosition::new(
                                (v.x * ili9341::ILI9341_WIDTH as f32) as i32,
                                (v.y * ili9341::ILI9341_HEIGHT as f32) as i32,
                            )
                            .to_logical(window.scale_factor());

                            Some(match self.last_touch.borrow_mut().replace(position) {
                                Some(_) => WindowEvent::PointerMoved { position },
                                _ => WindowEvent::PointerPressed {
                                    position,
                                    button: PointerEventButton::Left,
                                },
                            })
                        }
                        _ => self.last_touch.borrow_mut().take().map(|position| {
                            WindowEvent::PointerReleased {
                                position,
                                button: PointerEventButton::Left,
                            }
                        }),
                    } {
                        let is_pointer_release_event =
                            matches!(event, WindowEvent::PointerReleased { .. });

                        window.dispatch_event(event);

                        if is_pointer_release_event {
                            window.dispatch_event(WindowEvent::PointerExited);
                        }
                    }
                } else {
                    if touch_now.elapsed().as_millis() >= 20 {
                        // 每隔一段时间才能再次读取触摸,避免频繁处理
                        touch_ed = !touch_ed;
                    }
                }

                window.draw_if_needed(|renderer| {
                    renderer.render_by_line(&mut *self.buffer_provider.borrow_mut());
                });

                if !window.has_active_animations() {
                    // 如果没有需要绘制的东西就跳出,否则就继续绘制
                    break;
                }
            }
        }
        Ok(())
    }
}

struct DrawBuffer<'a, SPI, DC>
where
    SPI: Borrow<SpiDriver<'a>> + 'a,
    DC: Peripheral<P = DC> + OutputPin,
{
    display: ili9341::ILI9341<'a, SPI, DC>,
    buffer: &'a mut [Rgb565Pixel],
}

impl<'a, SPI, DC> LineBufferProvider for &mut DrawBuffer<'a, SPI, DC>
where
    SPI: Borrow<SpiDriver<'a>> + 'a,
    DC: Peripheral<P = DC> + OutputPin,
{
    type TargetPixel = Rgb565Pixel;

    fn process_line(
        &mut self,
        line: usize,
        range: std::ops::Range<usize>,
        render_fn: impl FnOnce(&mut [Rgb565Pixel]),
    ) {
        let buffer = &mut self.buffer[range.clone()];

        render_fn(buffer);
        self.display.write_pixel_slint(
            range.start as u16,
            line as u16,
            range.end as u16,
            line as u16,
            &buffer,
        );
    }
}

work

use std::{rc, thread};

use tokio::{runtime, task, time};

use esp_idf_svc::hal::{gpio, peripherals, spi};

use crate::module::*;

pub fn work() {
    thread::Builder::new()
        .stack_size(8 * 1024)
        .spawn(|| {
            task::LocalSet::new().block_on(
                &runtime::Builder::new_current_thread()
                    .enable_all()
                    .build()
                    .unwrap(),
                async {
                    let peripherals = peripherals::Peripherals::take().unwrap();

                    let spi = spi::SpiDriver::new::<spi::SPI2>(
                        peripherals.spi2,
                        peripherals.pins.gpio0,
                        peripherals.pins.gpio1,
                        Option::<gpio::AnyIOPin>::None,
                        &spi::SpiDriverConfig::new(),
                    )
                    .unwrap();

                    let spi = rc::Rc::new(spi);
                    let spi_1 = spi.clone();
                    let spi_2 = spi.clone();

                    task::spawn_local(async move {
                        ui::work(
                            spi_1,
                            spi_2,
                            peripherals.pins.gpio3,
                            peripherals.pins.gpio4,
                            peripherals.pins.gpio2,
                        )
                        .await;
                    });

                    loop {
                        time::sleep(time::Duration::MAX).await;
                    }
                },
            );
        })
        .unwrap();
}

build

fn main() {
    embuild::espidf::sysenv::output();

    slint_build::compile_with_config(
        "ui/appwindow.slint",
        slint_build::CompilerConfiguration::new()
            .embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer),
    )
    .unwrap();
}

ili9341

use std::borrow::*;

use esp_idf_svc::hal::{delay::Delay, gpio::*, peripheral::*, prelude::*, spi::*};

use slint::platform::software_renderer::Rgb565Pixel;

pub const ILI9341_WIDTH: u16 = 240;
pub const ILI9341_HEIGHT: u16 = 320;

/*
# 初始化
第一个字节是命令
等待5ms

36, 48 左上右下竖屏、c8 右下左上竖屏、e8 左下右上横屏、28右上左下横屏
3a, 55 像素格式 565
11     退出睡眠
29     开显示、28 关显示。不会更改内存内容

# 设置区域
可设置一点、一行或一个方块,方块区域会自动换行

2a x坐标
16bit xs
16bit xe

2b y坐标
16bit ys
16bit ye

2c
16bit 565 颜色数据
*/

pub struct ILI9341<'a, SPI, DC>
where
    SPI: Borrow<SpiDriver<'a>> + 'a,
    DC: Peripheral<P = DC> + OutputPin,
{
    spi: SpiDeviceDriver<'a, SPI>,
    dc: PinDriver<'a, DC, Output>,
    line_buf: Vec<u8>,
}

impl<'a, SPI, DC> ILI9341<'a, SPI, DC>
where
    SPI: Borrow<SpiDriver<'a>> + 'a,
    DC: Peripheral<P = DC> + OutputPin,
{
    pub fn new<CS>(spi: SPI, cs: CS, dc: DC) -> Self
    where
        CS: Peripheral<P = CS> + OutputPin,
    {
        let config = config::Config::default()
            .baudrate(40.MHz().into())
            .duplex(config::Duplex::Half3Wire);

        Self {
            spi: SpiDeviceDriver::new(spi, Some(cs), &config).unwrap(),
            dc: PinDriver::output(dc).unwrap(),
            line_buf: vec![0u8; (ILI9341_WIDTH * 2) as usize],
        }
    }

    pub fn open(&mut self) {
        let delay = Delay::new_default();

        for _ in 0..2 {
            delay.delay_ms(20);
            self.write_cmd_data_u8(0x36, Some(&[0x48]));
            self.write_cmd_data_u8(0x3a, Some(&[0x55]));
            self.write_cmd_data_u8(0x11, None);
            self.write_cmd_data_u8(0x29, None);
        }
    }

    pub fn write_pixel_slint(
        &mut self,
        x: u16,
        y: u16,
        x_end: u16,
        y_end: u16,
        pixel: &[Rgb565Pixel],
    ) {
        self.write_draw_range(x, y, x_end, y_end);
        self.write_cmd_data_slint(0x2c, pixel);
    }

    fn write_cmd_data_u8(&mut self, cmd: u8, data: Option<&[u8]>) {
        self.dc.set_low().unwrap();
        self.spi.write(&[cmd]).unwrap();

        if let Some(v) = data {
            self.dc.set_high().unwrap();
            self.spi.write(v).unwrap();
        }
    }

    fn write_draw_range(&mut self, x: u16, y: u16, x_end: u16, y_end: u16) {
        let mut x_buf = [0u8; 4];
        let mut y_buf = [0u8; 4];

        x_buf[0..=1].copy_from_slice(&x.to_be_bytes());
        x_buf[2..=3].copy_from_slice(&x_end.to_be_bytes());

        y_buf[0..=1].copy_from_slice(&y.to_be_bytes());
        y_buf[2..=3].copy_from_slice(&y_end.to_be_bytes());

        self.write_cmd_data_u8(0x2a, Some(&x_buf));
        self.write_cmd_data_u8(0x2b, Some(&y_buf));
    }

    fn write_cmd_data_slint(&mut self, cmd: u8, data: &[Rgb565Pixel]) {
        let mut i = 0;
        data.iter().for_each(|v| {
            self.line_buf[i..=i + 1].copy_from_slice(v.0.to_be_bytes().as_ref());
            i += 2;
        });

        self.dc.set_low().unwrap();
        self.spi.write(&[cmd]).unwrap();

        self.dc.set_high().unwrap();
        self.spi.write(self.line_buf[0..i].as_ref()).unwrap();
    }
}

xpt2046

use std::borrow::*;

use esp_idf_svc::hal::{gpio::*, peripheral::*, prelude::*, spi::*};

/*
d0 读 x 轴
90 读 y 轴
*/

pub struct XPT2046Touch {
    pub x: f32,
    pub y: f32,
}

pub struct XPT2046<'a, SPI>
where
    SPI: Borrow<SpiDriver<'a>> + 'a,
{
    spi: SpiDeviceDriver<'a, SPI>,
}

impl<'a, SPI> XPT2046<'a, SPI>
where
    SPI: Borrow<SpiDriver<'a>> + 'a,
{
    pub fn new<CS>(spi: SPI, cs: CS) -> Self
    where
        CS: Peripheral<P = CS> + OutputPin,
    {
        let config = config::Config::default()
            .baudrate(2.MHz().into())
            .duplex(config::Duplex::Half3Wire);

        Self {
            spi: SpiDeviceDriver::new(spi, Some(cs), &config).unwrap(),
        }
    }

    pub fn read(&mut self) -> Option<XPT2046Touch> {
        let mut x_u16 = [0u16; 3];
        let mut y_u16 = [0u16; 3];

        for i in 0..x_u16.len() {
            let mut x = [0u8; 2];
            let mut y = [0u8; 2];

            self.spi
                .transaction(&mut [Operation::Write(&[0xd0]), Operation::Read(&mut x)])
                .unwrap();

            self.spi
                .transaction(&mut [Operation::Write(&[0x90]), Operation::Read(&mut y)])
                .unwrap();

            x_u16[i] = u16::from_be_bytes(x) << 1 >> 4;
            y_u16[i] = u16::from_be_bytes(y) << 1 >> 4;
        }

        x_u16.sort();
        y_u16.sort();

        // 实测最大最小值
        let x = x_u16[1].max(336).min(3847);
        let y = y_u16[1].max(184).min(3584);

        let x = (x - 336) as f32 / (3847 - 336) as f32;
        let y = (y - 184) as f32 / (3584 - 184) as f32;

        // 判断有没有触摸
        if x == 0 as f32 && y == 1 as f32 {
            None
        } else {
            Some(XPT2046Touch { x, y })
        }
    }
}
  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值