Skip to content

多维度数据采集方案 #144

Description

@gac0812

多维度数据采集方案

以下文档经过真机测试得出

文档目的

本文档说明 Android 客户端各项数据是如何获取、系统函数实际返回什么、应用进行了哪些转换、最终形成哪些字段,以及这些数据的权限和精度限制。运动状态不作为已接入功能展开,仅说明方案状态、不可用原因和待定条件。

本文只整理采集端的数据定义和运动方案状态,不展开自动上传、后端存储和网页看板的实现细节。

需要区分四类数据来源:

  1. 系统状态直接值:例如铃声模式、电池状态。
  2. 系统原始测量值:例如经纬度、麦克风 PCM 采样。
  3. 外部服务返回值:例如 Open-Meteo 返回的天气和温度。
  4. 应用计算结果:例如由 PCM 振幅计算出的 dBFS 和噪声等级。

方案实现状态

这里的“可实现”表示当前项目有明确的系统或外部接口路径,并且可以在目标 Android 客户端中接入;仍然可能受到运行时权限、网络、设备硬件或系统设置影响。

功能或方案 状态 结论
铃声模式 可实现 AudioManager.getRingerMode() 可以直接返回系统铃声模式。
电量与充电 可实现 ACTION_BATTERY_CHANGED 可以返回电量、充电状态和电源类型。
定位 可实现 LocationManager 可以返回经纬度和定位相关信息,但需要用户授权定位。
天气与温度 可实现 通过定位结果调用 Open-Meteo;依赖网络和有效经纬度。
环境噪声 可实现 AudioRecord 可以读取 PCM 幅度并计算相对 dBFS;需要麦克风权限。
设备标识和时间 可实现 ANDROID_ID、设备型号和客户端系统时间可以直接读取。
调用Google Activity Recognition接口获取运动状态 不可实现 当前目标环境不能保证 Google Play services,不能作为可交付的通用方案。
系统传感器 + GPS 规则推断运动状态 待定 技术上可以运行,但精度取决于自定义规则设定、采样参数和实测校准,尚未达到确定交付标准。

Google Activity Recognition:不可实现的原因

原方案通常调用:

ActivityRecognition.getClient(context);
ActivityRecognitionClient.requestActivityUpdates(...);

或者使用 ActivityTransitionClient 监听活动进入和退出。

这些接口由 Google Play services 提供,主要问题是:

  1. 需要引入 com.google.android.gms:play-services-location 等 Google 依赖。
  2. 设备必须安装并正常运行 Google Play services;许多没有 GMS 的国内 Android 设备无法使用。
  3. Android 10 及以上还需要 android.permission.ACTIVITY_RECOGNITION 运行时权限。
  4. 在 GMS 缺失、版本不兼容或 Google 服务被限制时,接口可能无法初始化或持续返回结果。
  5. 它识别的是手机活动类型,不保证能区分具体汽车、公交或地铁,也不等于用户实际交通方式。

因此,Google 接口并非“技术上完全不存在”,而是在当前项目需要覆盖的设备环境中不可作为稳定交付方案。当前工程已移除该依赖、权限和接收器。

系统传感器 + GPS:待定

该方案可以调用 Android 原生接口:

SensorManager.registerListener(
        listener,
        accelerometer,
        SensorManager.SENSOR_DELAY_GAME
);

LocationManager.requestLocationUpdates(...);
Location.getSpeed();

但 Android 原生不会直接返回“步行”“跑步”“乘车”这样的高层分类,应用必须自己制定规则,例如:

  • 用加速度计计算线性加速度 RMS。
  • 用陀螺仪衡量设备旋转幅度。
  • Location.getSpeed() 判断持续速度。
  • 用采样窗口、阈值、平滑和滞回避免状态抖动。

该方案的精度取决于以下规则和条件:

  • 采样时长和采样频率。
  • 加速度、陀螺仪和 GPS 的阈值设定。
  • GPS 位置的精度、更新时间和是否存在漂移。
  • 手机放置姿态、机型传感器质量和厂商系统处理。
  • 是否使用多次采样投票、平滑和状态滞回。
  • 是否针对不同机型和场景单独校准。

因此当前应把输出理解为“规则推断结果”,而不是系统保证的真实交通状态。该方案只有在完成真实设备和场景测试后,才能从“待定”调整为“可实现”。

实现位置

