先把“测到了”与“广播被接收”分开
本例让指定SHT40每轮真实读取温湿度,经驱动检查CRC后放进自定义广播。固件没有演示温度常量、随机温度或失败时沿用旧值的分支;电脑夹具的合成值仅用于验证字节布局。本站尚未接板或运行手机,因此没有可展示的真实温湿度、RSSI或距离数据。
节点只做传统非可连接广播,无GATT、配对或收件ACK;手机扫描即可查看。一次adv_start返回0意味着协议栈接受开始,不证明手机看见、样本正确或无线链路稳定。把这几个证据层次分开,后面的故障才有可定位的依据。
参考:[4] Sensirion固定驱动:真实高精度读取/整数近似[7] NimBLE固定广播API:31B与广播参数[9] IDF5.5.5 NimBLE Kconfig:角色
断电核对 PID4885 的四个端点
硬件限定排针已可靠焊接的Adafruit PID4885 SHT40;未焊时先由有条件者完成焊接并检查连锡/虚焊,不能松插随附排针代替焊接。不能把裸SHT40或其他匿名模块套用同一电源表。VIN接主控3V3,GND共地,SDA8/SCL9,初始短线和100kHz;模块自身有电平转换及各10kΩ上拉,先不再并联上拉。模块标3V是稳压输出,保持留空,不与VIN作为两路输入。
完全拔电后逐根追线,用万用表查3V3/GND无短路,再只接桥USB下载与观察UART0 115200。0x44是此模块地址,不能靠改软件让它变成0x45。传感器离开发板发热源;编码两位小数不等于测量精度两位小数。
| 主控 / 排针 | PID4885 | 本例约束 |
|---|---|---|
| 3V3 / J1-1 | VIN | 同3.3V逻辑环境 |
| GND / J1-22 | GND | 共地 |
| GPIO8 / J1-12 | SDA | 100kHz;模块已有上拉 |
| GPIO9 / J1-15 | SCL | 7位地址0x44 |
| 不接 | 3V | 稳压输出,不再注入电源 |
- 做到这里,你应该看到
- GPIO信号号和排针序号分别核对;电源只走USB供电组。
参考:[1] 官方DevKitC1 v1.1固定文档:排针/USB/供电[2] Sensirion SHT4x V6.4:p3/10–13命令、CRC、换算[3] Adafruit PID4885 Pinouts:VIN、3V输出、10k上拉和0x44
锁驱动与广播角色,先完成不插板的编译
下载包保留Sensirion1.1.0固定提交的9个原文件和BSD许可,MIT HAL用IDF5.5.5新I²C master API。高精度函数发0xFD、等待10ms、读取两数据字并检查CRC;整数近似换算后应用再量化,既不启用加热器,也不把通信失败包装成样本。
按E01准备同版IDF与Python环境后,在解压工程根执行下列命令;将串口名换成实际桥口。dependencies.lock和组件清单随包,已有SDK复用即可。生成sdkconfig会覆盖默认值的优先级:修改defaults后用新的配置文件/构建目录,并核最终BROADCASTER=y、其余三个角色n。
本轮实际Python3.12.13/GCC14.2.0构建Exit0,应用bin417168B;这只是编译产物大小,不是内存峰值或功耗。无.git安装SDK的版本警告原样留档,以固定源码和实际配置核版本。
idf.py --version
python --version
idf.py -B build -D SDKCONFIG=sdkconfig.local build
idf.py -B build -D SDKCONFIG=sdkconfig.local -p /dev/cu.YOUR_BRIDGE_PORT flash monitor参考:[4] Sensirion固定驱动:真实高精度读取/整数近似[9] IDF5.5.5 NimBLE Kconfig:角色
把31字节预算与无效规则写成协议
传统广播总长31B:Flags3B,AD长度/type2B,自定义128位UUID16B,余下10B应用负载。UUID是36fb49a7-8c8a-4c92-9d55-6cf2b877d701;线上小端为01 D7 77 B8 F2 6C 55 9D 92 4C 8A 8C A7 49 FB 36,手机常把UUID单独格式化显示。此包未留设备名,扫描时不要只按名称筛选。
flags位0有效、位1湿度裁边、位2读取错误、位3陈旧。有效温度int16二补码/100得°C,湿度uint16/100得%RH;milli整数/10向零截断,RH裁到0..100并另置标志。换算容许温度−45..130°C是编码/公式边界,不是推荐工作范围或精度保证。失败/过期则VALID=0、T/RH=0、ageFFFF;这两个0仅占位。
样本年龄取完整64位单调时间,超过15000ms或时间倒退无效。实际每轮读取后立即编码,age通常0;若广播更新卡住,这个0也会冻结。接收者必须另以自己的单调时钟检查seq是否在15秒内变化;不断收到相同包的新RSSI或接收时间,不能使旧样本恢复新鲜。
| 负载偏移 / 长度 | 字段 | 解释 |
|---|---|---|
| 0 / 1B | version=1 | 未知版本拒绝 |
| 1 / 1B | flags | 高4位保留0 |
| 2 / 2B | seq uint16LE | 含失败尝试递增;回绕/重启分组 |
| 4 / 2B | T int16LE | 0.01°C;无效不用 |
| 6 / 2B | RH uint16LE | 0.01%RH;裁边单独标志 |
| 8 / 2B | age uint16LE | 编码年龄秒;无效FFFF |
参考:[2] Sensirion SHT4x V6.4:p3/10–13命令、CRC、换算[4] Sensirion固定驱动:真实高精度读取/整数近似[7] NimBLE固定广播API:31B与广播参数[8] NimBLE固定Service Data AD定义[12] IDF5.5.5 esp_timer:64位单调时钟
完整主程序:先真实读取,再尝试更新广播
下面main.c与下载包逐字一致;adv_model、HAL、固定Sensirion源及CMake/分区/锁一并下载,不能只复制main就假设驱动存在。程序每轮初始化输出变量,按真实返回码决定read_ok;错误字段的0不会被当成有效温湿度。若I²C初始化失败,修好接线后RESET重建总线。
主机同步时生成静态随机地址。固定API实现中gen_rnd(0)表示static,1才是NRPA;官方示例个别注释存在差异,本例按头文件和实现核对。这里不是周期RPA隐私,也不能把地址当永恒设备ID。读取后stop→set31B→start更新,名义广播间隔1秒另有协议随机延迟;采样还包含测量与调度耗时,不承诺精确5秒。
/* SPDX-License-Identifier: MIT */
#include <string.h>
#include <stdatomic.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "nvs_flash.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/ble_hs_id.h"
#include "host/util/util.h"
#include "sensirion_i2c_hal.h"
#include "sensirion_hal_idf.h"
#include "sht4x_i2c.h"
#include "adv_model.h"
static const char *TAG="W05";static atomic_bool synchronized=false;
static int gap_event(struct ble_gap_event *event,void *arg){(void)arg;if(event->type==BLE_GAP_EVENT_ADV_COMPLETE)ESP_LOGI(TAG,"ADV complete reason=%d; no phone receipt claim",event->adv_complete.reason);return 0;}
static void reset(int reason){atomic_store(&synchronized,false);ESP_LOGW(TAG,"NimBLE reset=%d; wait for new sync",reason);}
static void sync_ready(void){
ble_addr_t address;int rc=ble_hs_id_gen_rnd(0,&address); /* 0 means static random; not NRPA. */
if(!rc)rc=ble_hs_id_set_rnd(address.val);
if(!rc)rc=ble_hs_util_ensure_addr(1);
if(rc){ESP_LOGE(TAG,"address setup rc=%d",rc);return;}
ESP_LOGI(TAG,"random STATIC address this boot %02x:%02x:%02x:%02x:%02x:%02x; not periodic RPA",address.val[5],address.val[4],address.val[3],address.val[2],address.val[1],address.val[0]);
atomic_store(&synchronized,true);
}
static void host_task(void *arg){(void)arg;nimble_port_run();nimble_port_freertos_deinit();}
static void advertise(const uint8_t packet[W05_PACKET_SIZE]){
if(!atomic_load(&synchronized))return;
int rc=0;if(ble_gap_adv_active())rc=ble_gap_adv_stop();
if(rc){ESP_LOGW(TAG,"adv_stop rc=%d; no updated packet claim",rc);return;}
rc=ble_gap_adv_set_data(packet,W05_PACKET_SIZE);
if(rc){ESP_LOGW(TAG,"adv_set_data rc=%d",rc);return;}
struct ble_gap_adv_params p={0};p.conn_mode=BLE_GAP_CONN_MODE_NON;p.disc_mode=BLE_GAP_DISC_MODE_GEN;p.itvl_min=1600;p.itvl_max=1600;
rc=ble_gap_adv_start(BLE_OWN_ADDR_RANDOM,NULL,BLE_HS_FOREVER,&p,gap_event,NULL);
ESP_LOGI(TAG,"adv_start rc=%d; configured nominal1s interval, not a phone receipt",rc);
}
void app_main(void){
esp_err_t e=nvs_flash_init();if(e!=ESP_OK){ESP_LOGE(TAG,"NVS init=%s; preserve data, no automatic erase",esp_err_to_name(e));return;}
sensirion_i2c_hal_init();sht4x_init(0x44);if(demo_i2c_ready()){int16_t rc=sht4x_soft_reset();ESP_LOGI(TAG,"SHT40 soft_reset=%d; real reads follow",rc);}
e=nimble_port_init();if(e!=ESP_OK){ESP_LOGE(TAG,"NimBLE init=%s",esp_err_to_name(e));return;}
ble_hs_cfg.reset_cb=reset;ble_hs_cfg.sync_cb=sync_ready;nimble_port_freertos_init(host_task);
uint16_t sequence=0;
for(;;){
int32_t t=0,rh=0;int16_t rc=demo_i2c_ready()?sht4x_measure_high_precision(&t,&rh):-1;
w05_sample_t sample={.sequence=++sequence,.read_ok=rc==0,.t_milli=t,.rh_milli=rh,.sampled_ms=(uint64_t)esp_timer_get_time()/1000ULL};
uint8_t payload[W05_PAYLOAD_SIZE],packet[W05_PACKET_SIZE];w05_payload(&sample,(uint64_t)esp_timer_get_time()/1000ULL,payload);w05_packet(payload,packet);
ESP_LOGI(TAG,"ACTUAL_READ seq=%u rc=%d raw_t_milli=%"PRId32" raw_rh_milli=%"PRId32" flags=0x%02x; invalid fields are not measured zero",sample.sequence,rc,t,rh,payload[1]);
ESP_LOG_BUFFER_HEX_LEVEL(TAG,packet,sizeof(packet),ESP_LOG_INFO);advertise(packet);vTaskDelay(pdMS_TO_TICKS(5000));
}
}
参考:[4] Sensirion固定驱动:真实高精度读取/整数近似[5] NimBLE固定身份API:nrpa参数定义[6] NimBLE固定实现:随机地址实际处理[7] NimBLE固定广播API:31B与广播参数[12] IDF5.5.5 esp_timer:64位单调时钟
手机从RAW复制字节,按自己的时间验新鲜
操作参考官方nRF Connect Android4.29.1,本站尚未安装/运行手机。记录实际App/系统/手机型号;Android12+按“附近设备”扫描/连接授权,较旧系统还涉及位置权限/位置服务,按实际提示处理。固定旧App文档的权限段不能覆盖所有现代系统情况。
自定义Service Data不保证被自动识别为温湿度。解析工具只接收复制的HEX;--payload是UUID分离后的10B,--packet是本例完整31B。不要将MAC、RSSI或界面标签一并拼入参数。
- 先从桥口确认真实ACTUAL_READ rc/seq与adv_start rc;Scanner启动前去掉名称/RSSI过滤,按Service Data/UUID或RAW内容找本例。
- 展开一行→RAW核AD长度和0x21类型→复制UUID后10B。无需CONNECT;不可连接为预期。
- 在工程根运行python host/decode_adv.py --payload,参数换真实10B HEX;完整31B才用--packet。记录flags、温度符号/单位和seq,对照同轮UART。
- 至少观察两个seq变化;接收单调时间超过15秒无变化则按接收规则判陈旧,分开记录重启/uint16回绕。
- 固定位置/姿态后记录RSSI;遮挡、多径、功率和扫描调度都影响它,不把dBm直接标成米数。
- 做到这里,你应该看到
- 能明确说出“看见广播”“收到新seq”“VALID样本”三者的区别。
参考:[10] nRF Connect4.29.1:Scanner/RAW/客户端[11] Android官方:Android12+与旧系统BLE权限
做已有样本断线与冷启动缺模块两种实验
冷启动缺模块:断电取下模块后重启,预期每轮真实读取失败,广播VALID=0,不展示假0°C。恢复时断电复接四线再RESET,记录首个真实有效seq。没有做过的实验结果留空,不把预期写进已观察栏。
已有样本后断线:断电预装SDA支路低压SPST,正常出现有效样本后只切该信号开关,不带电挪杜邦线/拔电源。等下一轮读取错误,记录最后有效seq、错误flags、闭合后的恢复或所需RESET。冻结同一广播的场景还需检查接收端seq超时;单凭某次没扫到不能定位为确定RF丢包。
| 现象 | 先检查 | 恢复/记录 |
|---|---|---|
| Scanner空 | 广播rc、权限、前台扫描、过滤器、数据线 | 去过滤后再扫,保留真实系统/配置 |
| READ_ERROR | VIN/3V、8/9、共地、CRC/I²C返回 | 断电修线;必要RESET,记首个恢复seq |
| 重复seq但RSSI更新 | 广播缓存/更新失败、接收时钟 | 按>15秒陈旧,不能刷新旧样本时间 |
| 新defaults不生效 | 旧sdkconfig优先 | 新配置目录重建并保留旧日志 |
参考:[1] 官方DevKitC1 v1.1固定文档:排针/USB/供电[3] Adafruit PID4885 Pinouts:VIN、3V输出、10k上拉和0x44[7] NimBLE固定广播API:31B与广播参数
软件检查已完成,手机与物理记录等你执行
下载工程含README、完整依赖、原许可、同源C测试、独立Python解码器和仅表头observations.csv。本轮ASan/UBSan同源模型覆盖31B布局、UUIDLE、signed截断、裁边、无效掩码、seq回绕与完整64位陈旧;Python独立解析C产生的合成向量并拒绝畸形帧。另将原样Sensirion驱动接合成HAL字节,实际检查BEEF CRC、0xFD/10ms、两数据字CRC错误、读写失败与整数端点;这也不运行真实传感器或蓝牙。
原生构建与源/配置/bin/ELF/map/log SHA列在ble-usb-builds.json,固定源逐字核查在ble-usb-sources.json。真实手机扫描、模块精度、RSSI、断线恢复仍pending;真实完成后填CSV并保留失败行,不把合成向量当温湿度。下一篇在另一个工程中增加GATT配置与认证,不直接给此公开广播塞入口令。
资料来源与版本说明
按固定官方实现、厂家引脚和实际构建核查;主机夹具仅证明软件边界,指定板/手机/USB实体运行仍未执行。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。
- 官方DevKitC1 v1.1固定文档:排针/USB/供电
- Sensirion SHT4x V6.4:p3/10–13命令、CRC、换算
- Adafruit PID4885 Pinouts:VIN、3V输出、10k上拉和0x44
- Sensirion固定驱动:真实高精度读取/整数近似
- NimBLE固定身份API:nrpa参数定义
- NimBLE固定实现:随机地址实际处理
- NimBLE固定广播API:31B与广播参数
- NimBLE固定Service Data AD定义
- IDF5.5.5 NimBLE Kconfig:角色
- nRF Connect4.29.1:Scanner/RAW/客户端
- Android官方:Android12+与旧系统BLE权限
- IDF5.5.5 esp_timer:64位单调时钟