分享网络资源｜经验教程｜网站源码模板
项目背景与需求在Flutter开发中，我们经常遇到需要实现多Tab栏列表的场景，每个Tab具有相似的结构（下拉刷新、上拉加载、列表展示），但数据类型和UI展示各不相同。本文将介绍如何使用模板方法模式来优雅地解决这个问题。模板方法模式简介模板方法模式是一种行为设计模式，它在超类中定义了一个算法的框架，允许子类在不修改结构的情况下重写特定步骤。在我们的场景中，每个Tab的加载流程（初始化→刷新→加载更多）是相同的，但具体的数据加载和UI展示是不同的。代码实现解析1. 抽象基类定义const int defaultPageSize = 10; abstract class TabData<T> { final String name; // tab名称 int total; // 数据数量 bool isSelected; // 选中状态 bool hasMore; // 是否有更多数据 List<T> items; // 数据列表 bool _isInited = false; // 是否初始化完成 final RefreshController controller; final Widget Function(BuildContext context,T item) itemBuilder; TabData({ required this.name, required this.itemBuilder, this.total = 0, this.isSelected = false, this.hasMore = true, this.items = const [] }) : controller = RefreshController(); int get pageSize => defaultPageSize; Future<List<T>> loadData(int page); void updateItems(List<T> newItems, bool isRefresh); Future<bool> init() async{ if (_isInited) { return true; } _isInited = true; await onRefresh(); return false; } Future<void> onRefresh() async { try { final data = await loadData(1); updateItems(data, true); hasMore = data.length >= pageSize; controller.refreshCompleted(); } catch (e) { controller.refreshFailed(); rethrow; } } Future<void> onLoadMore() async { if (!hasMore) { controller.loadNoData(); return; } try { final currentPage = (items.length / pageSize).ceil() + 1; final data = await loadData(currentPage); updateItems(data, false); hasMore = data.length >= pageSize; controller.loadComplete(); } catch (e) { controller.refreshFailed(); rethrow; } } void dispose() { controller.dispose(); } }设计要点：定义了完整的生命周期方法（init、onRefresh、onLoadMore）使用泛型T支持不同的数据类型封装了刷新控制器和分页逻辑提供itemBuilder回调实现UI定制化2. 具体子类实现点赞Tab实现：class LikeTabData extends TabData<LikeTabDataEntity>{ LikeTabData():super(name: "点赞",itemBuilder: (context,item){ return Text(item.name); }); @override Future<List<LikeTabDataEntity>> loadData(int page) async{ await Future.delayed(const Duration(seconds: 1)); return List.generate(pageSize, (index)=> LikeTabDataEntity(name: "$name 数据 ${page * pageSize + index}")); } @override void updateItems(List<LikeTabDataEntity> newItems, bool isRefresh) { items = isRefresh ? newItems : [...items, ...newItems]; total = items.length; } }评论Tab实现：class CommentTabData extends TabData<CommentTabDataEntity>{ CommentTabData():super(name: "评论",itemBuilder: (context,item) { Widget text () => Text(item.text,style: const TextStyle(color: Colors.white)); return Container( margin: const EdgeInsets.all(8), padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.blueAccent, borderRadius: BorderRadius.circular(8), ), child: text(), ); }); @override Future<List<CommentTabDataEntity>> loadData(int page) async{ await Future.delayed(const Duration(seconds: 1)); return List.generate(pageSize, (index)=> CommentTabDataEntity(text: "$name 数据 ${page * pageSize + index}")); } @override void updateItems(List<CommentTabDataEntity> newItems, bool isRefresh) { items = isRefresh ? newItems : [...items, ...newItems]; total = items.length; } }子类只需实现：loadData：具体的数据加载逻辑updateItems：数据合并策略itemBuilder：列表项UI构建3. 状态管理class DialogState extends BaseViewState { List<TabData> tabDataList = [ LikeTabData(), CommentTabData(), LikeTabData(), CommentTabData(), ]; }4. 逻辑控制器class DialogLogic extends BaseGetXLifeCycleController{ late final PageController tabController; final state = DialogState(); @override void onInit() { super.onInit(); tabController = PageController(); state.viewState = TYPE_LOAD; update(); state.tabDataList.first.init().then((_)=> update()); Future.delayed(const Duration(seconds: 1),(){ state.tabDataList.first.isSelected = true; state.viewState = TYPE_NORMAL; update(); }); } @override void onClose() { tabController.dispose(); for(final item in state.tabDataList) { item.dispose(); } super.onClose(); } void onTabChange(int index) async{ for(int i = 0; i < state.tabDataList.length; i++) { state.tabDataList[i].isSelected = index == i; } update(); final isInited = await state.tabDataList[index].init(); if(!isInited) update(); } void changeTab(int index,BuildContext context){ final screenWidth = MediaQuery.of(context).size.width; tabController.animateTo(index * screenWidth,duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); } void onRefresh(Future<void> Function() refresh) async{ await refresh(); update(); } void onLoadMore(Future<void> Function() loadMore) async{ await loadMore(); update(); } }5. UI界面实现class Dialog extends BaseTitleBottomSheet with GetXControllerMixin<DialogLogic>{ Dialog({super.key}); final logic = Get.put(DialogLogic()); final state = Get.find<DialogLogic>().state; @override String getTitle() => "Dialog"; @override Widget buildContent() { return buildPartBaseView( state.viewState,context, _buildContentWidget() ); } Widget _buildContentWidget() { return Expanded( child: Column( children: [ SingleChildScrollView( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ for(int i = 0;i<state.tabDataList.length;i++) GestureDetector(onTap: ()=> logic.changeTab(i,context),child: _buildTabWidget(state.tabDataList[i])) ], ), ), Expanded( child: PageView.builder( controller: logic.tabController, onPageChanged: logic.onTabChange, itemCount: state.tabDataList.length, itemBuilder: (_,index){ final data = state.tabDataList[index]; if(data.items.isEmpty){ return const Center(child: Text("无数据"),); } return _KeepAliveList( data, keepAlive: true, onLoading: ()=> logic.onLoadMore(data.onLoadMore), onRefresh: ()=> logic.onRefresh(data.onRefresh), controller: data.controller ); }, ), ), ], ), ); } Widget _buildTabWidget(TabData entity){ final noSelectedStyle = TextStyle(color: Colors.black,fontSize: 23.w); final selectedStyle = TextStyle(color: Colors.blueAccent,fontSize: 23.w); final style = entity.isSelected ? selectedStyle : noSelectedStyle; return Row( mainAxisSize: MainAxisSize.min, children: [ Text(entity.name,style: style), Text("（${entity.total}）",style: style), ], ); } } class _KeepAliveList extends StatefulWidget { final TabData tabData; final bool keepAlive; final VoidCallback? onRefresh; final VoidCallback? onLoading; final RefreshController controller; const _KeepAliveList(this.tabData,{super.key,this.keepAlive = true,required this.controller,this.onRefresh,this.onLoading}); @override State<_KeepAliveList> createState() => _KeepAliveListState(); } class _KeepAliveListState extends State<_KeepAliveList> with AutomaticKeepAliveClientMixin{ @override Widget build(BuildContext context) { super.build(context); return SmartRefresher( controller: widget.controller, enablePullDown: true, enablePullUp: true, header: const ClassicHeader(), onRefresh: widget.onRefresh, onLoading: widget.onLoading, child: ListView.builder( itemCount: widget.tabData.items.length, itemBuilder: (context,index){ // 新版本dart可以使用switch匹配模式，可以穷举完全部密封类的子类 // 为了兼容只能使用if了 final tabData = widget.tabData; if(tabData is LikeTabData){ return tabData.itemBuilder(context,tabData.items[index]); }else if(tabData is CommentTabData){ return tabData.itemBuilder(context,tabData.items[index]); } throw "未知的TabData类型"; }, ), ); } @override bool get wantKeepAlive => widget.keepAlive; }设计优势1. 代码复用性公共的刷新、加载、分页逻辑在基类中实现新增Tab只需继承TabData并实现三个抽象方法2. 可维护性算法框架稳定，修改基类即可影响所有子类业务逻辑与UI展示分离3. 扩展性支持任意类型的列表数据可轻松添加新的Tab类型4. 一致性所有Tab保持相同的交互逻辑统一的错误处理和加载状态使用建议添加新Tab：只需创建新的TabData子类，实现loadData、updateItems和itemBuilder修改公共逻辑：在TabData基类中修改，所有子类自动生效自定义分页大小：在子类中重写pageSize getter特殊处理：子类可重写onRefresh或onLoadMore方法进行个性化处理总结通过模板方法模式，我们成功地将多Tab列表中的公共逻辑提取到基类中，同时保留了子类的灵活性。这种设计模式特别适用于具有相似流程但不同实现的场景，能够显著减少代码重复，提高项目的可维护性和扩展性。
我们通过 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 对话、实时日志等场景。
引言在 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、约束依赖型组件
这个教程算是在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() } } 效果展示浏览首页页面设置页面最终的效果展示
因为使用的是新版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
前言：说一下为什么要用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页面一致
为什么要折腾这个？想在 ESP32-C3 开发板上实现一个酷炫的 UI 界面，选了 Rust 生态的 Slint 框架。结果刚加上依赖就报错：error: 不能同时启用 critical-section 和 portable_atomic_unsafe_assume_single_core这两个特性不能同时存在解决问题过程第一步：创建最小测试项目# 生成纯净的 ESP32-C3 项目骨架 esp-generate --chip=esp32c3 slint_demo此时项目仅包含最基础的硬件驱动，编译一次成功：cargo build Compiling slint_demo v0.1.0 Finished dev [optimized] target(s)第二步：引入 Slint 引爆问题按官方文档添加依赖：cargo add slint@1.10.0 --no-default-features --features "compat-1-2 unsafe-single-threaded libm renderer-software"再次编译立刻报错：error: critical-section 与 portable_atomic_unsafe_assume_single_core 冲突第三步：定位问题注释 Slint 保留 ESP-HAL → 编译成功注释 ESP-HAL 保留 Slint → 依然报错Slint 的依赖树里藏了冲突的组件第四步：解决问题搜索 slint critical-section site:github.com锁定关键讨论：Issue #5057发现修复补丁：Commit 29168bc核心修改点：# Slint 内部配置调整 - portable-atomic = { features = ["critical-section"] } + portable-atomic = {} # 解除强制依赖最终配置方案关键依赖声明 (Cargo.toml)[dependencies] # 硬件抽象层 esp-hal = { version = "1.0.0-beta.0", default-features = false, features = ["esp32c3"] # 注意关闭默认特性 } # UI 框架 [dependencies.slint] git = "https://github.com/slint-ui/slint" rev = "29168bc89270798f6075a0a729c14a3f011ceb4f" # 锁定修复版本 default-features = false features = [ "compat-1-2", # 兼容旧版 API "unsafe-single-threaded", # 单核模式 "libm", # 数学库支持 "renderer-software" # 软件渲染 ] # 内存分配器 esp-alloc = "0.7.0"内存管理配置 (main.rs)// 初始化堆内存（72KB 用于 UI） #[main] fn main() -> ! { let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); let _peripherals = esp_hal::init(config); esp_alloc::heap_allocator!(size: 72 * 1024); }经验总结依赖管理铁律用 cargo tree -e features 查看依赖关系通过 default-features = false 精准控制特性版本锁定技巧使用 rev = "commit_hash" 锁定特定修复版本定期检查 GitHub Issues 跟踪问题进展
初始化项目参考 项目创建 新建一个基于std的新项目在Cargo.toml添加必要的依赖[package] name = "esp32c3-ble-demo" version = "0.1.0" authors = ["suxii <m@suxii.cn>"] edition = "2021" resolver = "2" rust-version = "1.77" bin name = "esp32c3-ble-demo" harness = false # do not use the built in cargo test harness -> resolve rust-analyzer errors [features] default = [] experimental = ["esp-idf-svc/experimental"] [dependencies] log = "0.4" esp-idf-svc = { version = "0.51", features = ["critical-section", "embassy-time-driver", "embassy-sync","experimental","std"] } futures = "0.3.31" anyhow = "1.0.98" enumset = "1.1.5" heapless = "0.8.0" esp-idf-sys = "0.36.1" esp-idf-hal = "0.45.2" # 处理序列化，反序列化 serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" [build-dependencies] embuild = "0.33" 开启BLE功能将sdkconfig.defaults 文件替换成# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000 CONFIG_BT_BLUEDROID_ENABLED=y CONFIG_BT_ENABLED=y CONFIG_BT_BLE_ENABLED=y CONFIG_BT_GATTS_ENABLE=y CONFIG_BT_BLE_SMP_ENABLE=y CONFIG_BT_GATTS_SEND_SERVICE_CHANGE_MODE=y CONFIG_BT_BTC_TASK_STACK_SIZE=7000 CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y CONFIG_BT_CTRL_BLE_MAX_ACT=10 CONFIG_BT_CTRL_BLE_MAX_ACT_EFF=10 CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=0 CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_SUPP=y CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=100 CONFIG_BT_CTRL_BLE_ADV_REPORT_DISCARD_THRSHOLD=20 CONFIG_BT_CTRL_BLE_SCAN_DUPL=y # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). # This allows to use 1 ms granuality for thread sleeps (10 ms by default). #CONFIG_FREERTOS_HZ=1000 # Workaround for https://github.com/espressif/esp-idf/issues/7631 #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n并且在同级目录新建一个 sdkconfig.esp32c3文件# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K) CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000 CONFIG_BT_ENABLED=y CONFIG_BT_BLE_ENABLED=y CONFIG_BT_GATTS_ENABLE=y CONFIG_BT_GATTS_SEND_SERVICE_CHANGE_MODE=y CONFIG_BT_BTC_TASK_STACK_SIZE=7000 CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n CONFIG_BTDM_CTRL_MODE_BTDM=n # Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default). # This allows to use 1 ms granuality for thread sleeps (10 ms by default). #CONFIG_FREERTOS_HZ=1000 # Workaround for https://github.com/espressif/esp-idf/issues/7631 #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n #CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n代码编写新建一个ble_server.rs文件，用来管理BLE use std::sync::{mpsc, Arc, Condvar, Mutex}; use std::thread; use enumset::enum_set; use esp_idf_hal::ledc::{LedcDriver, LedcTimerDriver, Resolution}; use esp_idf_hal::modem; use esp_idf_svc::bt::ble::gap::{AdvConfiguration, BleGapEvent, EspBleGap}; use esp_idf_svc::bt::ble::gatt::server::{ConnectionId, EspGatts, GattsEvent, TransferId}; use esp_idf_svc::bt::ble::gatt::{ AutoResponse, GattCharacteristic, GattDescriptor, GattId, GattInterface, GattResponse, GattServiceId, GattStatus, Handle, Permission, Property, }; use esp_idf_svc::bt::{BdAddr, Ble, BtDriver, BtStatus, BtUuid}; use esp_idf_svc::hal::delay::FreeRtos; use esp_idf_svc::nvs::{ EspNvsPartition, NvsDefault}; use esp_idf_svc::sys::{EspError, ESP_FAIL}; use log::{error, info, warn}; use crate::ble_recv_message; use crate::message::Message; pub fn start( modem: modem::Modem, nvs_clone:EspNvsPartition<NvsDefault> ) -> anyhow::Result<()> { // nvs（Non-Volatile Storage，非易失性存储）是持久化存储，保证断电数据不丢失 // 创建ble蓝牙驱动 let bt = Arc::new(BtDriver::new(modem, Some(nvs_clone))?); // 自定义一个蓝牙集合服务，创建gap，跟gatts let server = ExampleServer::new( Arc::new(EspBleGap::new(bt.clone())?), Arc::new(EspGatts::new(bt.clone())?), ); info!("BLE Gap and Gatts 初始化完成"); let gap_server = server.clone(); // 处理gap事件回调吗，gap在连接阶段参与，不参与内容传输 server.gap.subscribe(move |event| { gap_server.check_esp_status(gap_server.on_gap_event(event)); })?; let gatts_server = server.clone(); // gap处理完成，说明连接已经建立了，现在开始处理gatt，也就是数据通信的部分 server.gatts.subscribe(move |(gatt_if, event)| { gatts_server.check_esp_status(gatts_server.on_gatts_event(gatt_if, event)) })?; info!("BLE Gap and Gatts 订阅事件初始化完成"); server.gatts.register_app(APP_ID)?; info!("Gatts BTP app 完成注册"); // 新线程处理发送消息 let _ = thread::Builder::new() .stack_size(2000) .spawn(move||{ let mut ind_data = 0_u16; loop { server.indicate(&ind_data.to_le_bytes()).unwrap(); info!("广播给客户端的信息为: {ind_data}"); ind_data = ind_data.wrapping_add(1); FreeRtos::delay_ms(10000); } }); anyhow::Ok(()) } const APP_ID: u16 = 0; const MAX_CONNECTIONS: usize = 2; // Our service UUID pub const SERVICE_UUID: u128 = 0xad91b201734740479e173bed82d75f9d; /// Our "recv" characteristic - i.e. where clients can send data. pub const RECV_CHARACTERISTIC_UUID: u128 = 0xb6fccb5087be44f3ae22f85485ea42c4; /// Our "indicate" characteristic - i.e. where clients can receive data if they subscribe to it pub const IND_CHARACTERISTIC_UUID: u128 = 0x503de214868246c4828fd59144da41be; type ExBtDriver = BtDriver<'static, Ble>; type ExEspBleGap = Arc<EspBleGap<'static, Ble, Arc<ExBtDriver>>>; type ExEspGatts = Arc<EspGatts<'static, Ble, Arc<ExBtDriver>>>; #[derive(Debug, Clone)] struct Connection { peer: BdAddr, conn_id: Handle, subscribed: bool, mtu: Option<u16>, } #[derive(Default)] struct State { gatt_if: Option<GattInterface>, service_handle: Option<Handle>, recv_handle: Option<Handle>, ind_handle: Option<Handle>, ind_cccd_handle: Option<Handle>, connections: heapless::Vec<Connection, MAX_CONNECTIONS>, response: GattResponse, ind_confirmed: Option<BdAddr>, } #[derive(Clone)] pub struct ExampleServer { gap: ExEspBleGap, gatts: ExEspGatts, state: Arc<Mutex<State>>, condvar: Arc<Condvar>, } impl ExampleServer { pub fn new(gap: ExEspBleGap, gatts: ExEspGatts) -> Self { Self { gap, gatts, state: Arc::new(Mutex::new(Default::default())), condvar: Arc::new(Condvar::new()), } } } impl ExampleServer { // 发送indicate消息 fn indicate(&self, data: &[u8]) -> Result<(), EspError> { for peer_index in 0..MAX_CONNECTIONS { // 发送数据给全部已经订阅的服务 let mut state = self.state.lock().unwrap(); loop { if state.connections.len() <= peer_index { // 表示已经向全部订阅的客户发送了消息 break; } let Some(gatt_if) = state.gatt_if else { // 丢失了gatt的接口连接 break; }; let Some(ind_handle) = state.ind_handle else { // 丢失了handle break; }; // confirmed翻译为 证实 // 检查状态：确保上次的指示数据已被客户端确认接收。 // 发送数据：如果状态允许，则向指定客户端发送新的指示数据。 // 更新状态：记录当前客户端已接收到指示数据。 // 等待条件：如果状态不允许发送数据，则等待其他线程的通知。 if state.ind_confirmed.is_none() { let conn = &state.connections[peer_index]; self.gatts .indicate(gatt_if, conn.conn_id, ind_handle, data)?; state.ind_confirmed = Some(conn.peer); let conn = &state.connections[peer_index]; info!("Indicated数据发送给客户端 {}", conn.peer); break; } else { state = self.condvar.wait(state).unwrap(); } } } Ok(()) } /// 处理客户端订阅 fn on_subscribed(&self, addr: BdAddr) { // todo 客户端订阅 warn!("客户端 {addr} 订阅了"); } /// 处理客户端取消订阅 fn on_unsubscribed(&self, addr: BdAddr) { // todo 客户肯定取消订阅 warn!("客户端 {addr} 取消订阅"); } /// 接收消息回调 fn on_recv(&self, addr: BdAddr, data: &[u8], offset: u16, mtu: Option<u16>) { // todo 这里写接收消息的逻辑 // 按大端解析 // let res = u16::from_be_bytes([data[0],data[1]]); let res = String::from_utf8_lossy(data); if let Err(e) = ble_recv_message::handle_message(&res){ error!("处理消息错误：{e}") } warn!("接收消息来自客户端 {addr}: {data:?}, offset: {offset}, mtu: {mtu:?} text:{res}"); } /// GAP 事件的主事件处理程序 fn on_gap_event(&self, event: BleGapEvent) -> Result<(), EspError> { info!("得到gap事件: {event:?}"); // ble广播配置 Advertising：广告，广播 if let BleGapEvent::AdvertisingConfigured(status) = event { // 从gap事件中，结构出BtStatus状态，然后检查bt的状态 // 如果bt连接状态检查不通过，就直接往上级抛异常 self.check_bt_status(status)?; // gap处理完成，开启广播 self.gap.start_advertising()?; } Ok(()) } /// GATTS 事件的主事件处理程序 fn on_gatts_event( &self, gatt_if: GattInterface, event: GattsEvent, ) -> Result<(), EspError> { info!("得到gatts事件消息: {event:?}"); match event { // 处理服务注册，注册成功后，创建服务 GattsEvent::ServiceRegistered { status, app_id } => { self.check_gatt_status(status)?; if APP_ID == app_id { self.create_service(gatt_if)?; } } // 处理服务创建完成后的事件，配置gatts的服务 GattsEvent::ServiceCreated { status, service_handle, .. } => { self.check_gatt_status(status)?; self.configure_and_start_service(service_handle)?; } // 处理特征添加 GattsEvent::CharacteristicAdded { status, attr_handle, service_handle, char_uuid, } => { self.check_gatt_status(status)?; self.register_characteristic(service_handle, attr_handle, char_uuid)?; } // 添加描述符 GattsEvent::DescriptorAdded { status, attr_handle, service_handle, descr_uuid, } => { self.check_gatt_status(status)?; self.register_cccd_descriptor(service_handle, attr_handle, descr_uuid)?; } // 删除服务 GattsEvent::ServiceDeleted { status, service_handle, } => { self.check_gatt_status(status)?; self.delete_service(service_handle)?; } // 注销服务 GattsEvent::ServiceUnregistered { status, service_handle, .. } => { self.check_gatt_status(status)?; self.unregister_service(service_handle)?; } // 注册或更新其 MTU（Maximum Transmission Unit 最大传输单元）值 GattsEvent::Mtu { conn_id, mtu } => { self.register_conn_mtu(conn_id, mtu)?; } // 处理连接的客户端 GattsEvent::PeerConnected { conn_id, addr, .. } => { self.create_conn(conn_id, addr)?; } // 处理客户端断开连接 GattsEvent::PeerDisconnected { addr, .. } => { self.delete_conn(addr)?; // todo 开启广播 self.gap.start_advertising()?; } GattsEvent::Write { conn_id, trans_id, addr, handle, offset, need_rsp, is_prep, value, } => { let handled = self.recv( gatt_if, conn_id, trans_id, addr, handle, offset, need_rsp, is_prep, value, )?; // 值为true，表示是客户端的操作，例如客户端注册，注销，发送消息 if handled { self.send_write_response( gatt_if, conn_id, trans_id, handle, offset, need_rsp, is_prep, value, )?; } } GattsEvent::Confirm { status, .. } => { self.check_gatt_status(status)?; self.confirm_indication()?; } _ => (), } Ok(()) } /// 创建服务并开始广播， /// 在通知我们 GATTS 应用程序已注册后，从事件回调中调 fn create_service(&self, gatt_if: GattInterface) -> Result<(), EspError> { // 初始化结构体的 GattInterface self.state.lock().unwrap().gatt_if = Some(gatt_if); // 设置esp32设备的名字 self.gap.set_device_name("ESP32")?; // flag 标志位是一个位掩码，具体含义可以参考 Bluetooth SIG 定义的标准。 // 常见的标志位组合： // 0x01：LE Limited Discoverable Mode（有限可发现模式）。 // 0x02：LE General Discoverable Mode（通用可发现模式）。 // 0x04：BR/EDR Not Supported（不支持经典蓝牙）。 // 0x18：LE and BR/EDR Controller（同时支持 LE 和经典蓝牙）。 // 在代码中，flag: 2 表示设备处于通用可发现模式（LE General Discoverable Mode）。 self.gap.set_adv_conf(&AdvConfiguration { include_name: true, // 指示是否在广播数据中包含设备名称 include_txpower: true, // 指示是否在广播数据中包含设备的发射功率 flag: 2, service_uuid: Some(BtUuid::uuid128(SERVICE_UUID)), // 指定设备提供的服务 UUID // service_data: todo!(), // 用于广播与特定服务相关的数据 // manufacturer_data: todo!(), // 用于广播厂商自定义的数据 ..Default::default() })?; self.gatts.create_service( gatt_if, &GattServiceId { id: GattId { uuid: BtUuid::uuid128(SERVICE_UUID), inst_id: 0, // 指定服务的实例Id 如果设备支持多个相同 UUID 的服务实例，inst_id 用于区分它们 }, is_primary: true, // 指示服务是否为主服务 }, 8, )?; Ok(()) } /// 删除服务 fn delete_service(&self, service_handle: Handle) -> Result<(), EspError> { let mut state = self.state.lock().unwrap(); if state.service_handle == Some(service_handle) { state.recv_handle = None; state.ind_handle = None; state.ind_cccd_handle = None; } Ok(()) } /// 取消注册服务 fn unregister_service(&self, service_handle: Handle) -> Result<(), EspError> { let mut state = self.state.lock().unwrap(); if state.service_handle == Some(service_handle) { state.gatt_if = None; state.service_handle = None; } Ok(()) } /// 配置并且启动服务 /// 在通知我们创建服务后从事件回调中调用 fn configure_and_start_service(&self, service_handle: Handle) -> Result<(), EspError> { // 初始化service_handle服务 self.state.lock().unwrap().service_handle = Some(service_handle); // 开启gatts服务 // pub type Handle = u16; 由于u16是基本类型，实现了copy，所以下面两个函数都能使用，本质是copy了一份数据 self.gatts.start_service(service_handle)?; // 添加特性 self.add_characteristics(service_handle)?; Ok(()) } /// 添加注册的两个uuid特性到服务中 /// 在通知我们创建服务后从事件回调中调用 fn add_characteristics(&self, service_handle: Handle) -> Result<(), EspError> { // permissions 定义了客户端对该特征的操作权限。 // 在这里，Permission::Write 表示客户端可以写入该特征的值。 // 其他常见的权限包括： // Permission::Read：允许读取特征值。 // Permission::Notify：允许服务器主动通知客户端特征值的变化。 // Permission::Indicate：类似于通知，但需要客户端确认。 // properties 定义了特征的属性（Properties），描述了该特征支持的操作类型。 // 在这里，Property::Write 表示客户端可以通过写操作更新该特征的值。 // 其他常见的属性包括： // Property::Read：允许客户端读取特征值。 // Property::Notify：允许服务器发送通知。 // Property::Indicate：允许服务器发送指示 self.gatts.add_characteristic( service_handle, // 服务的唯一标识 &GattCharacteristic { uuid: BtUuid::uuid128(RECV_CHARACTERISTIC_UUID), permissions: enum_set!(Permission::Write), properties: enum_set!(Property::Write), max_len: 200, // 定义了特征值的最大长度（以字节为单位） auto_rsp: AutoResponse::ByApp, // 表示应用程序需要手动响应客户端的请求 }, &[], // 表示该特征的描述符（Descriptors）列表，&[] 表示没有附加任何描述符 )?; self.gatts.add_characteristic( service_handle, &GattCharacteristic { uuid: BtUuid::uuid128(IND_CHARACTERISTIC_UUID), permissions: enum_set!(Permission::Write | Permission::Read), properties: enum_set!(Property::Indicate), max_len: 200, // Mac iondicate data auto_rsp: AutoResponse::ByApp, }, &[], )?; Ok(()) } /// 添加 CCCD 描述 /// 只添加特征indicate fn register_characteristic( &self, service_handle: Handle, attr_handle: Handle, char_uuid: BtUuid, ) -> Result<(), EspError> { // 在找esp主动推送消息给客户端的uuid服务 let indicate_char = { let mut state = self.state.lock().unwrap(); if state.service_handle != Some(service_handle) { false } else if char_uuid == BtUuid::uuid128(RECV_CHARACTERISTIC_UUID) { state.recv_handle = Some(attr_handle); false } else if char_uuid == BtUuid::uuid128(IND_CHARACTERISTIC_UUID) { state.ind_handle = Some(attr_handle); true } else { false } }; if indicate_char { self.gatts.add_descriptor( service_handle, &GattDescriptor { uuid: BtUuid::uuid16(0x2902), // CCCD 通常用于控制特征的通知（Notify）和指示（Indicate）功能，允许客户端订阅或取消订阅特征值的变化 permissions: enum_set!(Permission::Read | Permission::Write), }, )?; } Ok(()) } /// 注册 CCCD 描述符 /// 在通知我们添加了描述符后，从事件回调中调用 /// 这段代码的作用是注册一个 CCCD（Client Characteristic Configuration Descriptor）描述符，并将其相关信息存储到内部状态中。具体功能如下： /// 验证描述符的 UUID 是否为标准的 CCCD UUID（0x2902）。 /// 确认描述符所属的服务句柄是否与内部记录的服务句柄一致。 /// 如果条件满足，则将描述符的属性句柄（attr_handle）存储到内部状态中，以便后续用于处理通知或指示功能。 /// 这种设计通常用于 BLE（蓝牙低功耗）设备的 GATT 服务实现中，确保 CCCD 的句柄被正确记录，从而支持客户端订阅或取消订阅特征值的变化 fn register_cccd_descriptor( &self, service_handle: Handle, attr_handle: Handle, descr_uuid: BtUuid, ) -> Result<(), EspError> { let mut state = self.state.lock().unwrap(); if descr_uuid == BtUuid::uuid16(0x2902) // CCCD UUID && state.service_handle == Some(service_handle) { state.ind_cccd_handle = Some(attr_handle); } Ok(()) } /// 从客户端接收消息 fn register_conn_mtu(&self, conn_id: ConnectionId, mtu: u16) -> Result<(), EspError> { let mut state = self.state.lock().unwrap(); // 找到目标客户端 if let Some(conn) = state .connections .iter_mut() .find(|conn| conn.conn_id == conn_id) { // 并且设置最大传输单元的值，字节 conn.mtu = Some(mtu); } Ok(()) } /// 创建一个新的连接 fn create_conn(&self, conn_id: ConnectionId, addr: BdAddr) -> Result<(), EspError> { // 创建指定大小的连接，如果超过，新的连接将不再处理 let added = { let mut state = self.state.lock().unwrap(); if state.connections.len() < MAX_CONNECTIONS { state .connections .push(Connection { peer: addr, conn_id, subscribed: false, mtu: None, }) .map_err(|_| ()) .unwrap(); true } else { false } }; if added { // 如果已经添加，则配置gap参数 // addr：表示目标设备的蓝牙地址（BdAddr），用于标识要配置连接参数的远程设备。 // min_int_ms：表示连接间隔的最小值（单位为毫秒）。 // max_int_ms：表示连接间隔的最大值（单位为毫秒）。 // latency_ms：表示连接延迟（单位为毫秒）。 // timeout_ms：表示超时时间（单位为毫秒）。 self.gap.set_conn_params_conf(addr, 10, 20, 0, 400)?; } Ok(()) } /// 删除一个连接 fn delete_conn(&self, addr: BdAddr) -> Result<(), EspError> { let mut state = self.state.lock().unwrap(); // Connection { peer, .. }：这是结构体解构语法，用于从 Connection 结构体中提取字段 peer if let Some(index) = state .connections .iter() .position(|Connection { peer, .. }| *peer == addr) { // remove： // 从向量中移除指定索引位置的元素。 // 将该索引之后的所有元素向前移动一位，以保持顺序。 // 时间复杂度为 O(n)，因为可能需要移动多个元素。 // swap_remove： // 从向量中移除指定索引位置的元素。 // 使用最后一个元素填补空缺，不保证顺序。 // 时间复杂度为 O(1)，因为只涉及少量操作。 state.connections.swap_remove(index); } Ok(()) } /// 一个辅助方法，用于处理向我们发送数据到 “recv” 特征的客户端 #[allow(clippy::too_many_arguments)] fn recv( &self, _gatt_if: GattInterface, conn_id: ConnectionId, _trans_id: TransferId, addr: BdAddr, handle: Handle, offset: u16, _need_rsp: bool, _is_prep: bool, value: &[u8], ) -> Result<bool, EspError> { let mut state = self.state.lock().unwrap(); let recv_handle = state.recv_handle; let ind_cccd_handle = state.ind_cccd_handle; let Some(conn) = state .connections .iter_mut() .find(|conn| conn.conn_id == conn_id) else { return Ok(false); }; if Some(handle) == ind_cccd_handle { // 订阅或取消订阅我们的（indication characteristic）指示特性 if offset == 0 && value.len() == 2 { let value = u16::from_le_bytes([value[0], value[1]]); if value == 0x02 { if !conn.subscribed { conn.subscribed = true; self.on_subscribed(conn.peer); } } else if conn.subscribed { conn.subscribed = false; self.on_unsubscribed(conn.peer); } } } else if Some(handle) == recv_handle { // Receive data on the recv characteristic self.on_recv(addr, value, offset, conn.mtu); } else { return Ok(false); } Ok(true) } /// 辅助方法，向Peer客户端发送响应节点，该 Peer 节点刚刚向我们发送了 “recv” 上的一些数据 /// characteristic. （特征） /// 这只是必要的，因为我们支持写确认（与未确认的写入相比，这是更复杂的情况） #[allow(clippy::too_many_arguments)] fn send_write_response( &self, gatt_if: GattInterface, conn_id: ConnectionId, trans_id: TransferId, handle: Handle, offset: u16, need_rsp: bool, is_prep: bool, value: &[u8], ) -> Result<(), EspError> { // 是否需要处理 写确认 if !need_rsp { return Ok(()); } // 是否准备好了数据 if is_prep { let mut state = self.state.lock().unwrap(); state .response .attr_handle(handle) .auth_req(0) .offset(offset) .value(value) .map_err(|_| EspError::from_infallible::<ESP_FAIL>())?; self.gatts.send_response( gatt_if, conn_id, trans_id, GattStatus::Ok, Some(&state.response), )?; } else { self.gatts .send_response(gatt_if, conn_id, trans_id, GattStatus::Ok, None)?; } Ok(()) } /// 处理下一个 indication fn confirm_indication(&self) -> Result<(), EspError> { let mut state = self.state.lock().unwrap(); if state.ind_confirmed.is_none() { // Should not happen：表示我们已收到 // 我们没有发送一个指示(indication) // unreachable!() 是 Rust 提供的一个宏，用于标记代码中“不应该到达”的位置，到这里会恐慌 unreachable!(); } // 通知主循环可以发送下一个indication state.ind_confirmed = None; // So that the main loop can send the next indication self.condvar.notify_all(); Ok(()) } fn check_esp_status(&self, status: Result<(), EspError>) { if let Err(e) = status { warn!("检查esp的状态错误为: {:?}", e); } } fn check_bt_status(&self, status: BtStatus) -> Result<(), EspError> { // matches! 是匹配模式，！取反 // bt状态不匹配就抛连接失败异常 if !matches!(status, BtStatus::Success) { warn!("Got status: {:?}", status); Err(EspError::from_infallible::<ESP_FAIL>()) } else { Ok(()) } } fn check_gatt_status(&self, status: GattStatus) -> Result<(), EspError> { if !matches!(status, GattStatus::Ok) { warn!("gatt的状态错误: {:?}", status); Err(EspError::from_infallible::<ESP_FAIL>()) } else { Ok(()) } } } 新建一个ble_recv_message.rs文件，处理BLE的消息接收use log::{error, info}; use crate::{driver_manage::{GLOBAL_LED}, message::{LedMessage, Message}}; pub fn handle_message(src:&str) -> anyhow::Result<()>{ let message = Message::from_str(src); if let Ok(message) = message{ match message { Message::Led(led_message) => { handle_led_message(led_message)? }, Message::Unknown => { error!("收到错误的类型") }, } } anyhow::Ok(()) } fn handle_led_message(led_message:LedMessage)->anyhow::Result<()>{ info!("{:?}",led_message); if led_message.value == 1{ let mut guard = GLOBAL_LED.lock().unwrap(); guard.as_mut().unwrap().set_high()?; }else { let mut guard = GLOBAL_LED.lock().unwrap(); guard.as_mut().unwrap().set_low()?; } anyhow::Ok(()) } 新建message.rs，处理消息传输，序列化跟反序列化 use serde::{Deserialize, Serialize}; #[derive(Debug,Serialize,Deserialize)] #[serde(tag="type")] pub enum Message{ #[serde(rename = "led")] Led(LedMessage), #[serde(other)] Unknown } #[derive(Debug,Serialize,Deserialize)] #[serde(rename_all = "camelCase")] pub struct LedMessage{ // 0关，1开 pub value:u8 } impl Message{ pub fn to_json(&self) -> serde_json::Result<String>{ serde_json::to_string(self) } pub fn from_str(src:&str) -> serde_json::Result<Self>{ serde_json::from_str(src) } } 新建driver_manage.rs文件，用来管理注册的外设use std::sync::Mutex; use esp_idf_hal::{gpio::{AnyIOPin, AnyOutputPin, Gpio9, Output, OutputPin, PinDriver}, peripheral::Peripheral, prelude::Peripherals}; pub static GLOBAL_LED :Mutex<Option<PinDriver<'static,AnyOutputPin,Output>>> = Mutex::new(None); pub fn init_global(led_gpio:AnyOutputPin) -> anyhow::Result<()>{ { let led = PinDriver::output(led_gpio)?; let mut guard = GLOBAL_LED.lock().unwrap(); *guard = Some(led); } anyhow::Ok(()) } 编辑main.rs文件 use esp_idf_hal::{ gpio::OutputPin, prelude::Peripherals}; use esp_idf_svc::{log::EspLogger, nvs::EspDefaultNvsPartition}; mod ble_server; mod driver_manage; mod message; mod ble_recv_message; fn main() -> anyhow::Result<()> { esp_idf_svc::sys::link_patches(); EspLogger::initialize_default(); let peripherals = Peripherals::take()?; let nvs = EspDefaultNvsPartition::take()?; driver_manage::init_global( peripherals.pins.gpio12.downgrade_output())?; ble_server::start(peripherals.modem,nvs.clone())?; anyhow::Ok(()) } 效果展示成功接收到客户端消息
安装esp-generate# 安装新版生成工具 cargo install esp-generate # 安装依赖工具链 cargo install espup espup install激活环境变量source $HOME/export-esp.sh # linuxWindows用户默认在C:\Users\HOME目录下有一个 export-esp.ps1文件，双击执行激活环境变量使用esp-genertate 创建新项目esp-generate --chip=esp32c3 demo这个项目是个hello world项目，不需要额外的配置，接着按s创建项目添加依赖在Cargo.toml的[dependencies]添加println依赖esp-println = { version = "0.13.1",features = ["esp32c3","log"] }点亮LED修改mian.rs点亮一个led#![no_std] #![no_main] use esp_hal::clock::CpuClock; use esp_hal::gpio::{Level, Output, OutputConfig}; use esp_hal::main; use esp_hal::time::{Duration, Instant}; use esp_println::println; #[panic_handler] fn panic(_: &core::panic::PanicInfo) -> ! { loop {} } #[main] fn main() -> ! { // generator version: 0.3.1 let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); let peripherals = esp_hal::init(config); let mut led = Output::new(peripherals.GPIO9, Level::Low, OutputConfig::default()); let mut count = 0; loop { let delay_start = Instant::now(); led.toggle(); println!("第{count}次数改变led状态"); count+=1; while delay_start.elapsed() < Duration::from_millis(1500) {} } // 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 } 成功打印日志
使用的是 I2C SSD1306 0.96寸的OLED屏幕1.在Cargo.toml中添加图形库依赖# 处理ssd1306屏幕 ssd1306 = {version = "0.10.0",features = ["graphics"]} embedded-graphics = "0.8.1"完整配置 Cargo.toml[package] name = "esp32c3-oled-demo" version = "0.1.0" authors = ["suxii <m@suxii.cn>"] edition = "2021" resolver = "2" rust-version = "1.77" bin name = "esp32c3-oled-demo" harness = false # do not use the built in cargo test harness -> resolve rust-analyzer errors [features] default = [] experimental = ["esp-idf-svc/experimental"] [dependencies] log = "0.4" esp-idf-svc = { version = "0.51", features = ["critical-section", "embassy-time-driver", "embassy-sync","experimental","std"] } futures = "0.3.31" anyhow = "1.0.98" esp-idf-hal = "0.45.2" # 处理ssd1306屏幕 ssd1306 = {version = "0.10.0",features = ["graphics"]} embedded-graphics = "0.8.1" [build-dependencies] embuild = "0.33" 2.在main.rs中编写rust代码use esp_idf_hal::{ delay::FreeRtos, i2c::*, peripherals::Peripherals, prelude::*, }; use embedded_graphics::{ mono_font::{ascii::FONT_6X10, MonoTextStyle}, pixelcolor::BinaryColor, prelude::*, primitives::{Circle, Line, PrimitiveStyle, Rectangle}, text::Text }; use ssd1306::{I2CDisplayInterface, Ssd1306, prelude::*, size::DisplaySize128x64}; fn main() -> anyhow::Result<()> { esp_idf_hal::sys::link_patches(); // 初始化外设 let peripherals = Peripherals::take()?; // 配置I2C let i2c = peripherals.i2c0; let sda = peripherals.pins.gpio5; let scl = peripherals.pins.gpio6; let config = I2cConfig::new().baudrate(400.kHz().into()); let i2c = I2cDriver::new(i2c, sda, scl, &config)?; // 创建SSD1306接口 let interface = I2CDisplayInterface::new(i2c); let mut display = Ssd1306::new( interface, DisplaySize128x64, DisplayRotation::Rotate0 ).into_buffered_graphics_mode(); // 初始化显示屏 display.init().expect("SSD1306 init failed"); display.flush().expect("Failed to flush display"); // 创建图形样式 let text_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On); let line_style = PrimitiveStyle::with_stroke(BinaryColor::On, 1); let mut run_x = 64; loop { // 清空屏幕 display.clear(BinaryColor::Off).unwrap(); // 绘制边框 Rectangle::new(Point::new(0, 0), Size::new(128, 64)) .into_styled(line_style) .draw(&mut display).unwrap(); // 绘制文本 Text::new("Hello Rust!", Point::new(10, 15), text_style) .draw(&mut display).unwrap(); // 绘制直线 Line::new(Point::new(10, 25), Point::new(118, 25)) .into_styled(line_style) .draw(&mut display).unwrap(); // 绘制圆形 if run_x > 128 { run_x = 0 } run_x+=10; Circle::new(Point::new(run_x, 40), 20) .into_styled(line_style) .draw(&mut display).unwrap(); // 更新显示 display.flush().expect("Failed to flush display"); // 间隔2秒 FreeRtos::delay_ms(2000); } }3.接线 SSD1306SDA -> io05SCL -> io06VCC -> 5vGND -> GND4.效果展示