以下路径相对于 Android 工程根目录:

  • app/src/main/java/com/example/devicepulse/DeviceReporter.java:铃声、电池、定位、天气和噪声采集编排。
  • app/src/main/java/com/example/devicepulse/WeatherClient.java:Open-Meteo 请求和地名解析。
  • app/src/main/java/com/example/devicepulse/NoiseMonitor.java:麦克风 PCM 采样和 dBFS 计算。
  • app/src/main/AndroidManifest.xml:Android 权限声明。

采集链路

Android 系统 API / 外部天气 API
                |
                v
DeviceReporter 读取并转换数据
                |
                v
Snapshot 保存最近一次有效数据
                |
                v
形成客户端上报字段

功能总览

功能 主要调用 原始数据 当前上报数据 权限 状态
铃声模式 AudioManager.getRingerMode() 整数模式常量 响铃振动静音 可实现
电量与充电 ACTION_BATTERY_CHANGED 电量、状态、电源类型 百分比、是否充电、充电来源 可实现
定位 LocationManager Location 对象 纬度、经度 定位权限 可实现
天气与温度 Open-Meteo 当前气温、体感温度、天气代码 天气文字、温度、体感温度、地点 网络和定位权限 可实现
环境噪声 AudioRecord 16 位 PCM 采样 dBFS、安静/一般/嘈杂 麦克风权限 可实现
设备标识和时间 Settings.SecureBuild、系统时钟 标识、厂商、型号和时间 device_iddevice_namecaptured_at 可实现

1. 铃声模式

1.1 调用的系统函数

AudioManager audio =
        (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);

int mode = audio.getRingerMode();

Context.getSystemService(Context.AUDIO_SERVICE) 返回 Android 的 AudioManager 系统服务对象。

AudioManager.getRingerMode() 返回一个 int,表示当前系统铃声模式。

1.2 系统真实返回值

数值 Android 常量 系统含义 应用转换值
0 AudioManager.RINGER_MODE_SILENT 静音模式 静音
1 AudioManager.RINGER_MODE_VIBRATE 振动模式 振动
2 AudioManager.RINGER_MODE_NORMAL 正常响铃模式 响铃

1.3 状态变化监听

系统铃声模式变化时会发送:

AudioManager.RINGER_MODE_CHANGED_ACTION

应用注册广播接收器,收到广播后重新调用 getRingerMode(),而不是直接依赖广播附带的值。

1.4 能提供和不能提供的数据

可以提供:

  • 当前铃声模式。
  • 铃声模式是否发生变化。

不能直接提供:

  • 当前铃声音量数值。
  • 当前媒体音量或闹钟音量。
  • 是否处于勿扰模式。
  • 某个具体通知是否会发声。

如需铃声音量,可增加:

audio.getStreamVolume(AudioManager.STREAM_RING);
audio.getStreamMaxVolume(AudioManager.STREAM_RING);

如需勿扰状态,可增加:

NotificationManager.getCurrentInterruptionFilter();

2. 电量与充电状态

2.1 调用的系统函数

Intent intent = context.registerReceiver(
        null,
        new IntentFilter(Intent.ACTION_BATTERY_CHANGED)
);

Intent.ACTION_BATTERY_CHANGED 是 Android 电池状态粘性广播。

传入 null 接收器不会持续注册监听器,而是立即取得系统保存的最近一份电池状态 Intent

2.2 读取的原始字段

当前代码从 Intent 中读取:

int level = intent.getIntExtra("level", -1);
int scale = intent.getIntExtra("scale", 100);
int status = intent.getIntExtra("status", -1);
int plugged = intent.getIntExtra("plugged", 0);
字段 Java 类型 真实含义 常见示例
level int 当前电量刻度 76
scale int 满电刻度,通常为 100 100
status int 电池充放电状态常量 2
plugged int 当前接入电源类型常量 2

2.3 电池状态原始值

Android 常量 数值 含义 当前是否视为充电
BATTERY_STATUS_UNKNOWN 1 未知
BATTERY_STATUS_CHARGING 2 正在充电
BATTERY_STATUS_DISCHARGING 3 正在放电
BATTERY_STATUS_NOT_CHARGING 4 已接电但当前未充电,或系统报告未充电
BATTERY_STATUS_FULL 5 电量已满

2.4 电源接入类型

