当前位置:首页 > 工业控制 > 电路设计项目集锦
[导读]我的公寓里处处都配备了传感器。每个房间都连接到我的 Home Assistant 系统,因此我无需多想就能知道厨房里的二氧化碳浓度或卧室的湿度。这虽是我小小的执念,但让我始终清楚地了解周围空气究竟有多好或多差。

我的公寓里处处都配备了传感器。每个房间都连接到我的 Home Assistant 系统,因此我无需多想就能知道厨房里的二氧化碳浓度或卧室的湿度。这虽是我小小的执念,但让我始终清楚地了解周围空气究竟有多好或多差。

然而,当我走出家门去跑步或骑车时,这些信息却瞬间消失无踪。我不知道自己到底在呼吸什么,也不知道正在跑的街道究竟有多嘈杂。几位骑行朋友曾问我:“今天出门锻炼真的值得吗?”我们从未真正得到过答案,只能靠天气应用和耸耸肩来回应。

我主修电气工程,于是决定亲自动手解决这个问题。这就是 StreetSense:一个小巧的传感器节点,可轻松贴在跑步前佩戴,搭配手机——而手机本身早已随身携带。

它是如何工作的

传感器节点每秒测量一次颗粒物、挥发性有机化合物和噪音,并通过蓝牙将数据传输到你的手机上。你可以将这次外出标记为跑步、骑行或步行,应用程序会将其整合成我所说的“会话”。

有趣的部分并不在于原始数据。没人会用微克每立方米来思考。因此,后端系统会将一个会话转化为剂量:不是“空气中的PM2.5浓度为34 µg/m³”,而是“你吸入了0.6毫克的PM2.5,最糟糕的30秒出现在那个路口”。系统会根据你呼吸的强度对读数进行加权,因为在一般空气质量下慢跑所消耗的体力,远比在相同环境下散步要多。

这些数据不会依赖你的精确位置。应用程序在发送任何信息前,都会将每个读数四舍五入到大约110米宽的网格单元,而你的实际路线从未离开过手机。

系统架构

StreetSense由四个部分组成,每个部分都有明确的任务:传感器节点负责测量,应用程序负责管理会话并保护你的位置信息,后端系统将读数转换为剂量和判断结果,仪表盘则实时显示人群分布图层。每个部分都已详细列出,包括具体的硬件、通信协议以及选择背后的原因。

传感器节点

单块电路板承担全部传感任务,每秒采样一次,并通过蓝牙低功耗(BLE)将每次读数发送出去。它并不了解“会话”或“剂量”的概念,仅负责测量和广播数据。保持固件如此简单是经过精心设计的:其唯一功能就是将传感器数值打包成固定大小的数据包,因此不会出现与应用程序或后端系统所预期的读数含义脱节的情况。

外壳采用3D打印技术,在Onshape中与内部布局一同设计,为SEN54传感器设置了两个独立的开口,而非一个:圆形孔用于风扇出风口,旁边矩形槽则为进气口。两者分开设计至关重要——Sensirion公司针对该传感器的设计指南要求进风口和出风口相互隔离,以防止排出的空气被直接吸入,影响下一次读数的准确性。

以下是实际安装在其中的内容。虽然略显紧凑,但所有电子元件都能被合理安置:

组装完成后,外观如下所示:

硬件

Adafruit LED 眼镜驱动器,nRF52840 作为微控制器和 BLE 无线电模块。该芯片的蓝牙低功耗(BLE)功耗极低,这对于使用小型电池供电的设备尤为重要。此外,该电路板还内置了一个 PDM 数字麦克风和一个加速度计,因此无需额外部件即可实现噪声检测功能。它作为外设支持 BLE,并同时充当下方两个传感器的 I2C 主机。

Sensirion SEN54 可测量颗粒物浓度(PM1、PM2.5、PM4、PM10)、挥发性有机化合物(VOC)指数、温度和湿度。通过 STEMMA QT 接口适配器连接至 I2C 总线(一种常见的双线协议,许多传感器芯片用于与微控制器通信),采用带按键的微型连接器,防止电缆反向插入,地址为 0x69。

内置的 PDM 数字麦克风可采集噪声电平,通过 nRF52840 内置的 PDM 外设在芯片上解码(PDM 即脉冲密度调制,是小型数字麦克风常用的音频编码方式)。

Adafruit MAX17048 能量计量器通过 STEMMA QT 接口以 I2C 总线传输数据,报告电池电压、充电状态和充电速率,地址为 0x36,与 SEN54 在不同地址共享总线。

一个2500 mAh、3.7V的锂聚合物电池为设备供电,既连接到驱动板上的内置充电电路,也直接连接到MAX17048的感应引脚。

