首页
苏兮影视
随笔记
壁纸
更多
直播
时光轴
友联
关于
统计
Search
1
v2ray节点搭建
1,074 阅读
2
软件添加id功能按钮
1,029 阅读
3
QQ扫码无法登录的解决方案
990 阅读
4
网易云音乐歌单ID获取教程
922 阅读
5
js逆向xhs
792 阅读
谈天说地
建站源码
经验教程
资源分享
动漫美图
登录
Search
标签搜索
java
rust
flutter
esp32c3
springboot
安卓
linux
vue
dart
设计模式
docker
ssh
joe
快捷键
git
fish shell
maven
redis
netty
groovy
尽意
累计撰写
116
篇文章
累计收到
41
条评论
首页
栏目
谈天说地
建站源码
经验教程
资源分享
动漫美图
页面
苏兮影视
随笔记
壁纸
直播
时光轴
友联
关于
统计
搜索到
116
篇与
的结果
Flutter 实现流式消息接收
我们通过 Dio 发起一个 ResponseType.stream 请求,消费后端逐字符返回的 HTTP 流。核心代码如下:void sendMessage() => Dio().get( "http://192.168.149.178:8080/stream/covert?url=${state.text}", options: Options(responseType: ResponseType.stream), cancelToken: CancelToken(), ).then((response) { final data = response.data; if (data is! ResponseBody) return; state.data = ""; update(); data.stream.listen((chunk) { final res = utf8.decoder.convert(chunk); state.data += res; update(); }); });UI 部分使用 Expanded + SingleChildScrollView 展示实时追加的文本:Column( children: [ TextField(controller: logic.text), TextButton(onPressed: logic.sendMessage, child: Text("发送消息")), Expanded( child: SelectionArea( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [Text(state.data)], ), ), ), ), ], )后端使用 Spring WebFlux 将目标网页内容逐字符延迟返回:@GetMapping("/covert") public Flux<String> covert(@RequestParam String url) { return Mono.fromCallable(() -> { // 同步抓取网页内容(在非 Netty 线程执行) var request = new Request.Builder().url(url).build(); try (var resp = client.newCall(request).execute()) { return resp.body().string(); } }) .flux() .flatMap(text -> Flux.fromStream(text.chars().mapToObj(c -> String.valueOf((char) c))) .delayElements(Duration.ofMillis(10)) // 每字符间隔 10ms ); }该方案无需 SSE 或 WebSocket,仅依赖标准 HTTP 分块传输,即可实现“打字机式”流式响应,适用于 AI 对话、实时日志等场景。
2026年01月07日
42 阅读
0 评论
1 点赞
Flutter LayoutBuilder 原理深度解析
引言在 Flutter 响应式布局体系中,LayoutBuilder 是一个强大而特殊的组件。与大多数在构建阶段(build phase)确定其子树的组件不同,LayoutBuilder 将子组件的构建推迟到布局阶段(layout phase),从而能够根据父级传递的实际布局约束动态构建 UI。本文将深入源码层面,全面剖析 LayoutBuilder 的工作原理。核心架构概览类继承层次Widget ├── RenderObjectWidget │ └── ConstrainedLayoutBuilder<BoxConstraints> │ └── LayoutBuilder (具体实现)主要角色分工ConstrainedLayoutBuilder<T> - 抽象基类,定义约束构建器的通用协议LayoutBuilder - 面向 BoxConstraints 的具体实现_LayoutBuilderElement - 连接 Widget 与 RenderObject 的桥梁_RenderLayoutBuilder - 实际执行布局的 RenderObject源码深度解析1. Element 的初始化与绑定// _LayoutBuilderElement.mount 方法 @override void mount(Element? parent, Object? newSlot) { super.mount(parent, newSlot); // 1. 创建对应的 RenderObject renderObject.updateCallback(_rebuildWithConstraints); // 2. 绑定重建回调 }关键点分析:super.mount() 会创建 _RenderLayoutBuilder 实例updateCallback 将 _rebuildWithConstraints 方法注册到 RenderObject此时回调并未立即执行,只是建立了调用关系2. 布局阶段的核心调用链// _RenderLayoutBuilder.performLayout 方法 @override void performLayout() { final BoxConstraints constraints = this.constraints; // 1. 获取父级约束 rebuildIfNecessary(); // 2. 触发约束重建 if (child != null) { child!.layout(constraints, parentUsesSize: true); // 3. 布局子节点 size = constraints.constrain(child!.size); // 4. 确定自身大小 } else { size = constraints.biggest; } }2.1 约束的来源追踪约束的传递遵循 Flutter 渲染管线的标准流程:// RenderObject 层级传递 void layout(Constraints constraints, { bool parentUsesSize = false }) { this.constraints = constraints; // 父组件传递约束 // ... 后续布局计算 }this.constraints 在 performLayout 中通过 this.constraints 访问,这是 RenderObject 的标准属性。2.2 重建触发机制// RenderConstrainedLayoutBuilder 混入类 void rebuildIfNecessary() { assert(_callback != null); invokeLayoutCallback(_callback!); // 调用注册的回调 } // RenderObject 中的回调执行 void invokeLayoutCallback<T extends Constraints>(LayoutCallback<T> callback) { owner!._enableMutationsToDirtySubtrees(() { callback(constraints as T); // 关键:传递当前约束 }); }3. 安全的回调执行环境_enableMutationsToDirtySubtrees 是保证布局阶段安全修改组件树的关键:void _enableMutationsToDirtySubtrees(VoidCallback callback) { assert(_debugDoingLayout); bool? oldState; assert(() { oldState = _debugAllowMutationsToDirtySubtrees; _debugAllowMutationsToDirtySubtrees = true; // 临时允许修改 return true; }()); try { callback(); // 执行 LayoutBuilder 的 builder } finally { _shouldMergeDirtyNodes = true; assert(() { _debugAllowMutationsToDirtySubtrees = oldState!; // 恢复原状态 return true; }()); } }安全机制解析:断言保护:仅在布局阶段允许此操作状态切换:临时开启脏子树修改权限异常安全:使用 try-finally 确保状态恢复操作合并:设置 _shouldMergeDirtyNodes 以优化更新4. 约束重建的实际执行void _rebuildWithConstraints(ConstraintType constraints) { // 构建子组件的闭包 void updateChildCallback() { Widget built = widget.builder(this, constraints); // 调用用户定义的builder _child = updateChild(_child, built, null); // 更新子Element _needsBuild = false; _previousConstraints = constraints; // 缓存约束 } // 条件性执行重建 final VoidCallback? callback = _needsBuild || (constraints != _previousConstraints) ? updateChildCallback : null; owner!.buildScope(this, callback); }智能优化机制:_previousConstraints 缓存上次的约束仅当约束变化或标记需要重建时才执行避免不必要的重建,提升性能5. 更新与重建调度5.1 组件更新处理@override void update(ConstrainedLayoutBuilder<ConstraintType> newWidget) { super.update(newWidget); renderObject.updateCallback(_rebuildWithConstraints); // 重新绑定 if (newWidget.updateShouldRebuild(oldWidget)) { _needsBuild = true; // 标记需要重建 renderObject.markNeedsLayout(); // 触发重新布局 } }5.2 智能调度策略void _scheduleRebuild() { // 根据调度阶段决定立即标记还是延迟 final bool deferMarkNeedsLayout = switch (SchedulerBinding.instance.schedulerPhase) { SchedulerPhase.idle || SchedulerPhase.postFrameCallbacks => true, _ => false, }; if (!deferMarkNeedsLayout) { renderObject.markNeedsLayout(); } else { // 在稳定阶段延迟调度 SchedulerBinding.instance.scheduleFrameCallback(_frameCallback); } }调度策略优势:立即响应:在布局/绘制阶段立即触发更新延迟优化:在空闲阶段延迟到下一帧,避免打断当前操作设计模式分析1. 延迟决策模式LayoutBuilder 实现了典型的延迟决策模式:构建阶段:只建立结构和回调关系布局阶段:根据实际约束做出具体构建决策优势:适应动态布局需求,支持响应式设计2. 回调注册模式通过 updateCallback 机制实现松耦合:Element 向 RenderObject 注册回调RenderObject 在适当时机触发回调分离了决策逻辑与执行时机3. 安全沙箱模式_enableMutationsToDirtySubtrees 提供安全执行环境:临时放宽框架限制执行用户代码后立即恢复平衡灵活性与安全性实战应用示例1. 经典响应式布局Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { // 根据宽度决定布局方式 if (constraints.maxWidth > 600) { return Row(children: [Sidebar(), ContentArea()]); } else { return Column(children: [AppBar(), ContentArea()]); } }, ); }2. 动态尺寸计算Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final itemWidth = (constraints.maxWidth - 32) / 3; return GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: itemWidth / 100, ), itemBuilder: (context, index) => ItemWidget(), ); }, ); }源码设计亮点1. 性能优化约束缓存:通过 _previousConstraints 避免重复构建条件更新:仅在实际变化时触发重建智能调度:根据框架状态选择最佳更新时机2. 错误边界void _rebuildWithConstraints(ConstraintType constraints) { try { built = widget.builder(this, constraints); } catch (e, stack) { built = ErrorWidget.builder(...); // 优雅降级 } // ... 错误处理继续 }3. 调试支持详细的断言检查调试信息收集异常报告机制总结与启示LayoutBuilder 的设计体现了 Flutter 框架的几个核心理念:关注点分离:将构建决策延迟到拥有完整布局信息时性能优先:通过多种优化避免不必要的计算安全第一:在灵活性与框架稳定性间取得平衡声明式表达:通过简单的回调 API 提供强大功能通过深入分析 LayoutBuilder 的源码,我们不仅理解了其工作原理,更能学习到如何设计既灵活又稳定的框架级组件。这种在布局阶段获取约束并动态构建的能力,为 Flutter 的响应式设计提供了坚实的基础设施支持。源码文件:packages/flutter/lib/src/widgets/layout_builder.dart 设计模式:延迟决策、回调注册、安全沙箱 适用场景:响应式布局、动态 UI、约束依赖型组件
2025年12月25日
38 阅读
0 评论
1 点赞
ESP32C3使用Slint渲染TFT屏幕
这个教程算是在st7789屏幕没有买之前就已经开始打算了,之前的使用slint驱动oled也算是为这个打下基础吧创建新项目可以参考 ESP32C3使用Slint渲染OLED屏幕 引入slint依赖,创建跟配置slint跟这篇文章一样,引入的依赖基本上大差不差,只不过这个项目还需要引入st7789屏幕的相关依赖。然后在Cargo.toml中添加依赖 [dependencies] # 基础依赖 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 = "1.0.0" embedded-graphics-core = "0.4.0" embedded-graphics = "0.8.1" embedded-graphics-framebuf = "0.5.0" display-interface = "0.5.0" display-interface-spi = "0.5.0" mipidsi = "0.9.0" embedded-hal-bus = "0.3.0" # 异步 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","executor-thread"] } #embassy-futures = "0.1.1" static_cell = { version = "2.1.0", features = ["nightly"] } tinygif = "0.0.4" [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"} 创建ui文件创建 ui/main.slint 文件,作为程序的入口文件import { System,LocalTime,Route , Setting} from "./global.slint"; import { HomePage } from "./page/home_page.slint"; import { SettingPage } from "./page/setting_page.slint"; // 导出给rust代码调用 export { System,LocalTime,Setting } export component MainView inherits Window { background: black; width: System.screen_width; height: System.screen_height; default-font-family: "黑体"; // 简单实现路由 if System.route==Route.HomePage: HomePage{} if System.route==Route.SettingPage: SettingPage{} // 显示帧率 if Setting.fps == true:Rectangle { padding: 2px; x:0px; y:0px; width: fps.width; height: fps.height; background: black; fps:=Text { font-size: 20px; text: @tr("FPS:{}",System.fps); color: white; } } // 触摸切换页面,用于测试 TouchArea { clicked => { if(System.route == Route.HomePage){ System.route = Route.SettingPage; }else{ System.route = Route.HomePage; } } } }创建 ui/global.slint 存储全局变量,跟rust代码进行交互 export enum Route { HomePage, SettingPage } export global System { // 屏幕宽高 out property <length> screen_width:240px; out property <length> screen_height:240px; // 路由页面 in-out property <Route> route:Route.HomePage; // 帧率 in-out property <int> fps:0; } export global Setting { // 开启fps帧率显示 in-out property <bool> fps:true; } export global LocalTime { in-out property <int> hour:12; in-out property <int> minute:12; in-out property <int> scond:30; in-out property <int> year:2025; in-out property <int> month:12; in-out property <int> day:12; in-out property <int> week:1; private property <[string]> weeks:["周日","周一","周二","周三","周四","周五","周六"]; in-out property <string> week_cn:weeks[week]; }创建ui/page/home_page.slint文件展示主页import { System,LocalTime } from "../global.slint"; export component HomePage inherits Rectangle{ width: System.screen_width; height: System.screen_height; // 背景图片 bg:=Image { source: @image-url("../../assets/bg_ndmz.png"); } animate x { easing: ease-in-out; duration: 300ms; } VerticalLayout { spacing: 4px; alignment: start; week:=HorizontalLayout{ padding-top: 20px; alignment: center; spacing: 5px; Text { text: LocalTime.month; color: white; font-size: 20px; } Text { text: LocalTime.week_cn; color: white; font-size: 20px; } } time:=HorizontalLayout { padding: 2px; spacing: 5px; alignment: center; Rectangle { // background: blue; Text { text: LocalTime.hour; font-size: 60px; color: white; font-weight: 500; } } Text { width: 20px; text: ":"; font-size: 60px; color: white; font-weight: 500; } Rectangle { // background: pink; Text { text: LocalTime.minute; font-size: 60px; color: white; font-weight: 500; } } } } // 这里的定时器是简单模拟时间,实际项目应该从rust代码传入时间 Timer { running: true; interval: 1s; triggered => { LocalTime.minute+=1; } } }创建ui/page/setting_page.slint展示设置页面,这里简单做了给跑马灯的效果import { System } from "../global.slint"; export component SettingPage inherits Rectangle{ width: System.screen_width; height: System.screen_height; background: white; property <length> pos_x:0px; text:=Text { x:pos_x; text: "{}这里是设置页面aaaadfsadfsdf"; font-size: 40px; font-weight: 999; } Timer { running: true; interval: 50ms; triggered => { if(pos_x < System.screen_width - text.width){ pos_x = System.screen_width; }else{ pos_x -= 1px; } } } }编写main.rs代码#![no_std] #![no_main] extern crate alloc; use alloc::boxed::Box; use alloc::format; use alloc::rc::Rc; use alloc::string::ToString; use alloc::vec::Vec; use core::cell::RefCell; use core::fmt::Debug; use core::sync::atomic::{AtomicBool, Ordering}; use critical_section::Mutex; use embassy_executor::Spawner; use embassy_time::Timer; use embedded_graphics::mono_font::ascii::FONT_10X20; use embedded_graphics::mono_font::iso_8859_7::FONT_5X8; use embedded_graphics::mono_font::MonoTextStyle; use embedded_graphics::prelude::Primitive; use embedded_graphics::primitives::PrimitiveStyle; use embedded_graphics::text::Text; use embedded_graphics_core::Drawable; use embedded_graphics_core::pixelcolor::{Bgr565, BinaryColor, Rgb565}; use embedded_graphics_core::prelude::{DrawTarget, ImageDrawable, Point, RgbColor, Size}; use embedded_graphics_core::primitives::Rectangle; use embedded_hal::delay::DelayNs; use embedded_hal_bus::spi::{ExclusiveDevice, NoDelay}; use esp_hal::clock::{CpuClock, RadioClockController}; use esp_hal::delay::Delay; use esp_hal::{handler, main, ram, spi, Blocking}; use esp_hal::gpio::{Event, Input, InputConfig, Io, Level, Output, OutputConfig, Pull}; use esp_hal::i2c::master::I2c; use esp_hal::peripherals::Peripherals; use esp_hal::rtc_cntl::Rtc; use esp_hal::spi::master::Config; use esp_hal::spi::master::Spi; use esp_hal::spi::Mode; use esp_hal::time::{Duration, Instant, Rate}; use esp_hal::timer::systimer::SystemTimer; use esp_hal::timer::timg::TimerGroup; use esp_println::logger::{init_logger, init_logger_from_env}; use esp_println::println; use log::{error, info, warn, LevelFilter}; use mipidsi::{Builder, Display}; use mipidsi::interface::SpiInterface; use mipidsi::models::ST7789; use mipidsi::options::{ColorInversion, Orientation}; use slint::platform::{Platform, WindowAdapter}; use slint::platform::software_renderer::{LineBufferProvider, MinimalSoftwareWindow, Rgb565Pixel}; use slint::{invoke_from_event_loop, ModelRc, PhysicalSize, PlatformError, Rgb8Pixel, Rgba8Pixel, SharedPixelBuffer, TimerMode, Weak}; slint::include_modules!(); #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { error!("panic:{}",info); loop {} } #[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 mut delay = Delay::new(); let mut rst = Output::new(peripherals.GPIO3,Level::Low,OutputConfig::default()); rst.set_high(); let dc = Output::new(peripherals.GPIO4,Level::Low,OutputConfig::default()); let mut backlight = Output::new(peripherals.GPIO5,Level::Low,OutputConfig::default()); let sclk = peripherals.GPIO6; let sda = peripherals.GPIO7; let sdi = peripherals.GPIO8; let cs = peripherals.GPIO10; let spi = Spi::new(peripherals.SPI2, Config::default() .with_mode(Mode::_3) .with_frequency(Rate::from_mhz(60)) ).unwrap() .with_sck(sclk) .with_miso(sdi) .with_mosi(sda); let cs_output = Output::new(cs,Level::High,OutputConfig::default()); let spi_device = ExclusiveDevice::new_no_delay(spi,cs_output).unwrap(); let di = SpiInterface::new(spi_device,dc,unsafe{ &mut BUFFER }); let display = Builder::new(ST7789, di) .display_size(240,240) .invert_colors(ColorInversion::Inverted) .color_order(mipidsi::options::ColorOrder::Rgb) .orientation(Orientation::new()) .reset_pin(rst) .init(&mut delay).unwrap(); println!("display 初始化完成"); backlight.set_high(); info!("开启异步任务"); spawner.spawn(ui_run(display)).ok(); // for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v1.0.0-beta.0/examples/src/bin } static mut BUFFER: [u8; 512] = [0_u8; 512]; type DISPLAY = Display<SpiInterface<'static, ExclusiveDevice<Spi<'static,Blocking>,Output<'static>,NoDelay>,Output<'static>>,ST7789,Output<'static>>; #[embassy_executor::task] async fn ui_run(mut display: DISPLAY){ // 创建平台实例 let window = MinimalSoftwareWindow::new( slint::platform::software_renderer::RepaintBufferType::ReusedBuffer, ); window.set_size(PhysicalSize::new(240,240)); 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(240,240)); // 初始化显示 display.clear(Rgb565::BLACK).unwrap(); info!("清理屏幕"); // 创建行缓冲区 let display_width = 240; // 根据实际显示宽度设置 let mut the_frame_buffer = [Rgb565Pixel(0); 240 ]; let mut time = Instant::now();; let mut last_frame_time = time.duration_since_epoch(); let mut fps = 0; let mut frame_count = 0; loop { frame_count+=1; // 更新UI状态 slint::platform::update_timers_and_animations(); // 渲染UI window.draw_if_needed(|renderer| { renderer.render_by_line(FrameBuffer{ frame_buffer: &mut the_frame_buffer, stride: display_width,display:&mut display }); }); // 计算FPS let now = time.duration_since_epoch(); if now.as_millis()-last_frame_time.as_millis() >= 1000{ last_frame_time = now; fps = frame_count; frame_count = 0; ui.global::<System>().set_fps(fps); } // 根据动画状态决定是否延迟 if !window.has_active_animations() { if let Some(duration) = slint::platform::duration_until_next_timer_update(){ Timer::after_millis(duration.as_millis() as u64).await; continue; } } Timer::after_millis(10).await; } } // 平台实现结构体 struct EspPlatform{ window: Rc<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 DISPLAY } 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 buf = &mut self.frame_buffer[range.clone()]; render_fn(buf); self.display.set_pixels( range.start as u16, line as _, range.end as u16, line as u16, buf.iter().map(|x|{ embedded_graphics_core::pixelcolor::raw::RawU16::new(x.0).into() }) ).unwrap() } } 效果展示浏览首页页面设置页面最终的效果展示
2025年05月10日
312 阅读
2 评论
1 点赞
2025-05-09
ESP32C3驱动TFT屏幕
因为使用的是新版esp-generate创建的项目,很多api的使用都大改了,网上能搜到的类似的教程也都完全没法用,这个教程是使用最新版esp-hal基于rust开发esp32c3驱动st7789屏幕。看网上有些项目叫MOSI,MISO的,也有叫SDI,SDO,这两个本质就是一个东西,在不同协议叫法不同。SPI 协议中大家都喜欢叫 MOSI / MISO,有些芯片厂商文档上写的是 SDI / SDO;MOSI == SDI(对于从机):表示“数据输入”MISO == SDO(对于从机):表示“数据输出”项目创建在 rust裸机开发esp32c3 的基础上,先新建一个最基础的环境引入依赖[dependencies] critical-section = "1.2.0" esp-hal = { version = "1.0.0-beta.0", features = ["esp32c3","unstable"] } esp-println = { version = "0.13.1",features = ["esp32c3","log"] } log = "0.4.27" # 图形相关 embedded-hal = "1.0.0" embedded-graphics-core = "0.4.0" embedded-graphics = "0.8.1" embedded-graphics-framebuf = "0.5.0" display-interface = "0.5.0" display-interface-spi = "0.5.0" mipidsi = "0.9.0" embedded-hal-bus = "0.3.0" 编写main.rs代码#![no_std] #![no_main] use embedded_graphics::Drawable; use embedded_graphics::image::{Image, ImageRawLE}; use embedded_graphics::pixelcolor::Rgb565; use embedded_graphics::prelude::{DrawTarget, Point, Primitive, RgbColor}; use embedded_graphics::primitives::PrimitiveStyle; use embedded_graphics_core::prelude::Size; use embedded_graphics_core::primitives::Rectangle; use embedded_hal::delay::DelayNs; use embedded_hal::digital::ErrorType; use embedded_hal_bus::spi::ExclusiveDevice; use esp_hal::clock::CpuClock; use esp_hal::delay::Delay; use esp_hal::gpio::{Level, Output, OutputConfig}; use esp_hal::main; use esp_hal::spi::master::{Config, Spi}; use esp_hal::spi::Mode; use esp_hal::time::{Duration, Instant, Rate}; use esp_println::logger::init_logger; use esp_println::println; use log::{error, LevelFilter}; use mipidsi::_mock::MockSpi; use mipidsi::models::ST7789; use mipidsi::Builder; use mipidsi::interface::SpiInterface; use mipidsi::options::Orientation; #[panic_handler] fn panic(e: &core::panic::PanicInfo) -> ! { println!("{}",e); loop { } } #[main] fn main() -> ! { // generator version: 0.3.1 let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); init_logger(LevelFilter::Debug); let peripherals = esp_hal::init(config); let mut delay = Delay::new(); let mut rst = Output::new(peripherals.GPIO3,Level::Low,OutputConfig::default()); delay.delay_ms(20); // 保持 RST 为低电平至少 10~20ms rst.set_high(); // 然后再拉高 delay.delay_ms(100); // 拉高后也延迟一下,再继续初始化 let dc = Output::new(peripherals.GPIO4,Level::Low,OutputConfig::default()); let mut backlight = Output::new(peripherals.GPIO5,Level::Low,OutputConfig::default()); let sclk = peripherals.GPIO6; let sda = peripherals.GPIO7; let sdi = peripherals.GPIO8; let cs = peripherals.GPIO10; let spi = Spi::new(peripherals.SPI2, Config::default() .with_mode(Mode::_3) .with_frequency(Rate::from_mhz(40)) ).unwrap() .with_sck(sclk) .with_miso(sdi) .with_mosi(sda); let cs_output = Output::new(cs,Level::High,OutputConfig::default()); let spi_device = ExclusiveDevice::new_no_delay(spi,cs_output).unwrap(); let mut buffer = [0_u8; 512]; let di = SpiInterface::new(spi_device,dc,&mut buffer); let mut display = Builder::new(ST7789,di) .display_size(240,240) .orientation(Orientation::new()) .reset_pin(rst) .init(&mut delay).unwrap(); println!("display 初始化完成"); backlight.set_high(); let raw_image_data = ImageRawLE::new(include_bytes!("../../assets/ferris.raw"), 86); let mut x = 0; loop { display.clear(Rgb565::BLACK).unwrap(); let ferris = Image::new(&raw_image_data, Point::new(x, 0)); // draw image on black background display.clear(Rgb565::BLACK).unwrap(); ferris.draw(&mut display).unwrap(); delay.delay_millis(400); x+=10; if x >= 140{ x = 0; } } // for inspiration have a look at the examples at https://github.com/esp-rs/esp-hal/tree/esp-hal-v1.0.0-beta.0/examples/src/bin } 效果展示{callout color="#efb44d"}屏幕是倒着放到,图片显示是正的{/callout}{lamp/}本项目参考了mipidsi(esp32c3) esp-idf-hal/examples /spi_st7789.rs
2025年05月09日
309 阅读
0 评论
1 点赞
2025-05-05
ESP32C3使用Slint渲染OLED屏幕
前言:说一下为什么要用slint,既然都选择用rust开发嵌入式了避免不了要折腾,也是为后续驱动st7789做铺垫吧。而其实对应OLED屏幕已经有一个很好用的库了embedded-graphics,几乎能满足OLED屏幕的全部需求了,但是slint有动画,定时器,声明式,这些优势,再嵌入式开发再合适不过了。在写这个的时候也遇到不少坑,所以记录一下吧创建项目引入库编译问题已经在 ESP32C3引入Slint 文章解决了先创建一个最基础的esp32c3的项目然后再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页面一致
2025年05月05日
310 阅读
0 评论
0 点赞
1
2
3
...
24