首页
苏兮影视
随笔记
壁纸
更多
直播
时光轴
友联
关于
统计
Search
1
v2ray节点搭建
1,023 阅读
2
软件添加id功能按钮
937 阅读
3
QQ扫码无法登录的解决方案
846 阅读
4
网易云音乐歌单ID获取教程
719 阅读
5
typecho非常有特色的模块
664 阅读
谈天说地
建站源码
经验教程
资源分享
动漫美图
登录
Search
标签搜索
java
rust
flutter
esp32c3
springboot
安卓
linux
vue
dart
设计模式
docker
joe
快捷键
git
fish shell
maven
redis
netty
groovy
js
尽意
累计撰写
112
篇文章
累计收到
39
条评论
首页
栏目
谈天说地
建站源码
经验教程
资源分享
动漫美图
页面
苏兮影视
随笔记
壁纸
直播
时光轴
友联
关于
统计
搜索到
96
篇与
的结果
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日
248 阅读
0 评论
0 点赞
ESP32C3引入Slint: 解决依赖冲突
为什么要折腾这个?想在 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 跟踪问题进展
2025年05月03日
162 阅读
0 评论
0 点赞
2025-05-01
ESP32C3使用BLE
初始化项目参考 项目创建 新建一个基于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(()) } 效果展示成功接收到客户端消息
2025年05月01日
176 阅读
0 评论
1 点赞
2025-04-30
rust裸机开发esp32c3
安装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 } 成功打印日志
2025年04月30日
119 阅读
0 评论
0 点赞
2025-04-27
rust开发esp32c3驱动oled屏幕
使用的是 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.效果展示
2025年04月27日
114 阅读
0 评论
1 点赞
1
2
3
...
20