
前阵子给一个居家健康监护方向的Demo项目做功能迭代团队里反复商量需求最后锁定的核心场景是独居老人在家跌倒后能不能在几分钟内通知到家人。市面上的方案并不少有挂坠式的专业跌倒报警器也有带摔倒检测的智能手环。挂坠式设备的优势是检测准但问题很现实——用户得多戴一个设备充电、佩戴、遗忘这些事在日常使用里都会发生。我们讨论到最后决定换个思路让用户口袋里那台手机直接充当跌倒检测传感器。这篇文章就把我从零实现一个跌倒警报器APP的全过程拆开讲包括传感器原理、判定算法、核心代码、误报调试和真机测试。适合两类人看一类是刚接触安卓传感器开发、想快速做健康监护类Demo的开发者另一类是硬件或产品相关从业者想评估手机方案做跌倒检测到底可不可行。读完你至少能动手搭一个能跑通的原型并且知道一整套参数该怎么调、坑在哪里。1. 为什么手机能代替专业跌倒检测设备1.1 加速度计究竟在测什么很多人对加速度计有个误解以为它测的是速度的变化其实它测量的是比力可以简单理解成设备感受到的受力情况。手机静止放在桌面上时加速度计读数是9.8m/s²左右方向竖直向上这正好是重力加速度的量值。也就是说加速度计在静止状态下测到的其实是地球引力给的支撑力。这个细节对跌倒检测非常关键。当手机跟随人体一起自由下落时手机和人体都在做同样的加速运动加速度计反而读不到重力了三轴读数会同时趋近于0。这就形成了跌倒信号里的第一个特征——失重阶段。等人体撞击地面手机瞬间被强烈的冲击力带动加速度计会在极短时间内测到一个远大于重力加速度的尖峰比如20m/s²甚至更高这是第二个特征——撞击阶段。撞击之后如果人躺在地上没有动作加速度计读数又会回到接近9.8m/s²的水平只是方向相对原先有了变化这是第三个特征——静止阶段。所以手机里那颗小小的加速度计已经足够捕获跌倒动作的核心物理特征。剩下的问题只是如何把这三段特征从噪声里干净地识别出来。1.2 三轴坐标与佩戴姿势的麻烦安卓系统的传感器坐标系有约定X轴指向屏幕右侧Y轴指向屏幕上方Z轴垂直于屏幕向外。手机竖屏放进口袋时Z轴大致朝向身体静息读数的基准值会偏向Z轴。但手机一旦横着放、倒着放甚至拿在手里刷视频静态基准就完全变了。这就是手机方案和挂坠式设备最大的区别挂坠的姿态相对固定可以针对单一姿态优化算法手机的姿态千变万化如果直接拿原始单轴数据做判断很容易误报漏报。所以在做算法设计时我不会盯着某一个轴的原始读数看而是先计算三轴合成加速度也就是Math.sqrt(x*x y*y z*z)这个标量值不依赖手机朝向无论手机是横是竖静止时它都稳定在重力加速度附近。用合加速度作为判断依据是整个方案里最基础也最重要的一步。后面的失重判断、撞击判断全部围绕这个合成值展开而不是某个轴单独读数。这个简化让算法在面对不同佩戴姿势时依然能够保持稳定。1.3 手机方案和专用硬件的取舍专用跌倒检测设备在准确率上确实有优势尤其是那些内置多传感器、专门针对跌倒场景调优的医疗级产品。但这类设备的推广一直有个绕不过去的坎用户得记得戴。独居老人日常行动不便挂坠很容易被摘下来放在床头柜上充电一旦摘下来再灵敏的传感器也等于零。手机方案天然解决了佩戴这个问题。现代人几乎不会脱离开手机尤其是独居老人手机不仅是通讯工具还是他们和外界最重要的联系渠道。手机方案的难点就变成了硬件性能受限、佩戴位置不固定、系统后台限制多。但这些难点都可以通过算法和工程手段去弥补。我当时的选择是两者结合把手机方案作为主要终端同时在代码层预留了接入外部BLE跌倒检测手环的接口。如果用户手头有手环手环触发信号直接作为高优先级事件如果没有手环手机上的传感器算法作为兜底。这样既保证了易用性也保留了升级空间。2. 跌倒检测算法一次跌倒留下的三条波形线索2.1 一次典型跌倒的瞬时过程我做过很多次模拟测试在软垫上反复练习不同类型的跌倒同时用手机记录传感器波形。排除掉各种干扰之后一条清晰的跌倒信号通常包含三个阶段。第一阶段是失重人体失去平衡开始下坠加速度计读数急剧下降合成的合加速度从9.8m/s²掉到2m/s²以下持续的时间通常在0.2到0.4秒左右。第二阶段是撞击身体或手机撞到地面合加速度在瞬间冲到20m/s²以上甚至能达到30m/s²这个尖峰非常短只有几十毫秒。第三阶段是静止撞击结束后如果人没有立即站起来合加速度回到接近9.8m/s²的水平但持续一段时间保持低幅值波动。如果只用单一阈值判断比如合加速度超过25m/s²就报警那弯腰捡东西、用力拍桌子、手机从口袋滑落都会触发误报。因为这些动作也可能产生单次高尖峰。跌倒之所以能被识别关键在于它是失重-撞击-静止这个完整序列而不是孤立的一个尖峰。所以检测算法的核心思想是识别这个三段式序列。2.2 失重、撞击、静止三段式判定逻辑我把检测逻辑实现成一个简单的状态机在内存里维护一个有限状态集合。初始状态是空闲态此时持续读取合加速度。一旦发现合加速度低于失重阈值比如小于2.5m/s²就进入失重态同时记录进入失重态的时间戳。在失重态中如果在限定的时间窗口内比如1.5秒内合加速度突然冲到撞击阈值以上比如20m/s²就认为检测到了撞击状态进入撞击态。如果在窗口内没有等到撞击说明这次只是短暂的失重状态回到空闲态。进入撞击态之后接下来要做的是静止确认。因为人真的跌倒后通常会有短暂的静止或微弱移动而误报动作往往在撞击之后立刻结束。我设定的确认逻辑是在撞击发生后的约3秒内如果合加速度连续稳定在9.8m/s²附近小于2m/s²的波动并且这个状态保持1秒左右才判定为一次真正需要报警的跌倒。这个三段式逻辑看起来简单但实际效果比我用过的很多复杂机器学习模型都要稳定。原因在于它抓住了跌倒动作的物理本质而不是单纯拟合数据。2.3 阈值参数不是拍脑袋定的初始阈值我给出了一组参考值参数初始值说明失重阈值合加速度 2.5 m/s²明显低于静止重力值确认进入失重窗口撞击阈值合加速度 20 m/s²约为重力加速度的2倍排除普通挥手等动作失重到撞击窗口1.5 秒从失重开始计算超时未撞击则放弃静止确认波动 2.0 m/s²撞击后约3秒内合加速度波动范围静止保持时间≥ 1 秒确认人在撞击后没有立刻站起这些值不是凭空定的。失重阈值取2.5是因为正常行走时合加速度虽然会波动但很少连续降到这个水平弯腰捡东西时合加速度确实会下降但一般不会跌破2.5并持续足够长时间。撞击阈值取20是因为人在正常活动尤其是慢速活动中合加速度峰值很少超过这个值而硬质地面上的跌倒撞击几乎必然超过它。不过我必须说明这些参数只是起点不同人群、不同使用方式都需要调整。比如腿脚不便的老人走路姿态更不稳定撞击阈值可能需要下调到18甚至16年轻人在实验垫上做模拟跌倒峰值往往更大阈值可以上调降低误报。参数必须做成外部可配置不要写死在代码里。3. 从传感器读数到报警短信核心代码落地3.1 工程准备与权限声明这个原型项目的技术栈用的是原生安卓开发Java语言没有引入第三方SDK核心依赖就是系统自带的传感器API、定位API和短信API。工程目录我就不展开了直接说关键配置。先在AndroidManifest里声明权限uses-permission android:nameandroid.permission.SENSOR / uses-permission android:nameandroid.permission.SEND_SMS / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_LOCATION / uses-permission android:nameandroid.permission.POST_NOTIFICATIONS / uses-permission android:nameandroid.permission.VIBRATE /注意SENSOR和VIBRATE属于普通权限安装即授予。SEND_SMS、定位、通知属于动态权限需要在App启动时逐项申请。定位权限建议同时申请精确和模糊两项并且在前台服务里声明定位用途否则部分系统版本会直接拦截前台服务启动。权限这块有个容易踩的坑很多国产定制系统里SMS权限即使你动态申请了系统默认也可能不弹窗而是把选项埋在设置里的应用权限管理中。测试时如果发现短信发不出去先检查这里。3.2 采样率选型和后台线程传感器监听的采样率直接决定识别效果和功耗。我测试过几种模式之后选了SensorManager.SENSOR_DELAY_GAME这个档位大约每20到50毫秒返回一次数据已经足够捕获跌倒的失重和撞击尖峰。SENSOR_DELAY_UI大约60毫秒一次对瞬时撞击峰值的捕捉不够稳容易漏掉。SENSOR_DELAY_FASTEST虽然最灵敏但功耗明显高长时间挂后台会让手机发烫没必要。另一个细节是传感器回调默认跑在主线程数据密集时容易卡顿所以我推荐用Handler把回调指定到后台线程HandlerThread sensorThread new HandlerThread(sensor-thread); sensorThread.start(); Handler sensorHandler new Handler(sensorThread.getLooper()); sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME, sensorHandler);这样做的好处是即便传感器数据再多也不会干扰界面刷新。3.3 状态机代码骨架下面这段是我实际跑通的简化版代码去掉了日志和部分边界处理保留了核心判定流程private static final int STATE_IDLE 0; private static final int STATE_FREE_FALL 1; private static final int STATE_IMPACT 2; private int state STATE_IDLE; private long freeFallStartTime 0; private long impactTime 0; private static final float FREE_FALL_THRESHOLD 2.5f; private static final float IMPACT_THRESHOLD 20.0f; private static final long FALL_TO_IMPACT_WINDOW_MS 1500; private static final long STILL_CONFIRM_WINDOW_MS 3000; Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() ! Sensor.TYPE_ACCELEROMETER) { return; } float x event.values[0]; float y event.values[1]; float z event.values[2]; double magnitude Math.sqrt(x * x y * y z * z); long now System.currentTimeMillis(); switch (state) { case STATE_IDLE: if (magnitude FREE_FALL_THRESHOLD) { state STATE_FREE_FALL; freeFallStartTime now; } break; case STATE_FREE_FALL: if (magnitude IMPACT_THRESHOLD) { state STATE_IMPACT; impactTime now; break; } if (now - freeFallStartTime FALL_TO_IMPACT_WINDOW_MS) { state STATE_IDLE; } break; case STATE_IMPACT: if (now - impactTime STILL_CONFIRM_WINDOW_MS) { state STATE_IDLE; } else if (isStillConfirmed(magnitude, now)) { triggerAlert(); state STATE_IDLE; } break; } }isStillConfirmed的判断逻辑是在撞击之后的3秒窗口内统计合加速度保持在9.8 ± 2.0范围内的时间连续超过1秒就返回true。这个函数内部要维护一个时间累加器我实际实现时用了一个简单的差值计数器每来一条数据就判断当前读数是否在静止区间是则累加否则清零。这里要特别提醒判定逻辑一定要用时间戳和窗口不要用连续几帧来判断因为不同手机的采样率差异很大同一段动作在不同设备上帧数差好几倍。3.4 警报链路与误触取消机制检测到真实跌倒后直接发短信是最直观的通知方式但流程不能一步走完。我在触发警报前先开启一次高音量警告音和振动同时在屏幕上弹出倒计时默认10秒内如果用户主动点了我没事就取消警报。这个设计非常有必要因为算法再精确也会有误报宁可让用户多按一次取消按钮也不要让家属收到虚假的紧急短信。倒计时结束后才真正执行警报逻辑核心两步是获取位置和发送短信private void sendAlertMessage() { LocationManager lm (LocationManager) getSystemService(LOCATION_SERVICE); Location location null; if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) PackageManager.PERMISSION_GRANTED) { location lm.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (location null) { location lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); } } String fallbackLocation 未获取到定位; if (location ! null) { fallbackLocation 纬度: location.getLatitude() ,经度: location.getLongitude(); } String message 【跌倒警报】我可能跌倒了当前定位 fallbackLocation 请尽快确认我的情况。; SmsManager smsManager SmsManager.getDefault(); smsManager.sendTextMessage(emergencyPhone, null, message, null, null); }这里有个很大的工程教训GPS首次定位很慢如果跌倒后才去等卫星信号可能要几十秒甚至更久警报短信就失去意义。所以我在前台服务里用一个定时任务每60秒更新一次最近已知位置缓存到内存和本地SharedPreferences警报时直接读取缓存几乎零延迟。4. 误报控制与功耗被真实使用逼出来的优化4.1 复盘那些典型的误报动作第一版原型跑通后我和几个同事在办公室做了大量日常动作测试结果误报率让人头大。归纳起来最常见的几类误报各有各的原因。弯腰捡东西合加速度确实会掉到很低甚至接近失重状态但由于弯腰动作很快失重持续时间非常短而且紧接着的撞击往往是手碰到地面撞击幅值远小于真正的跌倒。这类误报在加入失重到撞击窗口判断后被过滤掉了大部分。快速坐下人坐下瞬间臀部撞到椅子会产生一个明显的加速度尖峰有时能超过20m/s²。但它的问题在于失重阶段不明显身体从站立到坐下的垂直速度变化比跌倒小得多合加速度不会跌到2.5以下。所以状态机根本不会进入失重态直接挡住。把手机拍在桌上这个动作的撞击峰值又高又陡但完全没有前置失重阶段属于典型的孤立尖峰同样被状态机挡掉。而且撞击之后合加速度迅速回到9.8附近并很快稳定不存在长时间静止异常的确认条件。逐步排除误报的过程让我意识到三段式序列判断比任何单点阈值都可靠得多。它本质上是在问三个问题有没有失重失重后有没有撞击撞完之后人有没有动三个答案都是是才值得报警。4.2 动态基线与个性化参数阈值参数虽然能挡住大多数误报但不同用户的身体状态差异很大。后来我加了一个动态基线校准机制用户首次开启应用后的前10秒App进入静息采集模式统计当前三轴合加速度的平均值比如有的老人身体前倾坐着静息值可能不是标准的9.8而是9.3那么后续的失重阈值和静止区间就基于这个动态基线计算。这个机制特别重要。静止确认阶段判断合加速度是否在9.8附近如果用户手机放在裤兜里微微倾斜合加速度本来就是10.1用固定9.8判断就容易漏报。动态基线解决的就是这类真实佩戴差异。同时我把所有阈值参数放到了设置页用可拖动的滑块暴露出来用户可以根据自己的行动能力调节检测灵敏度。比如行动迟缓的老人平时走路合加速度波动本来就小可以把撞击阈值调低平时动作幅度大的用户可以调高阈值减少误报。参数设置界面不大但这个设计在整个项目验收时帮了大忙测试人员可以实时调参而不用反复改代码重新编译。4.3 长期待机的省电策略传感器长时间高频采样耗电是客观存在的。如果一直用SENSOR_DELAY_GAME挂后台一部手机的续航会明显缩短老年人往往又忘记充电这会反过来导致设备关机报警功能失效。我的省电方案是级联采样。前台服务里默认用SENSOR_DELAY_NORMAL约200毫秒一次这个频率日常检测不到跌倒尖峰但能感知到运动能量急剧变化这类粗粒度事件。当连续检测到合加速度突然下降或上升超过某个粗阈值时才动态把采样频率切换到SENSOR_DELAY_GAME持续几秒后如果没有确认跌倒再自动降回低频率。用这个策略待机功耗大概降低了60%到70%同时检测延迟没有明显恶化。因为在真正的跌倒动作里失重阶段持续200到400毫秒即便低频率采样漏掉了开头高频采样的切换动作依然来得及捕捉后续的撞击尖峰和静止阶段。还有一个系统层面的坑安卓高版本的系统资源回收策略很激进如果应用没有开启前台服务灭屏几分钟后传感器就会被冻结。所以检测服务必须以前台服务方式运行并在通知栏常驻一个不起眼的守护中通知。部分国产定制系统还会在后台管理界面里默认限制应用的后台运行用户需要在电池优化白名单里手动把本应用加入否则一切白搭。5. 真机测试方法与我踩过的几个坑5.1 怎么用演戏的方式测跌倒算法调完参数必须进行真机测试。我管这个方法叫演员测试法需要一个人反复模拟各种跌倒姿势另外一个人记录每次测试的判定结果。测试动作清单我整理成了一张表测试动作预期结果备注正向平地滑倒报警重点测试标准场景侧向摔倒报警撞击方向不同合加速度仍应触发缓慢下蹲后坐下不报警误报高危场景弯腰捡手机不报警模拟日常捡物快速坐下不报警办公室高频动作手机从桌上摔落不报警设备本体跌倒但人无事拍打手机不报警模拟碰撞外来物跌倒后立刻爬起不报警或延迟报警报告先不生效可手动解除测试场地一定要铺厚软垫别直接在硬地上真摔。我亲眼见过测试时把腰闪了的情况做健康应用的人自己先受工伤说出去都丢人。每轮测试不要只看报不报警还要把传感器日志打出来记录触发那几秒钟的合加速度波形方便复盘哪一段特征不满足预设条件。没有日志的误报排查等于瞎猜。5.2 真机环境下的典型问题测试阶段踩了几个坑每个都值得单独说。第一个是定位冷启动太慢。最初版本在触发警报时才去请求定位实际上跌倒报警触发后GPS首次定位通常需要几十秒短信已经发出去了家属收到的却是未获取到定位的占位文案。后来改成了每60秒缓存一次最近位置才解决了这个问题。第二个是SMS权限在不同定制系统上的表现不一致。部分定制系统里即使你在代码里申请了发送短信权限用户在权限弹窗里点了允许实际发送时系统也可能静默拦截。这个问题我排查了很久最后发现需要在系统设置的应用权限管理中手动开启发送短信选项。所以我在应用内做了一个权限自检引导页首次启动时逐项检查SENSOR、定位、短信、通知是否真正可用并给出跳转系统设置页的按钮。第三个问题是连续报警风暴。如果用户跌倒后身体还在轻微移动手机可能会连续触发多次失重-撞击判断导致家属在一分钟内收到好几条报警短信。我加了一个冷却机制一次真正触发报警后5分钟内不再重复发送短信只更新通知栏状态。这个机制在真实风险事件中也能避免冗余信息干扰让家属更快聚焦第一条报警的位置和内容。5.3 权限与隐私的合规处理传感器本身不涉及隐私问题但定位和短信都是敏感能力必须慎重设计。我在做这个应用时坚持三个原则。第一权限最小化。只有用户明确在设置页里填写了紧急联系人电话并且主动打开了跌倒检测开关我才会申请定位和短信权限。应用不会在启动时把所有权限全部拉满而是按功能使用路径逐步申请。第二数据最小化。本地只保存最近几秒钟的传感器数据和最近一次定位缓存用于算法判定和日志回看不做长期行为轨迹记录更不会上传任何服务器。短信内容仅包含定位信息和一句固定求助语不携带手机上其他任何用户数据。第三知情与撤回。设置页里要明确说明每一类权限的用途。用户关闭跌倒检测开关后前台服务和定位缓存立即停止更新已存储的缓存数据也要提供一键清空入口。合规处理看似增加工作量但对健康类应用来说这是上线前的底线用户对这类应用的安全感比功能丰富度更敏感。6. 这个原型还能往哪些方向扩展原型稳定运行之后我琢磨过不少扩展方向如果是在团队项目里继续往下走有几条路线相当值得尝试。把检测结果同步到可穿戴设备。现在很多人有手环或手表这些设备本身也有加速度计如果手机能通过低功耗蓝牙把手环的传感器数据也纳入算法那就能覆盖手机离身的盲区比如用户去洗澡时把手机放在客厅手环仍可以独立检测跌倒并通过手机发送警报。引入机器学习分类器。纯规则状态机在可解释性方面很强误报也好排查但对复杂动作的区分能力有上限。要是数据量够可以采集大量跌倒与非跌倒样本训练一个轻量分类器把合加速度之外的旋转角速度、姿态变化速率作为补充特征。我的经验是先把规则状态机跑稳再决定要不要叠加模型直接上模型反而会因为数据质量差而难以调优。现有的三段式状态机逻辑也可以移植到其他健康场景比如癫痫发作、突然晕厥等异常状态识别。这类动作同样有失去控制-撞击-异常静止的物理特征只是阈值和窗口参数不同。换一个场景往往只是换一组参数的问题。我也尝试过把算法嵌入到老人常用的健康管理类应用里作为后台守护模块。因为这套检测逻辑不需要常驻高功耗平时几乎零干扰很适合作为通用健康API提供出去让更多应用在不开发底层传感器逻辑的前提下获得跌倒检测能力。最后说点个人体会。我在项目里调误报参数那阵子最深的感触是算法不能只对着实验室数据调得跟着真人测试走。第一次联调时我在软垫上模拟跌倒把阈值调到太灵敏结果从床上坐起来都触发报警家属收到好几次假消息那叫一个尴尬。后来我养成了两个习惯一是所有参数做成可调并保留本地日志二是在设置界面里加了一个检测灵敏度选项让使用者根据自己的状态调整。这些小功能不复杂但决定了这个原型能不能真的被长期使用。做健康监护类应用说到底拼的不是某一版算法有多惊艳而是它在真实琐碎的生活里能不能稳定当好那个不添乱的守护者。