Android 常量 常见数值 应用显示
BATTERY_PLUGGED_AC 1 适配器充电中
BATTERY_PLUGGED_USB 2 USB 充电中
BATTERY_PLUGGED_WIRELESS 4 无线充电中
其他接电类型 其他 充电中
未在充电 - 未充电

2.5 最终提供的数据

{
  "battery_level": 76,
  "charging": true,
  "charge_source": "USB 充电中"
}
字段 类型 含义
battery_level integernull 0 到 100 的电量百分比
charging boolean 当前实现中表示正在充电或已经充满
charge_source stringnull USB、适配器、无线或未充电

2.6 其他可选电池接口

Android 还提供:

BatteryManager batteryManager =
        (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE);

int capacity = batteryManager.getIntProperty(
        BatteryManager.BATTERY_PROPERTY_CAPACITY
);

该接口通常直接返回 0 到 100 的百分比,但不能完整替代电池广播,因为充电状态和电源来源仍需要其他字段。

3. 定位数据

3.1 调用的系统服务

LocationManager locationManager =
        (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);

当前项目首先尝试读取最近一次位置:

List<String> providers = locationManager.getProviders(true);
Location location = locationManager.getLastKnownLocation(provider);

如果没有可用的最近位置,则请求新位置:

locationManager.requestLocationUpdates(
        LocationManager.NETWORK_PROVIDER,
        0L,
        0f,
        locationListener,
        Looper.getMainLooper()
);

同时也会尝试:

LocationManager.GPS_PROVIDER

3.2 定位来源

Provider 主要来源 特点
GPS_PROVIDER 卫星 室外精度通常较高,首次定位可能较慢,耗电较高
NETWORK_PROVIDER Wi-Fi、基站和系统网络定位 返回较快,室内可能可用,但精度通常低于 GPS

minTime=0minDistance=0 表示应用希望接收所有可用更新,但实际频率仍由系统、硬件和节电策略决定。

3.3 Location 能提供的真实数据

系统函数 返回类型 含义 当前是否上报
getLatitude() double 纬度,单位为度
getLongitude() double 经度,单位为度
getAccuracy() float 水平精度估计,单位为米
getTime() long 该位置的 UTC 时间戳,毫秒
getElapsedRealtimeNanos() long 从设备启动以来的位置时间
getAltitude() double 海拔,单位为米
getSpeed() float 速度,单位为米/秒 否,本功能文档不涉及运动识别
getBearing() float 运动方向角,单位为度
getProvider() String 位置提供者名称

3.4 当前应用保存的数据

{
  "latitude": 31.187164,
  "longitude": 121.605015
}

当前没有保存或上传:

  • 定位精度 accuracy
  • 定位数据自身的时间。
  • Provider 名称。
  • 海拔和方向。

3.5 最近位置选择规则

当前代码遍历所有已启用 Provider 的 getLastKnownLocation(),选择 Location.getTime() 最大的一条,也就是系统报告时间最新的位置。

当前没有限制该位置的最大年龄,因此最新的一条仍有可能是几分钟甚至更久之前的位置。

3.6 新位置等待和失败条件

  • 无最近位置时,请求 Network 和 GPS 更新。
  • 收到第一条新位置后立即停止本次天气定位监听。
  • 最长等待约 12 秒。
  • 12 秒内没有位置则返回“定位超时”。
  • 系统定位服务关闭时返回“请打开系统定位服务”。
  • 权限不可用时返回“定位权限不可用”。

3.7 权限

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Android 12 及以上用户可能只授予模糊位置,此时应用仍可收到位置,但经纬度精度会降低。

当前项目没有申请后台定位权限,因此定位只在应用前台、用户主动采集天气时使用。

3.8 数据质量限制

  • getLastKnownLocation() 不保证是刚刚产生的数据。
  • 室内、地下、隧道或弱卫星环境可能没有 GPS 定位。
  • Network Provider 精度受到 Wi-Fi 和基站环境影响。
  • 当前未上报 accuracy,后端无法直接判断位置可信范围。

建议后续增加:

{
  "location_accuracy_m": 18.4,
  "location_provider": "gps",
  "location_captured_at": "2026-08-03T16:45:00+08:00"
}

4. 天气与温度

4.1 数据来源说明

Android 系统本身没有通用的“当前天气”接口。当前项目使用 Android 定位获得经纬度,然后调用 Open-Meteo。

因此:

  • 经纬度来自 Android 系统定位。
  • 天气和温度来自 Open-Meteo 气象服务。
  • 温度不是手机硬件温度传感器的读数。

