第一章:多功能传感器工具箱——项目概览与架构设计

各位同学,欢迎来到实战篇。

说实话,前面讲了那么多单个传感器的用法,大家可能觉得不过瘾。每个传感器单独用,就像手里有几把好用的工具,但没装进工具箱里。今天这个项目,就是要把加速度计、陀螺仪、磁力计、光线传感器、距离传感器这些家伙,全部整合到一个 App 里。

我管它叫「多功能传感器工具箱」。名字很直白,功能也很硬核:数据采集、实时可视化、导出 CSV、简单分析,一条龙搞定。

项目核心目标:打造一个可扩展的传感器数据采集与分析平台,支持多传感器并发、实时图表展示、数据导出与回放。

1.1 为什么需要这样一个工具箱?

你想想看,平时调试传感器,是不是经常遇到这些情况:

  • 想看看加速度计在某个动作下的波形,但没有实时绘图工具
  • 采集了一堆数据,想导出到电脑分析,结果发现格式乱七八糟
  • 多个传感器同时工作,日志混在一起,根本分不清谁是谁

我在做某个运动追踪项目时,就吃过这个亏。当时为了验证算法,手动记录了几百组数据,眼睛都快看瞎了。后来我花了两天时间,写了个简单的采集工具——嗯,就是今天这个项目的雏形。

所以,这个工具箱不是凭空想出来的,是实实在在从坑里爬出来的经验。

1.2 整体架构设计

先看一张架构图,我把整个项目的模块划分和调用关系画出来了:

多功能传感器工具箱 — 架构图 UI 层(Activity / Fragment) ViewModel + SensorDataManager SensorManager(加速度 / 陀螺仪 / 磁力计 / 光线 / 距离) 数据存储层(Room / CSV导出 / 文件管理) 数据流方向:UI → ViewModel → SensorManager → 存储层

这个架构其实不复杂。从上往下看:

  • 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 数据采集的核心流程

数据采集听起来简单,但有几个坑必须提前说:

  1. 采样频率要可控:不是越快越好。加速度计最高能到 200Hz,但你的 UI 刷新率跟不上,数据就白采了。
  2. 时间戳必须统一:多个传感器同时采集时,时间戳要对齐。我习惯用 System.nanoTime(),精度高,而且不受系统时间修改影响。
  3. 缓冲区要防溢出:如果采集频率高,数据量会很大。我用的是环形缓冲区(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