第一章:多功能传感器工具箱——项目概览与架构设计
各位同学,欢迎来到实战篇。
说实话,前面讲了那么多单个传感器的用法,大家可能觉得不过瘾。每个传感器单独用,就像手里有几把好用的工具,但没装进工具箱里。今天这个项目,就是要把加速度计、陀螺仪、磁力计、光线传感器、距离传感器这些家伙,全部整合到一个 App 里。
我管它叫「多功能传感器工具箱」。名字很直白,功能也很硬核:数据采集、实时可视化、导出 CSV、简单分析,一条龙搞定。
项目核心目标:打造一个可扩展的传感器数据采集与分析平台,支持多传感器并发、实时图表展示、数据导出与回放。
1.1 为什么需要这样一个工具箱?
你想想看,平时调试传感器,是不是经常遇到这些情况:
- 想看看加速度计在某个动作下的波形,但没有实时绘图工具
- 采集了一堆数据,想导出到电脑分析,结果发现格式乱七八糟
- 多个传感器同时工作,日志混在一起,根本分不清谁是谁
我在做某个运动追踪项目时,就吃过这个亏。当时为了验证算法,手动记录了几百组数据,眼睛都快看瞎了。后来我花了两天时间,写了个简单的采集工具——嗯,就是今天这个项目的雏形。
所以,这个工具箱不是凭空想出来的,是实实在在从坑里爬出来的经验。
1.2 整体架构设计
先看一张架构图,我把整个项目的模块划分和调用关系画出来了:
这个架构其实不复杂。从上往下看:
- UI 层:负责展示图表、按钮、实时数据。我用的是 Fragment + ViewPager,每个传感器一个页面。
- ViewModel 层:管理传感器数据的生命周期。说白了,就是当手机旋转时,数据不会丢。
- SensorManager 层:核心。注册、注销传感器监听,统一管理采样频率。
- 存储层:Room 数据库存历史数据,CSV 导出给外部工具用。
我的经验:一开始别想着把所有传感器塞到一个 Activity 里。我建议每个传感器一个 Fragment,通过 ViewPager 切换。这样代码清晰,也方便后续加新的传感器。
1.3 关键技术选型
做这个项目,我选了几个趁手的工具:
| 模块 | 技术选型 | 为什么选它 |
|---|---|---|
| 图表绘制 | MPAndroidChart | 社区活跃,支持实时刷新,曲线图、柱状图都有 |
| 本地存储 | Room + CSV | Room 适合结构化查询,CSV 方便导出到 Excel |
| 架构模式 | MVVM + LiveData | Google 官方推荐,生命周期安全 |
| 传感器管理 | 自定义 SensorManager 封装 | 统一管理注册/注销,避免内存泄漏 |
这里多说一句。MPAndroidChart 这个库,我用了好几年了。虽然它有些 API 设计得比较老,但胜在稳定。我曾经试过用 Canvas 自己画曲线,结果折腾了两周,效果还不如直接用这个库。所以,别重复造轮子。
1.4 项目结构预览
我们来看一下最终的包结构长什么样:
com.sensortoolbox/
├── ui/
│ ├── main/
│ │ ├── MainActivity.kt
│ │ └── MainViewModel.kt
│ ├── accelerometer/
│ │ ├── AccelerometerFragment.kt
│ │ └── AccelerometerViewModel.kt
│ ├── gyroscope/
│ ├── magnetometer/
│ ├── light/
│ └── proximity/
├── data/
│ ├── model/
│ │ └── SensorData.kt
│ ├── local/
│ │ ├── AppDatabase.kt
│ │ └── SensorDao.kt
│ └── export/
│ └── CsvExporter.kt
├── sensor/
│ └── SensorManagerWrapper.kt
└── utils/
└── TimeUtils.kt
这个结构是我个人比较喜欢的。每个传感器一个包,互不干扰。如果你以后想加个气压计或者心率传感器,直接复制一个 Fragment 改改就行。
注意:千万不要把所有传感器的逻辑写在一个 Fragment 里。我曾经见过一个项目,一个文件 3000 行,维护起来简直噩梦。每个传感器独立成模块,这是底线。
1.5 数据采集的核心流程
数据采集听起来简单,但有几个坑必须提前说:
- 采样频率要可控:不是越快越好。加速度计最高能到 200Hz,但你的 UI 刷新率跟不上,数据就白采了。
- 时间戳必须统一:多个传感器同时采集时,时间戳要对齐。我习惯用 System.nanoTime(),精度高,而且不受系统时间修改影响。
- 缓冲区要防溢出:如果采集频率高,数据量会很大。我用的是环形缓冲区(Circular Buffer),固定大小,满了就覆盖旧数据。
来看一段核心的采集代码:
// SensorManagerWrapper.kt
class SensorManagerWrapper(private val context: Context) {
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val bufferMap = mutableMapOf<Int, CircularBuffer<SensorData>>()
fun registerSensor(sensorType: Int, samplingPeriodUs: Int) {
val sensor = sensorManager.getDefaultSensor(sensorType) ?: return
// 每个传感器分配一个缓冲区,容量 1024
bufferMap[sensorType] = CircularBuffer(1024)
sensorManager.registerListener(
sensorEventListener,
sensor,
samplingPeriodUs
)
}
private val sensorEventListener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
val buffer = bufferMap[event.sensor.type] ?: return
val data = SensorData(
timestamp = System.nanoTime(),
values = event.values.clone()
)
buffer.add(data)
// 通过 LiveData 通知 UI 更新
liveDataMap[event.sensor.type]?.postValue(data)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
// 精度变化时记录日志
Log.d("SensorToolbox", "精度变化: ${sensor?.name} -> $accuracy")
}
}
}
这段代码里,我用了 CircularBuffer 来缓存数据。为什么不用 ArrayList?因为如果用户一直开着采集,内存会爆。环形缓冲区固定大小,内存安全。
避坑指南:我曾经在 onSensorChanged 里直接更新 UI,结果卡成 PPT。正确的做法是:传感器回调里只做数据缓存,通过 LiveData 异步通知 UI 更新。记住,传感器回调是在 Binder 线程池里跑的,别阻塞它。
1.6 本章小结
这一章我们搭好了项目的骨架。你知道了:
- 为什么要做这个工具箱(说白了就是被坑出来的)
- 整体架构怎么分层(UI → ViewModel → SensorManager → 存储)
- 技术选型怎么选(MPAndroidChart、Room、MVVM)
- 数据采集的核心流程和注意事项
下一章,我们会正式开始写代码。先从加速度计模块入手,把实时曲线图画出来。到时候你会看到,数据在手机屏幕上跳动的感觉,真的很爽。
公众号:蓝海资料掘金营,微信deep3321