4.2 Open-Meteo 请求

当前请求格式:

GET https://api.open-meteo.com/v1/forecast
    ?latitude=31.18716
    &longitude=121.60502
    &current=temperature_2m,apparent_temperature,weather_code
    &timezone=auto
    &forecast_days=1

客户端设置:

connection.setConnectTimeout(10_000);
connection.setReadTimeout(10_000);
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("User-Agent", "DevicePulse/1.0");

Open-Meteo 不需要当前项目配置 API Key。

4.3 外部接口真实返回示例

Open-Meteo 返回内容包含大量字段,当前项目只读取 current 中的三个值:

{
  "current": {
    "time": "2026-08-03T16:45",
    "interval": 900,
    "temperature_2m": 34.7,
    "apparent_temperature": 37.0,
    "weather_code": 3
  }
}
Open-Meteo 字段 类型 含义 当前是否保存
temperature_2m double 距地面约 2 米处的气温,单位通常为摄氏度
apparent_temperature double 根据温度、湿度、风等因素计算的体感温度
weather_code integer WMO 天气代码 转换成中文后保存
time string 天气数据对应时间
interval integer 当前数据时间区间,单位秒

4.4 天气代码转换

当前项目采用以下映射:

WMO 代码 应用文字
0 晴朗
12 少云
3 阴天
4548
5157 毛毛雨
6167 下雨
7177 下雪
8082 阵雨
8586 阵雪
95 及以上 雷雨
其他未匹配值 多变天气

4.5 地点名称解析

项目使用 Android Geocoder 将经纬度转换为城市或行政区名称:

Geocoder geocoder = new Geocoder(
        context,
        Locale.SIMPLIFIED_CHINESE
);

List<Address> addresses = geocoder.getFromLocation(
        latitude,
        longitude,
        1
);

应用按以下优先级选择地点:

  1. Address.getLocality():城市。
  2. Address.getSubAdminArea():次级行政区。
  3. Address.getAdminArea():省、州或一级行政区。
  4. 均不可用时使用保留两位小数的经纬度文本。

例如:

上海市

或回退为:

31.19, 121.61

4.6 最终提供的数据

{
  "weather_condition": "阴天",
  "temperature_c": 34.7,
  "apparent_temperature_c": 37.0,
  "weather_place": "上海市",
  "latitude": 31.187164,
  "longitude": 121.605015
}

4.7 数据含义和限制

  • temperature_c 是气象模型的 2 米高度气温,不是手机附近空气的直接测量值。
  • apparent_temperature_c 是模型计算结果,不是人体传感器测量值。
  • 天气精度受定位精度和 Open-Meteo 网格分辨率影响。
  • Geocoder 在部分设备或网络环境中可能不可用,此时只显示坐标。
  • 当前未保存 Open-Meteo 返回的天气数据时间,无法直接判断天气数据本身的年龄。
  • 无网络、服务超时或非 2xx HTTP 响应时,本次天气采集失败,并保留上一次成功结果。

5. 环境噪声

5.1 调用的系统接口

当前项目使用 Android AudioRecord 读取麦克风原始 PCM 数据。

先查询设备支持的最小缓冲区:

int minimumBuffer = AudioRecord.getMinBufferSize(
        44_100,
        AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT
);

然后创建录音对象:

AudioRecord recorder = new AudioRecord(
        MediaRecorder.AudioSource.MIC,
        44_100,
        AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT,
        bufferSize
);

5.2 采样参数

参数 当前值 含义
音源 MediaRecorder.AudioSource.MIC 设备主麦克风输入
采样率 44,100 Hz 每秒最多 44,100 个采样点
声道 CHANNEL_IN_MONO 单声道
编码 ENCODING_PCM_16BIT 16 位有符号 PCM
最小缓冲区 AudioRecord.getMinBufferSize() 系统报告的最小可用字节数
实际缓冲区 max(minimumBuffer, 4096) 至少 4,096 字节
单次采样时长 2.2 秒 按钮采样窗口

5.3 系统真实返回值

short[] samples = new short[bufferSize / 2];
int count = recorder.read(samples, 0, samples.length);

AudioRecord.read() 返回本次成功写入数组的采样数量 count

每个 short 是 16 位 PCM 振幅,理论范围约为:

-32768 到 32767

这些数值不是分贝,也不是声音文件,而是麦克风 ADC 和系统音频链路输出的相对数字振幅。