通信协议

该节点会广播自定义的蓝牙低能耗(BLE)GATT服务,这是两个BLE设备协商数据格式的标准方式。它包含一个通知特征:手机在订阅后,将自动每秒接收一次已命名的数据值,无需再次请求。传输的数据以固定、压缩的二进制包形式进行,而非JSON文本格式。由于双方已就固定的数据结构达成一致,因此在小型微控制器上解析JSON并非必要,从而避免了额外开销。

当前的线缆格式将所有信息打包成26个固定大小、固定比例的字节。早期版本较短,仅20字节,特意设计为符合蓝牙默认消息大小限制(即MTU),以便读取时无需协商更大的数据包。但添加电池数据需要更多空间,因此新格式在旧版本所避免的协商过程中进行了优化,并引入版本号,使后端系统仍能无特殊处理地读取旧固件中的数据包。这种精确的布局也会通过固件、应用程序和后端的逐字节匹配测试进行检查,因此如果这三个部分在格式上出现不一致,测试会在设备发生问题之前就发现并解决。

固件

固件使用的是 PlatformIO 和 Arduino 框架,基于 C++ 编写,而非 Java。对于如此小尺寸的微控制器,在功耗和闪存预算限制下,使用 JVM 是不现实的选择。本项目所依赖的所有硬件驱动程序——包括无线电、麦克风以及两个 I2C 传感器——均采用 C++ 和 Arduino 原生语言编写。Java 在本项目中的作用完全体现在后端,用于展示一些实际的预览功能。

应用程序

Android 应用程序是传感器节点唯一的蓝牙连接方式,也是用户会话的实际运行场所。它会扫描到该节点,建立连接,并每秒将实时数据绘制到屏幕上。此外,该应用还负责整个流程中一个关键的隐私保护步骤:在任何数据离开手机之前,应用程序都会将你的 GPS 位置向下取整至一个约 110 米宽的网格单元格。你的具体路线轨迹仅保存在手机本地,从未上传。

该应用运行的是 Java 17 而非 Java 26,这并非用户的选择:Android 自带的构建工具目前尚不支持更新版本。后端宣称使用 Java 26,而应用本身仅支持 Java 17,这两个事实分别反映了两个不同的编译链。

此外,该应用仅支持 Android 12 及更高版本,跳过了 Android 早期处理蓝牙权限的一个旧分支(旧版本在扫描 BLE 设备时仅请求位置权限,而当前版本则采用独立的蓝牙权限)。这种简化措施虽然便利,但代价是牺牲了对旧款手机的支持。

会话地图通过开源库渲染 OpenStreetMap 地图瓦片,而非使用商业地图 SDK,主要是为了使应用运行时无需 API 密钥或计费账户。这一点对于项目至关重要,因为该项目旨在被他人克隆并运行,而不仅仅是用于已配置机器的演示。

后端

这里是 Java 26 的核心所在,一个原始读取数据会在此转化为人类可以实际操作的内容。构建工具链声明要求使用 JDK 26,而提交的 build.log 文件则记录了该工具链实际生成的 java --version 输出信息,由系统自动生成,从而为这一声明提供可验证的凭证,而非仅依赖配置文件中的版本号。

Java 26 中仅有两项预览功能,且两者都真正实现了具体工作,而不仅仅是表面形式:

结构化并发(Structured Concurrency)。每个传入的读取数据都会同时启动三个任务:保存数据、更新滚动基线、检查是否存在异常情况。结构化并发将这三个任务视为一个整体工作单元。如果其中任何一个失败,所有任务都将一同失败,确保系统不会出现“部分更新”的情况——即有读取数据被保存,但没有对应的基线匹配。

模式和 switch 中的原生类型支持。根据客户端不同,读取数据以多种数值类型混合形式到达。现在,一个完整的 switch 语句已能处理全部类型,取代了过去大量繁琐的 instanceof 检查。

此外,我还尝试过并随后放弃了一项 Java 26 的第三项预览功能:懒惰常量(Lazy Constants),用于一次性网格设置。当我真正写出来时,一个简单的常量就能完成完全相同的功能,而且毫无复杂性,所以我干脆删掉了,而不是保留下来只为了说明我用过它。

后端的其余部分依赖于较早完成的Java特性,为两个预览功能提供了一个真正的领域来工作,而不仅仅是一个玩具演示:

外联函数与内存API会解码来自固件的原始传感器数据包,逐字段匹配其C结构体布局。

作用域值(Scoped Values)一次性绑定会话上下文,因此从结构化并发中分叉出的每个任务都可以直接读取它,无需在每个方法调用中反复传递。

流收集器计算每块的滚动基线和剂量本身,这两者都需要对滑动窗口进行累积处理,而不仅仅是对列表求和。

密封接口结合穷举式模式匹配,用于分类实际在空气中发生的变化:交通高峰看起来与烟雾不同,烟雾又与溶剂气味不同,代码必须证明它能处理所有情况。每一个判断都附带了决定该判断的数值依据,因此可以将推理过程与屏幕上显示的内容进行比对,而非仅凭信任接受。

虚拟线程使得每次读取时并行处理三个任务的成本足够低,以至于在负载下几乎不显着。

它还将一次会话的读取数据转化为剂量,该剂量根据每项活动发布的通风乘数进行加权,并将所有贡献者的读取数据汇总到共享的群体图层中,因此一个网格单元格被一个人采样一次,其结果与十来个人一致采样的人所得到的结果不同。

Spring Boot 是后端唯一的运行时依赖:没有独立的数据库层、模板引擎或前端构建(关于这一点,将在下方的仪表板部分详细说明)。为了Java语言竞赛而刻意保持这一列表简洁,因此由Java 26个特性直接完成可见的工作,而非由库代为实现。

仪表板

仪表板直接从后端提供服务,中间没有任何独立的服务、构建步骤或框架:使用的是与后端相同的原始HTML、CSS和JavaScript,仅将其应用于项目前端部分,从而保持整个系统的依赖清单简短。这是人群图层的实时公共展示,是一张全屏的品牌化地图,每个网格单元格的颜色和大小都根据其背后的数据量进行渲染。因此,一个人采样一次的区块与十个人一致同意的区块在视觉上会明显不同。

信心并非仅靠标签来体现,而是通过实际数据验证。大多数网格区域中常见的已确认单元格,会获得一条清晰的细线边框,并以50%不透明度填充;而单个贡献者单元格仍需暂定标记,因此保留了较浅的30%不透明度和虚线边缘,使其更易识别。这些单元格紧密地从边缘对齐排列,彼此之间无间隙,这本身需要特殊处理:由于经度在越北或越南时覆盖的实际距离比纬度短,简单的网格投影会导致远离赤道的区域出现矩形拉伸现象。Web墨卡托投影自身应用了相同的割线校正,使得每个单元格的经度跨度被扩展,因此一个区块在真实米单位下看起来是一个真正的正方形,且由于该投影保持等距性,无论缩放至何种级别,像素尺寸也保持一致。

点击某个单元格将打开一个详细面板,显示地图颜色无法呈现的所有信息:完整的八通道污染物分解数据、当日PM2.5小时级图表及其下方的逐小时表格视图、该区域记录的最清洁、最安静的时段,以及实际经过该特定区块的行程记录——这些记录仅基于该单元格内的测量值,而非某人整个路线的数据。屏幕底部的实时流会随着数据的到来逐一显示读数,其列表或表格的切换方式与上方图表相同。

更新通过服务器发送事件(Server Sent Events)传递到页面,这是一种简单的方式,让服务器无需反复向已打开的网页请求新数据即可持续推送。在此系统中,数据始终单向流动,即从后端传送到浏览器,因此无需借助 WebSockets 等双向通信机制,而 Spring 已内置此类功能,因此无需额外库即可实现连接。用户当前获取的读数无需刷新页面即可立即显示在屏幕上。

更大的理念

一个传感器节点回答一个人的问题,同一城市中的数百个节点共同回答所有人的提问。

每一条读数都被汇总到共享地图中,该地图基于网格单元而非精确坐标构建,因此即使你从未走过某条街道,只要其他人的手机已经记录过,该街道的结果仍会显示出来。仪表板甚至能展示某块区域被一人采样一次与被十几人一致采样结果之间的差异,让你清楚地判断其可信度。

与如今城市监测空气质量的方式相比,情况截然不同:通常只有少数昂贵的固定参考站,每个覆盖极小范围,每小时更新一次,且完全静止不动。而StreetSense则颠覆了这一模式。其读数虽然个体上更嘈杂,但数量成千上万,实时移动、动态更新,覆盖人们实际使用的街道,而非监控预算所能负担的区域。

在真实尺度下,这不再只是一个健身应用,而成为一种前所未有的实时街道级数据集——由身处街头的人们亲手构建,而不是依赖城市需要购买和维护的硬件车队。研究人员能获得来自实际暴露位置的信号,而非仅靠最近站点的估算;城市规划者可以精准定位需要改进的具体路口,而不只是泛指某个街区。而且,每一位参与者都能获得最初安装该应用的初衷:今天是否真的值得出门,以及应该朝哪个方向走。

本文编译自hackster.io

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