Demo:三軸加速度與狀態(tài)機(jī)報(bào)警鏈路)
簡(jiǎn)介面向 Android 開(kāi)發(fā)與計(jì)算機(jī)視覺(jué)入門(mén)者的跌倒檢測(cè)識(shí)別演示程序聚焦移動(dòng)端實(shí)時(shí)摔倒識(shí)別這一常見(jiàn)場(chǎng)景可用于課程設(shè)計(jì)、畢業(yè)項(xiàng)目原型驗(yàn)證或算法落地體驗(yàn)兼顧初學(xué)者與有一定移動(dòng)端開(kāi)發(fā)經(jīng)驗(yàn)的讀者。zip 壓縮包僅含 2 個(gè)文件以 1 個(gè) APK 安裝包和 1 個(gè) JSON 輔助文件為主前者可直接安裝體驗(yàn)跌倒檢測(cè)流程后者用于記錄構(gòu)建輸出等信息整體約 50.44MB體積輕量獲取后即可上手。目前已有 1946 人學(xué)習(xí)下載說(shuō)明該方向具有穩(wěn)定的實(shí)踐需求。對(duì)希望快速了解 Android 端跌倒檢測(cè)效果的讀者而言可通過(guò)實(shí)際運(yùn)行演示觀(guān)察識(shí)別流程與交互表現(xiàn)并以此為參照評(píng)估自身方案在移動(dòng)端的可行性適合作為入門(mén)體驗(yàn)與原型參考。1. 跌倒檢測(cè) Demo 的第一印象它不是一個(gè)“摔倒就報(bào)警”的按鈕給家里老人手機(jī)裝一個(gè) Android 跌倒檢測(cè) Demo最直觀(guān)的驗(yàn)證場(chǎng)景是手機(jī)放在口袋里人快速下蹲再起身App 不能亂響模擬一次自由落體加撞擊它要在幾秒內(nèi)彈窗或響鈴。這個(gè) Demo 解決的就是把三軸加速度、姿態(tài)角和狀態(tài)機(jī)串成一條可運(yùn)行的跌倒識(shí)別鏈路適合想快速驗(yàn)證傳感器方案、準(zhǔn)備做老人看護(hù)或戶(hù)外作業(yè)監(jiān)護(hù)的 Android 開(kāi)發(fā)者。它不是一上來(lái)就堆攝像頭模型而是先把摔倒識(shí)別最核心的采集、判定、報(bào)警跑通后面再擴(kuò)展視頻跌倒檢測(cè)和識(shí)別也更容易。2. 跌倒識(shí)別工程骨架SensorManager 加前臺(tái)服務(wù)的最小閉環(huán)2.1 目錄結(jié)構(gòu)一個(gè)能跑的最小閉環(huán)這個(gè) Demo 的工程目標(biāo)很克制把 Android 跌倒檢測(cè) 從傳感器注冊(cè)到通知彈窗跑成一條閉環(huán)不追求一上來(lái)就做深度學(xué)習(xí)分類(lèi)。常見(jiàn)目錄會(huì)按職責(zé)拆成 sensor、detector、service、alarm、ui 五塊先把數(shù)據(jù)采集和判定分開(kāi)后面換算法不用動(dòng)界面。路徑職責(zé)改動(dòng)頻率sensor/AccelCollector.kt注冊(cè)加速度計(jì)、統(tǒng)一采樣率、把SensorEvent轉(zhuǎn)成數(shù)組低detector/FallDetector.kt濾波、閾值、狀態(tài)機(jī)、輸出跌倒事件高service/FallMonitorService.kt前臺(tái)服務(wù)?;睢鞲衅魃芷?、報(bào)警分發(fā)中alarm/AlarmBus.kt本地通知、震動(dòng)、頁(yè)面回調(diào)的解耦入口中ui/MainActivity.kt權(quán)限申請(qǐng)、開(kāi)始/停止監(jiān)測(cè)、倒計(jì)時(shí)取消中res/layout/activity_main.xml一個(gè)開(kāi)始按鈕、一個(gè)狀態(tài)文本、一個(gè)日志區(qū)低真正影響跌倒識(shí)別效果的只有detector和service兩個(gè)目錄。很多初版 Demo 把傳感器注冊(cè)寫(xiě)在 Activity 里屏幕一關(guān)就收不到數(shù)據(jù)后面排查半天以為是算法問(wèn)題其實(shí)是生命周期沒(méi)管住。我一般會(huì)把采集放進(jìn)前臺(tái)服務(wù)Activity 只負(fù)責(zé)權(quán)限和開(kāi)關(guān)這樣鎖屏、切后臺(tái)都不容易翻車(chē)。2.2 技術(shù)棧拆解為什么用三軸加速度而不是一上來(lái)就上攝像頭選型上三軸加速度計(jì)是跌倒識(shí)別最省電的入口。手機(jī)放在口袋或腰間自由落體、撞擊、靜止三個(gè)物理階段都能在加速度曲線(xiàn)上看到。攝像頭方案能看姿態(tài)但功耗、隱私、角度遮擋都是成本端側(cè)姿態(tài)關(guān)鍵點(diǎn)模型適合做二次校驗(yàn)不適合作為唯一觸發(fā)。這個(gè) Demo 用SensorManager拿TYPE_ACCELEROMETER配合前臺(tái)服務(wù)和本地通知先把“摔倒識(shí)別”最剛需的鏈路跑通。依賴(lài)配置不用堆太多庫(kù)常見(jiàn)做法是只保留核心和界面基礎(chǔ)庫(kù)// app/build.gradle.kts dependencies { implementation(androidx.core:core-ktx) implementation(androidx.appcompat:appcompat) implementation(androidx.lifecycle:lifecycle-service) // 需要曲線(xiàn)調(diào)試時(shí)再加圖表庫(kù)不要在首版塞進(jìn)來(lái) }這段配置不寫(xiě)具體版本號(hào)是為了讓工程跟著本機(jī) Android Studio 的模板走。lifecycle-service主要給前臺(tái)服務(wù)用方便在服務(wù)里拿生命周期如果 Demo 里沒(méi)有用到刪掉也不影響傳感器采集。依賴(lài)同步失敗時(shí)先看倉(cāng)庫(kù)網(wǎng)絡(luò)和鏡像配置不要先懷疑代碼。清單文件里要提前聲明前臺(tái)服務(wù)和通知權(quán)限!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_HEALTH / uses-permission android:nameandroid.permission.POST_NOTIFICATIONS / uses-permission android:nameandroid.permission.WAKE_LOCK / service android:name.service.FallMonitorService android:foregroundServiceTypehealth android:exportedfalse /foregroundServiceTypehealth是新系統(tǒng)對(duì)健康類(lèi)前臺(tái)服務(wù)的類(lèi)型聲明不寫(xiě)會(huì)在啟動(dòng)時(shí)拋異常。POST_NOTIFICATIONS在較新系統(tǒng)上必須運(yùn)行時(shí)申請(qǐng)否則報(bào)警通知只會(huì)在通知欄外面“路過(guò)”一下。WAKE_LOCK不是讓你一直持有而是在沖擊確認(rèn)后的短窗口里保證 CPU 不睡死報(bào)警發(fā)完就釋放。提示監(jiān)控入口最好從可見(jiàn)界面觸發(fā)不要靠開(kāi)機(jī)廣播直接拉起前臺(tái)服務(wù)新系統(tǒng)對(duì)后臺(tái)啟動(dòng)限制很?chē)?yán)。2.3 檢測(cè)鏈路采集、濾波、閾值、狀態(tài)機(jī)、報(bào)警一條完整的跌倒檢測(cè)和識(shí)別鏈路通常按 50Hz 采樣先做低通濾波分離重力再算合加速度和姿態(tài)角。狀態(tài)機(jī)比單閾值靠譜自由落體、沖擊、靜止確認(rèn)、報(bào)警中間任何一步不滿(mǎn)足就回空閑。常見(jiàn)參數(shù)是自由落體閾值 0.5 到 0.6g沖擊閾值 2.5 到 3.0g靜止窗口 2 到 3 秒姿態(tài)角變化 50 到 60 度。具體值跟手機(jī)佩戴位置有關(guān)別照抄。鏈路可以拆成五步采集SensorManager.registerListener注冊(cè)加速度計(jì)采樣率用SENSOR_DELAY_GAME大約 50Hz夠看清沖擊尖峰。濾波用一階低通分離重力分量原始值減重力得到線(xiàn)性加速度合加速度仍然用原始三軸計(jì)算。判定先看合加速度是否掉到自由落體區(qū)間再看是否出現(xiàn)短時(shí)高沖擊最后看沖擊后是否持續(xù)靜止且姿態(tài)角明顯改變。確認(rèn)狀態(tài)機(jī)進(jìn)入靜止確認(rèn)后等 2 到 3 秒期間線(xiàn)性加速度不能太大否則回空閑。報(bào)警本地通知加震動(dòng)頁(yè)面彈 10 秒倒計(jì)時(shí)用戶(hù)點(diǎn)“我沒(méi)事”就取消不點(diǎn)再走緊急聯(lián)系人。功耗上50Hz 對(duì)加速度計(jì)壓力不大真正耗電的是屏幕常亮和網(wǎng)絡(luò)請(qǐng)求。Demo 階段建議只在報(bào)警時(shí)發(fā)網(wǎng)絡(luò)請(qǐng)求平時(shí)只寫(xiě)本地日志。誤報(bào)抑制也別只靠調(diào)閾值加入“沖擊后靜止”和“姿態(tài)角改變”兩個(gè)條件坐下、起身、把手機(jī)扔桌上這類(lèi)動(dòng)作會(huì)少很多。后面第 4 章會(huì)講幾個(gè)真機(jī)調(diào)試時(shí)最容易踩的坑。3. 跌倒檢測(cè)和識(shí)別核心三軸加速度采集與狀態(tài)機(jī)判定3.1 傳感器采集與滑動(dòng)窗口濾波先看判定類(lèi)。它不直接持有SensorManager只接收三軸數(shù)組和時(shí)間戳這樣單元測(cè)試和離線(xiàn)回放都能復(fù)用。下面這段是 Demo 里最核心的FallDetector把濾波、閾值、狀態(tài)機(jī)放在一個(gè)類(lèi)里方便新手順著讀。// detector/FallDetector.kt class FallDetector( private val onFall: (FallEvent) - Unit ) { enum class State { IDLE, FREEFALL, IMPACT, STILL, ALARM } private var state State.IDLE private val gravity FloatArray(3) private var baseline floatArrayOf(0f, 0f, 9.81f) private var freeFallAt 0L private var impactAt 0L private var stillAt 0L private var lastAt 0L fun reset() { state State.IDLE lastAt 0L } fun process(values: FloatArray, now: Long) { if (lastAt 0L) { lastAt now // 第一次采樣把當(dāng)前重力方向當(dāng)作佩戴基線(xiàn) for (i in 0..2) gravity[i] values[i] baseline gravity.copyOf() return } lastAt now // 一階低通分離重力0.8 越大越穩(wěn)越小越靈敏 for (i in 0..2) { gravity[i] 0.8f * gravity[i] 0.2f * values[i] } val rawG sqrt( values[0] * values[0] values[1] * values[1] values[2] * values[2] ) / 9.81f val linearMax sqrt( (values[0] - gravity[0]) * (values[0] - gravity[0]) (values[1] - gravity[1]) * (values[1] - gravity[1]) (values[2] - gravity[2]) * (values[2] - gravity[2]) ) / 9.81f val angle angleBetween(gravity, baseline) when (state) { State.IDLE - { if (rawG 0.55f) { state State.FREEFALL freeFallAt now } } State.FREEFALL - { if (rawG 2.6f now - freeFallAt 1200) { state State.IMPACT impactAt now } else if (now - freeFallAt 1500) { state State.IDLE } } State.IMPACT - { if (now - impactAt 1200) { state State.IDLE } else if (rawG in 0.85f..1.15f) { state State.STILL stillAt now } } State.STILL - { val still rawG in 0.8f..1.2f linearMax 0.25f if (!still) { state State.IDLE } else if (now - stillAt 2500 angle 55f) { state State.ALARM onFall( FallEvent( time now, peakG rawG, angle angle, reason 沖擊后靜止且姿態(tài)改變 ) ) } } State.ALARM - { // 等頁(yè)面取消或上層調(diào)用 reset不在這里重復(fù)上報(bào) } } } private fun angleBetween(a: FloatArray, b: FloatArray): Float { val dot a[0] * b[0] a[1] * b[1] a[2] * b[2] val ma sqrt(a[0] * a[0] a[1] * a[1] a[2] * a[2]) val mb sqrt(b[0] * b[0] b[1] * b[1] b[2] * b[2]) if (ma 0f || mb 0f) return 0f val cos (dot / (ma * mb)).coerceIn(-1f, 1f) return Math.toDegrees(acos(cos).toDouble()).toFloat() } } data class FallEvent( val time: Long, val peakG: Float, val angle: Float, val reason: String )這段代碼的關(guān)鍵在三個(gè)量rawG是原始合加速度用來(lái)抓自由落體和沖擊linearMax是去掉重力后的線(xiàn)性加速度用來(lái)判斷沖擊后是否真的靜下來(lái)angle是當(dāng)前重力方向與佩戴基線(xiàn)的夾角用來(lái)區(qū)分“手機(jī)還在手里晃”和“人已經(jīng)躺下”。gravity的 0.8 系數(shù)是低通濾波的平滑度調(diào)大后重力估計(jì)更穩(wěn)但姿態(tài)變化反應(yīng)慢調(diào)小后反應(yīng)快靜止時(shí)容易抖。狀態(tài)機(jī)不能省。單看一個(gè) 3g 的沖擊就報(bào)警拍桌子、跑步落地都會(huì)中招單看自由落體也不夠手機(jī)從手里滑到沙發(fā)上也會(huì)觸發(fā)自由落體。FREEFALL到IMPACT限時(shí) 1.2 秒是防止下落信號(hào)和沖擊信號(hào)隔太久IMPACT到STILL限時(shí) 1.2 秒是防止人已經(jīng)站起來(lái)才進(jìn)入靜止判斷STILL持續(xù) 2.5 秒且角度超過(guò) 55 度才上報(bào)是給誤報(bào)加一道閘。參數(shù)可以按下面這張表調(diào)參數(shù)默認(rèn)值作用調(diào)大后的影響調(diào)小后的影響自由落體閾值0.55g判斷失重更難觸發(fā)漏報(bào)增加更容易觸發(fā)誤報(bào)增加沖擊閾值2.6g判斷撞擊漏掉輕摔日常動(dòng)作容易中招靜止窗口2500ms確認(rèn)人不再動(dòng)報(bào)警更慢可能人還沒(méi)停就報(bào)警姿態(tài)角閾值55 度判斷手機(jī)姿態(tài)改變漏掉平躺角度小的場(chǎng)景坐下也可能觸發(fā)線(xiàn)性加速度上限0.25g判斷是否靜止更容易進(jìn)入靜止更難進(jìn)入靜止3.2 前臺(tái)服務(wù)與傳感器生命周期判定類(lèi)只負(fù)責(zé)算真正讓它在鎖屏后繼續(xù)跑的是前臺(tái)服務(wù)。下面這段把傳感器注冊(cè)、線(xiàn)程切換、通知和事件分發(fā)串起來(lái)。HandlerThread用來(lái)避開(kāi)主線(xiàn)程傳感器回調(diào)本來(lái)就在獨(dú)立線(xiàn)程但統(tǒng)一放到一個(gè) worker 里更好排查。// service/FallMonitorService.kt class FallMonitorService : Service(), SensorEventListener { private lateinit var sensorManager: SensorManager private var accelerometer: Sensor? null private lateinit var detector: FallDetector private lateinit var worker: HandlerThread private lateinit var handler: Handler override fun onCreate() { super.onCreate() startForeground(1001, buildMonitorNotification()) worker HandlerThread(fall-sensor).also { it.start() } handler Handler(worker.looper) sensorManager getSystemService(SENSOR_SERVICE) as SensorManager accelerometer sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) detector FallDetector { event - publishAlarm(event) } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val sensor accelerometer if (sensor null) { // 設(shè)備沒(méi)有加速度計(jì)直接停掉別讓用戶(hù)以為在監(jiān)控 stopSelf() return START_NOT_STICKY } sensorManager.registerListener( this, sensor, SensorManager.SENSOR_DELAY_GAME, handler ) return START_STICKY } override fun onSensorChanged(event: SensorEvent) { // elapsedRealtime 不受用戶(hù)改時(shí)間影響狀態(tài)機(jī)時(shí)間戳用它 detector.process(event.values, SystemClock.elapsedRealtime()) } override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) Unit private fun publishAlarm(event: FallEvent) { val manager getSystemService(NOTIFICATION_SERVICE) as NotificationManager manager.notify(1002, buildAlarmNotification(event)) AlarmBus.emit(event) } override fun onDestroy() { sensorManager.unregisterListener(this) worker.quitSafely() super.onDestroy() } }SENSOR_DELAY_GAME是采樣率參數(shù)大約 50Hz比SENSOR_DELAY_UI快又不像SENSOR_DELAY_FASTEST那樣耗電。SystemClock.elapsedRealtime()比System.currentTimeMillis()穩(wěn)用戶(hù)改系統(tǒng)時(shí)間不影響狀態(tài)機(jī)計(jì)時(shí)。START_STICKY讓服務(wù)被系統(tǒng)回收后有機(jī)會(huì)重建但別把它當(dāng)?;钌衿髑芭_(tái)通知和電池優(yōu)化白名單才是正路。publishAlarm里先發(fā)本地通知再走AlarmBus給頁(yè)面。通知和頁(yè)面回調(diào)分開(kāi)是為了后面接短信、電話(huà)、網(wǎng)絡(luò)上報(bào)時(shí)不用改判定類(lèi)。通知渠道要在buildMonitorNotification和buildAlarmNotification里提前建好渠道被用戶(hù)關(guān)掉后代碼里再notify也不會(huì)顯示。3.3 報(bào)警確認(rèn)與取消倒計(jì)時(shí)跌倒檢測(cè)最怕“報(bào)了但用戶(hù)沒(méi)法取消”。Demo 里給一個(gè) 10 秒倒計(jì)時(shí)用戶(hù)點(diǎn)“我沒(méi)事”就取消點(diǎn)不到再走緊急邏輯。這個(gè)交互比單純彈通知更接近真實(shí)產(chǎn)品。// ui/MainActivity.kt 中的報(bào)警確認(rèn) private fun showFallCountdown(event: FallEvent) { val dialog AlertDialog.Builder(this) .setTitle(檢測(cè)到跌倒) .setMessage(${event.reason}10 秒后通知緊急聯(lián)系人) .setNegativeButton(我沒(méi)事) { d, _ - d.dismiss() AlarmBus.cancel(event) } .create() dialog.show() handler.postDelayed({ if (dialog.isShowing) { dialog.dismiss() sendEmergency(event) // 這里再接短信、電話(huà)或自己的上報(bào)接口 } }, 10_000) }倒計(jì)時(shí)用Handler.postDelayed就夠了不需要額外引調(diào)度庫(kù)。AlarmBus.cancel(event)里把當(dāng)前事件標(biāo)記為已取消服務(wù)端和頁(yè)面都不要再重復(fù)彈窗。sendEmergency是二次開(kāi)發(fā)接出去的口子Demo 里可以先寫(xiě)日志后面換成自己的接口。注意倒計(jì)時(shí)期間如果 Activity 被銷(xiāo)毀handler要跟著清理否則容易泄漏。4. 真機(jī)跑通與避坑摔倒識(shí)別 Demo 的 5 個(gè)高頻翻車(chē)點(diǎn)4.1 運(yùn)行步驟從導(dǎo)入工程到模擬一次跌倒拿到壓縮包后先用 Android Studio 打開(kāi)工程根目錄等依賴(lài)同步完。不要直接在模擬器上點(diǎn)開(kāi)始加速度計(jì)在模擬器里通常是固定值狀態(tài)機(jī)永遠(yuǎn)進(jìn)不了自由落體。真機(jī)開(kāi)啟開(kāi)發(fā)者模式和 USB 調(diào)試連上后跑app模塊。首次啟動(dòng)按這個(gè)順序走授予通知權(quán)限沒(méi)有通知權(quán)限時(shí)先別點(diǎn)開(kāi)始監(jiān)測(cè)。點(diǎn)“開(kāi)始監(jiān)測(cè)”確認(rèn)通知欄出現(xiàn)常駐通知。把手機(jī)放口袋里走動(dòng) 30 秒看日志里rawG是否在 0.8 到 1.2 之間波動(dòng)。模擬跌倒手持手機(jī)快速下墜到軟墊別真摔手機(jī)觀(guān)察狀態(tài)是否從IDLE走到IMPACT。鎖屏 1 分鐘再重復(fù)一次確認(rèn)前臺(tái)服務(wù)還在采數(shù)據(jù)。模擬跌倒時(shí)自由落體階段要短暫沖擊要明顯。用手把手機(jī)從 30 厘米高度松到沙發(fā)墊上通常能打出 2g 以上沖擊如果日志里rawG峰值只有 1.5g先把沖擊閾值降到 2.2g 試一次再?zèng)Q定是否改硬件佩戴方式。4.2 常見(jiàn)問(wèn)題排查現(xiàn)象、原因、解決現(xiàn)象一模擬器上日志一直不動(dòng)狀態(tài)機(jī)永遠(yuǎn)空閑。原因模擬器沒(méi)有真實(shí)加速度計(jì)SensorEvent返回固定數(shù)組。 解決換真機(jī)必須在模擬器上演示時(shí)用傳感器注入工具手動(dòng)喂三軸數(shù)據(jù)但別拿注入結(jié)果判斷算法好壞。現(xiàn)象二屏幕一鎖跌倒不報(bào)警。原因傳感器注冊(cè)在 Activity界面不可見(jiàn)后回調(diào)被停或者服務(wù)被系統(tǒng)限制。 解決把注冊(cè)放進(jìn)前臺(tái)服務(wù)通知類(lèi)型聲明正確測(cè)試機(jī)把 App 加入電池優(yōu)化白名單。鎖屏后看onSensorChanged是否還有日志沒(méi)有日志就是生命周期問(wèn)題不是閾值問(wèn)題?,F(xiàn)象三坐著起身、把手機(jī)放桌上也報(bào)警。原因只看了沖擊閾值沒(méi)有靜止確認(rèn)和姿態(tài)角或者沖擊閾值太低。 解決把沖擊閾值從 2.2g 提到 2.6g 以上靜止窗口加到 2500ms姿態(tài)角閾值加到 55 度。先錄一段正常動(dòng)作的 CSV回放看誤報(bào)點(diǎn)落在哪個(gè)狀態(tài)別盲調(diào)?,F(xiàn)象四報(bào)警時(shí)通知欄沒(méi)有彈窗。原因沒(méi)有申請(qǐng)POST_NOTIFICATIONS或者通知渠道被用戶(hù)關(guān)掉或者渠道重要性太低。 解決啟動(dòng)時(shí)運(yùn)行時(shí)申請(qǐng)權(quán)限在設(shè)置頁(yè)引導(dǎo)用戶(hù)檢查通知渠道報(bào)警通知用高重要性渠道并加震動(dòng)。代碼里notify不報(bào)錯(cuò)不代表用戶(hù)能看到?,F(xiàn)象五registerListener返回 false或者服務(wù)啟動(dòng)就崩。原因設(shè)備沒(méi)有加速度計(jì)采樣率不支持或者前臺(tái)服務(wù)類(lèi)型沒(méi)聲明。 解決先判斷getDefaultSensor是否為 null為空就提示設(shè)備不支持前臺(tái)服務(wù)類(lèi)型按健康類(lèi)聲明把registerListener的返回值打日志失敗時(shí)降一檔采樣率再試。4.3 用日志和曲線(xiàn)驗(yàn)證閾值調(diào)閾值最忌“憑感覺(jué)”。我一般會(huì)在onSensorChanged里把時(shí)間戳、三軸、rawG、狀態(tài)寫(xiě)進(jìn) CSV跑完導(dǎo)出來(lái)看曲線(xiàn)。正常走路是 0.8 到 1.2g 的小波動(dòng)自由落體有一段明顯低于 0.6g沖擊是一個(gè)尖峰靜止后回到 1g 附近。把這三個(gè)階段在曲線(xiàn)上標(biāo)出來(lái)閾值就有依據(jù)。# 真機(jī)調(diào)試時(shí)抓日志過(guò)濾自己的 tag adb logcat -s FallDetector:V FallService:V-s后面跟的是日志 tag代碼里用Log.d(FallDetector, ...)打狀態(tài)遷移用Log.d(FallService, ...)打傳感器注冊(cè)結(jié)果。日志里重點(diǎn)看狀態(tài)順序是不是IDLE - FREEFALL - IMPACT - STILL - ALARM如果經(jīng)常FREEFALL直接回IDLE說(shuō)明自由落體窗口太長(zhǎng)或沖擊沒(méi)達(dá)到閾值。CSV 回放比真機(jī)反復(fù)摔手機(jī)靠譜也更容易對(duì)比兩套參數(shù)。5. 二次開(kāi)發(fā)把誤報(bào)壓下去并把報(bào)警接出去第一版跑通后最值得改的不是界面而是誤報(bào)和報(bào)警出口。單閾值狀態(tài)機(jī)在實(shí)驗(yàn)室里夠用放到真實(shí)場(chǎng)景里會(huì)遇到跑步、下樓梯、坐電梯。我一般會(huì)先加特征不急著換模型滑動(dòng)窗口里算均值、方差、峰值、過(guò)零率、姿態(tài)角變化再用一個(gè)輕量分類(lèi)器做二次確認(rèn)。特征和狀態(tài)機(jī)不沖突狀態(tài)機(jī)負(fù)責(zé)觸發(fā)候選分類(lèi)器負(fù)責(zé)壓誤報(bào)。改造點(diǎn)做法驗(yàn)證指標(biāo)多特征分類(lèi)2 秒窗口提取均值、方差、峰值、過(guò)零率、姿態(tài)角召回率、每天誤報(bào)次數(shù)場(chǎng)景判別用屏幕狀態(tài)、位置傳感器區(qū)分口袋、手持、桌面誤報(bào)下降比例報(bào)警外發(fā)定義FallAlarmSink接口本地通知和遠(yuǎn)端上報(bào)解耦斷網(wǎng)重試、重復(fù)上報(bào)是否冪等離線(xiàn)回放CSV 錄制與回放跑同一套判定類(lèi)混淆矩陣、狀態(tài)遷移次數(shù)接報(bào)警出口時(shí)別把網(wǎng)絡(luò)請(qǐng)求寫(xiě)進(jìn)FallDetector。判定類(lèi)只吐FallEvent服務(wù)層決定發(fā)通知還是調(diào)接口。下面這個(gè)接口就夠用interface FallAlarmSink { fun onFall(event: FallEvent) fun onCancel(event: FallEvent) }onFall里做本地通知、震動(dòng)、上報(bào)onCancel里把已經(jīng)排隊(duì)的緊急動(dòng)作撤掉。網(wǎng)絡(luò)上報(bào)要加冪等鍵避免重試導(dǎo)致重復(fù)通知。驗(yàn)證時(shí)先錄 20 組正常動(dòng)作和 10 組模擬跌倒離線(xiàn)回放跑參數(shù)再上真機(jī)。閾值不是玄學(xué)記錄曲線(xiàn)后就是調(diào)參問(wèn)題。從那以后我每次改閾值都先走一遍 CSV 回放再拿真機(jī)試不直接拿手機(jī)反復(fù)摔。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取