5.4 dBFS 计算

应用先将每个采样归一化:

normalized = sample / 32768.0

然后计算均方根:

RMS = sqrt(sum(normalized * normalized) / count)

最后计算数字满量程分贝:

dBFS = 20 * log10(RMS)

当 RMS 为 0 时使用最低值 -90 dBFS,最终结果限制在:

-90 dBFS 到 0 dBFS

解释:

  • 0 dBFS 表示数字信号达到满量程附近。
  • 越接近 -90 dBFS 表示数字振幅越低。
  • dBFS 通常为负数。

5.5 平滑处理

为减少单次读数跳动,当前使用指数平滑:

smoothed = previous * 0.72 + current * 0.28

采样线程约每 240 毫秒向上层提供一次平滑结果。按钮采样约 2.2 秒,最终保存最近一次有效平滑值。

5.6 噪声等级

dBFS 范围 应用等级
< -60.0 安静
>= -60.0< -38.0 一般
>= -38.0 嘈杂

5.7 最终提供的数据

{
  "noise_dbfs": -43.8,
  "noise_level": "一般"
}

5.8 dBFS 与真实环境分贝的区别

noise_dbfs 不是专业声级计中的 dB SPLdB(A)

指标 含义 是否需要校准
dBFS 数字音频相对于满量程的幅度 不需要,但不同设备不可直接比较
dB SPL 物理声压级 需要校准麦克风
dB(A) 经过 A 计权的人耳感知声级 需要校准和频率加权

同一个环境在不同手机上可能产生不同 dBFS,原因包括:

  • 麦克风灵敏度不同。
  • 自动增益控制不同。
  • 厂商音频处理不同。
  • 手机外壳、麦克风位置和遮挡不同。
  • 系统可能进行降噪、限幅或回声处理。

因此当前值适合:

  • 判断同一台设备上环境相对变响或变安静。
  • 做粗略的安静、一般、嘈杂分级。

不适合:

  • 作为法律、职业健康或工程测量依据。
  • 直接比较不同型号手机的绝对噪声值。

5.9 权限和隐私

需要运行时权限:

<uses-permission android:name="android.permission.RECORD_AUDIO" />

当前实现:

  • 不创建音频文件。
  • 不保存 PCM 数组。
  • 不向后端上传录音内容。
  • 只上传计算后的 noise_dbfsnoise_level

5.10 当前实现注意事项

  • 麦克风被其他应用占用时可能初始化失败。
  • 设备不支持 44.1 kHz、单声道、16 位 PCM 组合时会返回格式不支持。
  • 当前 lastNoise 在新一轮采样开始时没有主动重置;极端情况下,如果本轮没有产生新读数,可能复用上一轮值。更严格的实现应在每次采样开始时重置为 Double.NaN

6. 设备标识和时间

6.1 设备 ID

String deviceId = Settings.Secure.getString(
        context.getContentResolver(),
        Settings.Secure.ANDROID_ID
);

ANDROID_ID 返回字符串标识,例如:

0ede23a41dd64933

它不是硬件序列号。现代 Android 中通常与设备、用户和应用签名范围相关,以下情况可能发生变化:

  • 恢复出厂设置。
  • 更换 Android 用户或工作资料。
  • 某些系统升级或厂商实现差异。
  • 应用签名发生变化。

6.2 设备名称

String deviceName = Build.MANUFACTURER + " " + Build.MODEL;

例如:

Xiaomi 23127PN0CC

其中:

  • Build.MANUFACTURER 是厂商标识。
  • Build.MODEL 是系统提供的型号字符串。

这两个值可以被厂商定制,不保证是面向消费者的正式产品名称。

6.3 客户端采集时间

new SimpleDateFormat(
        "yyyy-MM-dd'T'HH:mm:ssXXX",
        Locale.US
).format(new Date());

示例:

2026-08-03T16:46:06+08:00

该时间来自手机系统时钟,并包含时区偏移。

6.4 服务端接收时间

后端收到数据时生成:

2026-08-03T08:46:07+00:00

该值保存为 received_at,使用 UTC。

两个时间的区别:

字段 产生位置 用途
captured_at Android 客户端 表示客户端生成快照的时间
received_at Python 后端 表示后端真正收到数据的时间

如果手机系统时间不准确,captured_at 也会不准确;received_at 可以辅助发现客户端时间漂移或网络延迟。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions