前言:说一下为什么要用slint,既然都选择用rust开发嵌入式了避免不了要折腾,也是为后续驱动st7789做铺垫吧。而其实对应OLED屏幕已经有一个很好用的库了embedded-graphics,几乎能满足OLED屏幕的全部需求了,但是slint有动画,定时器,声明式,这些优势,再嵌入式开发再合适不过了。在写这个的时候也遇到不少坑,所以记录一下吧
创建项目
引入库编译问题已经在 ESP32C3引入Slint 文章解决了
然后再Cargo.toml
中添加依赖
# 基础依赖
critical-section = "1.2.0"
esp-hal = { version = "1.0.0-beta.0", features = ["esp32c3","unstable"] }
esp-alloc = "0.7.0"
esp-println = { version = "0.13.1",features = ["esp32c3","log"] }
log = "0.4.27"
esp-backtrace = { version = "0.15.1" ,features = ["esp32c3","println","panic-handler"]}
# 图形相关
embedded-hal = "0.2.7"
embedded-graphics-core = "0.4.0"
embedded-graphics = "0.8.1"
embedded-graphics-framebuf = "0.5.0"
ssd1306 = { version = "0.10.0" ,features = ["graphics"]}
# 异步
esp-hal-embassy = { version = "0.7.0", features = ["esp32c3"] }
embassy-time = { version = "0.4.0", features = ["generic-queue-8"] }
embassy-executor = { version = "0.7.0",features = ["task-arena-size-20480"] }
static_cell = { version = "2.1.0", features = ["nightly"] }
[dependencies.slint]
git = "https://github.com/slint-ui/slint"
rev = "29168bc89270798f6075a0a729c14a3f011ceb4f" # 修复提交的哈希
default-features = false
features = [
"compat-1-2",
"unsafe-single-threaded", # 启用单线程模式
"libm",
"renderer-software",
]
[build-dependencies]
slint-build = {git = "https://github.com/slint-ui/slint",rev = "29168bc89270798f6075a0a729c14a3f011ceb4f"}
配置Slint
我们使用slint的ui跟代码分离的模式,修改build.rs
新增一个build_slint函数,然后在main函数中调用它
fn main(){
build_slint();
...
}
fn build_slint(){
slint_build::compile_with_config(
"ui/MainView.slint",
slint_build::CompilerConfiguration::new()
.embed_resources(slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer)
).unwrap()
}
创建UI文件
在项目根目录下创建 ui/MainView.slint 文件,定义 UI 界面
// 由于是在no_std的环境下,没有默认字体,使用需要引入自定义的字体
import "./fonts/simhei.ttf";
// 这里定义了一个全局的状态,用于在rust代码里进行修改ui状态
export global MyState {
in-out property <bool> is_show : true;
}
export component MainView inherits Window {
default-font-family: "黑体";
width: 128px;
height: 64px;
Rectangle {
x:0px;
y:0px;
width: 100%;
height: 100%;
background: transparent;
border-color: black;
border-width: 5px;
}
Rectangle {
x:10px;
y:6px;
width: 50px;
height: 50px;
background: black;
border-radius: 360px;
visible: MyState.is_show;
}
property <bool> is_show : false;
Rectangle {
x:80px;
y:6px;
width: 20px;
height: 20px;
background: black;
visible: is_show;
}
// 使用了slint的ui的定时器
timer:=Timer{
interval: 0.5s;
running: true;
triggered => {
is_show = !is_show;
}
}
Text {
font-family: "SimHei";
text: "12345";
color: black;
font-weight: 900;
font-size: 40px;
x:66px;
}
}
编写main.rs代码
因为是使用的no_std
,没有多线程,而slint的run方法会调用invoke_from_event_loop
方法,进而导致阻塞线程,所以需要改造,不使用run方法来渲染,而是手动处理渲染,在实现traitPlatform
时,不去重写invoke_from_event_loop
函数,我们在初始化代码块去使用异步发方式实现
slint::include_modules!();
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
loop {
error!("panic:{}",info);
}
}
#[esp_hal_embassy::main]
async fn main(spawner: Spawner) -> ! {
// generator version: 0.3.1
// init_logger_from_env();
init_logger(LevelFilter::Debug);
esp_alloc::heap_allocator!(size: 100 * 1024);
info!("分配内存完成");
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals =esp_hal::init(config);
// 配置异步
let timer0 = SystemTimer::new(peripherals.SYSTIMER);
esp_hal_embassy::init(timer0.alarm0);
let i2c = esp_hal::i2c::master::I2c::new(peripherals.I2C0, esp_hal::i2c::master::Config::default()
.with_frequency(Rate::from_khz(400)))
.unwrap()
.with_sda(peripherals.GPIO8)
.with_scl(peripherals.GPIO9);
let display = Ssd1306::new(
I2CDisplayInterface::new(i2c),
DisplaySize128x64,
DisplayRotation::Rotate0).into_buffered_graphics_mode();
info!("开启异步任务");
spawner.spawn(ui_run(display,spawner)).ok();
loop{
Timer::after_secs(2).await;
}
// for inspiration have a look at the examples at https://gitcahub.com/esp-rs/esp-hal/tree/esp-hal-v1.0.0-beta.0/examples/src/bin
}
#[embassy_executor::task]
async fn ui_run(mut display: SSD,spawner: Spawner){
// 创建平台实例
let window = slint::platform::software_renderer::MinimalSoftwareWindow::new(
slint::platform::software_renderer::RepaintBufferType::ReusedBuffer,
);
window.set_size(PhysicalSize::new(128,64));
let platform = EspPlatform::new(window.clone());
slint::platform::set_platform(Box::new(platform)).unwrap();
// 创建ui
let ui = MainView::new().unwrap();
ui.window().set_size(PhysicalSize::new(128,64));
let ui_weak = ui.as_weak();
spawner.spawn(ui_task(ui_weak)).ok();
info!("进入了loop run");
// 初始化显示
display.init().unwrap();
display.clear(BinaryColor::Off).unwrap();
display.flush().unwrap();
info!("清理屏幕 on");
// 创建行缓冲区
let display_width = 128; // 根据实际显示宽度设置
let mut the_frame_buffer = [Rgb565Pixel(0); 128 * 64]; // 128x64
// 这里是我们手动实现的invoke_from_event_loop
loop {
// 更新UI状态
slint::platform::update_timers_and_animations();
// 渲染UI
window.draw_if_needed(|renderer| {
info!("进入渲染回调");
renderer.render_by_line(FrameBuffer{ frame_buffer: &mut the_frame_buffer, stride: display_width,display:&mut display });
info!("退出渲染回调");
// 退出渲染回调,说明整个帧已经渲染完成,这时候刷新屏幕
display.flush().unwrap();
});
Timer::after_millis(100).await;
}
}
#[embassy_executor::task]
async fn ui_task(view:Weak<MainView>){
info!("ui_task 已经开启");
loop{
// 在这里用rust的代码更新slint的ui状态
let view = view.clone();
let view = view.upgrade().unwrap();
let show = view.global::<MyState>().get_is_show();
view.global::<MyState>().set_is_show(!show);
info!("show的状态为:{}",show);
Timer::after_secs(2).await;
}
}
// 平台实现结构体
// 为了方便,先将OLED的屏幕生命周期设置为静态的
type SSD = Ssd1306<I2CInterface<I2c<'static,Blocking>>,DisplaySize128x64,BufferedGraphicsMode<DisplaySize128x64>>;
struct EspPlatform{
window: Rc<slint::platform::software_renderer::MinimalSoftwareWindow>,
}
impl EspPlatform {
fn new(window:Rc<MinimalSoftwareWindow>) -> Self {
Self {
window
}
}
}
/// 实现 Platform trait
impl Platform for EspPlatform {
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
info!("成功创建了window");
Ok(self.window.clone())
}
fn duration_since_start(&self) -> core::time::Duration {
let time = Instant::now().duration_since_epoch().as_millis();
// info!("time :{}",time);
core::time::Duration::from_millis(
time
)
}
}
struct FrameBuffer<'a>{ frame_buffer: &'a mut [Rgb565Pixel], stride: usize,display:&'a mut SSD }
impl<'a> LineBufferProvider for FrameBuffer<'a> {
type TargetPixel = Rgb565Pixel;
fn process_line(
&mut self,
line: usize,
range: core::ops::Range<usize>,
render_fn: impl FnOnce(&mut [Self::TargetPixel]),
) {
let line_start = line * self.stride;
let line_pixels = &mut self.frame_buffer[line_start..line_start + 128];
render_fn(&mut line_pixels[range.clone()]);
// 规定黑色为亮,由于是在oled屏幕上渲染,只有亮或者不亮,为了方便我们规定一个颜色为亮色,其余的颜色都视为不亮,在ui页面也是,使用黑色作为渲染的区域
let black = Rgb565Pixel(0);
for (x,row) in line_pixels.iter().enumerate(){
// 先将要更新的行的地方进行清屏
self.display.set_pixel(x as u32,line as u32,false);
// 然后对应要渲染的像素点进行渲染
if row == &black{
self.display.set_pixel(x as u32, line as u32, true);
}
}
}
}
编译查看效果
预览页面
烧录的效果跟我们slint编写的ui页面一致
评论 (0)