Skip to main content

Android 爛裝置想跑人臉辨識 ?:物理世界需要 watchdog

Android 爛裝置想跑人臉辨識 6 – 物理世界需要 Watchdog

《這不是 App,這是一台掛在牆上的死機製造機》

我們不是在做給手機用戶滑的 App。手機 App 閃退了,大不了用戶罵句髒話,手指點一下重新打開。 我們做的是一台掛在牆上、風吹日曬的人臉辨識門禁機。開機就跑模型,模型就要活。當它在半夜三點因為網路死鎖、相機斷流、或者被 OOM 砍掉時,難道你要半夜騎車去現場拔電源?

老闆會先殺了你。

所以,我們需要一個看門狗 (Watchdog)。而且是一個真正具有「不死之身」的看門狗。

靈魂拷問:為什麼不直接上硬體 MCU 看門狗?

如果把我們這套架構拿去給做大廠高階人臉辨識機的工程師看,他大概會皺著眉頭說:「你這做法太克難了吧?」 沒錯,市面上主流的高級門禁機(如 RK3399 配 4G RAM),主板上絕對會標配一顆獨立的 MCU。Android 死機?MCU 沒收到心跳直接硬體重置,優雅又穩定。外置 MCU 才是終極的硬體防禦。

那我們為什麼不用? 一句話:BOM 表成本與歷史業障。 我們面對的是高通 A215 加上可憐的 2GB RAM,主板早就洗好了。老闆為了省那幾毛錢的美金成本,硬是把這項終極防禦機制給拔了。身為軟體工程師,既然無法改變已經成型的硬體,我們只能在有限的爛牌裡,用軟體把看門狗逼到極限。

架構抉擇:為什麼放棄 Service,改用靜態廣播?

在主流做法中,設備商通常會把 Watchdog 寫成獨立的 C++ Native Daemon,跑在 Linux 底層,完全不受 Android 系統干擾。但受限於 BSP 開發成本,我們的看門狗只能掛在 Java 層。

很多人直覺會寫一個 RestartService 放背景跑無窮迴圈定時檢查。如果你手上有 8GB RAM,或許可以。但在這台一開機系統就吃掉 1.3GB 的破銅爛鐵上,當活體檢測模型吃滿記憶體時,Android 內建的 Low-Memory Killer (LMKD) 是六親不認的。 一旦發生 OOM,你的 App 進程瞬間被秒殺。依附在 App 上的 Service 看門狗也會跟著下地獄。看門狗自己都死了,還看個屁門?

所以,本專案唯一的解法是:改用靜態註冊的 SystemEventReceiver

什麼叫靜態註冊?就是寫死在 AndroidManifest.xml 裡的東西。它的霸道之處在於:即使你的 App 已經被系統殺得連灰都不剩,只要 Android 系統收到定時的 Alarm 廣播,系統就會被迫把你的 App 從墳墓裡挖出來,重新拉起進程來執行。這才是窮人的「不死之身」。

三大監控機制:大腦、眼睛、嘴巴

我們這隻從墳墓爬出來的 SystemEventReceiver 收到廣播後,一律丟給背景 ThreadPoolManager 非同步執行避免 ANR。人臉門禁機要能正常運作,只有三個核心要素。只要瞎了、啞了、或腦死了,這台機器就是廢鐵。我們只死盯著這三個硬指標:

1. 網路死鎖(嘴巴啞了)

門禁機是 IoT 設備,沒網路就不能打卡,等於直接變磚。但你以為呼叫 API 拿到 isConnected = true 就代表網路暢通嗎?錯了,當網卡晶片當機,Android 系統一樣會跟你說連線正常,這叫「假連線」盲區。 我們不信 API,直接去聽更底層 Linux 的 NUD (Neighbor Unreachability Detection) 廣播。如果系統底層連續 60 秒發 ARP 單播封包都沒人鳥你(找不到網關),這就是物理死鎖,準備重啟。

2. 進程死亡(大腦死了)

系統沒死,但你的 App 被 OOM 殺了。此時機器螢幕可能只剩桌面,路人站到面前毫無反應。 所以我們用 AlarmManager 每 15 秒發送定時心跳。收到心跳後第一件事:檢查主進程 PID 還在不在? 如果 PID 消失了,代表大腦被砍,秒觸發重啟。

3. 相機斷流(眼睛瞎了,與兩段式救援)

這是最陰險的 Bug。App 活著,網路也通,但 USB 相機驅動當機。這時候如果你去呼叫 API,系統可能還會回報「Camera is Open」,但畫面上只剩凍結的殘影,機器已經是個瞎子了。

我們怎麼確認相機死機? 唯一的真理是「有沒有收到畫面」。我們在相機的預覽回調中埋點,只要收到新影像,就更新系統時間戳 System.currentTimeMillis()。 看門狗心跳到來時,我們只做減法:把現在時間減去「最後一次收到畫面的時間戳」。如果相機明明開著,這時間差卻大於 30 秒,管你 API 狀態多正常,這相機已經死了。

遇到相機瞎了怎麼辦?兩段式降維打擊:

  • 第一段(局部電擊):我們不會立刻重啟整台機器。我們寫了一支 usbRgbHwReset(),直接 Call 驅動層,把專門供電給 USB 相機的 GPIO 引腳拉低(斷電)500 毫秒後再拉高。這等於物理上「拔掉 USB 線再重插」,通常相機就能滿血復活。
  • 第二段(同歸於盡):如果局部重啟超過容忍極限(RGB_RESET_LIMIT),代表整個主板的 USB 總線 (USB Bus) 徹底死鎖了。這時我們才會無情宣判死刑,進入全機重啟。

 

當這三個指標有任何一個亮起致命紅燈(或是相機局部急救失敗),我們就會呼叫底層驅動,把主板上的 GPIO 48 拉高,強制切斷主板電源再重新上電。這就等於你親手去拔了機器的插頭。

 

 

如果你也是在這種極限環境下求生的可憐工程師,請記得: 我們做的事,不是讓系統跑得多優雅,而是確保它在最爛的硬體上,遭遇最慘的狀況時,還能自己爬起來繼續幹活。如果下一代專案老闆還是不肯加 MCU,至少拿這篇文章去砸他的臉。