基于Android+树莓派+充电宝的断电断网告警方案

常在家里搭物理服务器的朋友都知道,意外是一定会发生的。不论是偶尔的全小区停电,还是网络意外断线,只要人一离开家,意外发生后就很难知道家里到底发生了什么。
于是,我就用一台Android手机,一块树莓派,一个充电宝,实现了一个简单的告警系统。

目标

  1. 出现问题时,应当立刻发送出告警信息。
  2. 出现问题时,应当立刻对家里的环境拍照,并上传。
  3. 和其他系统隔离,不能依赖家里的其他服务器。

硬件思路

硬件需求

  1. 一台Android手机
    1. 需要带sim卡,且sim卡要能上网。
    2. 需要允许USB调试。并且信任树莓派。
  2. 一块树莓派
    1. 需要安装Android ADB
  3. 充电宝
    1. 需要支持边充边放

连接方式是:充电插头接充电宝,充电宝接树莓派,树莓派接Android。

我的硬件

Android手机是一台红米10A。
这个设备不需要什么性能,基本上只要是Android手机就行。唯一需要注意的就是,如果设备过于老旧,可能会因为系统版本太低导致问题。(比如Android 11开始有些敏感页面就不再允许截图等等)

树莓派是一台3B
因为基本只是调用ADB和bash script,所以也没啥太高的要求。当然如果要进一步,比如要搞什么自动开启临时的内网穿透线路之类的话,那最好还是上树莓派4或者5。

充电宝是一台小米无线充电宝
毕竟是电池,所以首先是要大牌子,而且最好是要官方明确写明了支持边充边放。其次是有线输出的功率要够。


状态判断的思路

断电判断

如果发生断电,那么上游的路由器一定会断电。所以,不论是通过wifi还是有线连接,树莓派都可以通过定时任务监控与路由器的连通状态来判断是否发生断电。当发现路由器无法连接,则认为发生断电。

断网判断

在判断没有断电的情况下,只要连不上网,自然就是一般断网了。

缺陷和不足

诚然,这个判断存在漏洞。如果仅仅是路由器和树莓派之间的连接出现意外(比如网线被老鼠啃了),或者就是单纯的路由器由于意外关机,都会判断成断电。
但就我个人的感受来说,目前这个判断逻辑还是比较准确的。


处理思路

断电处理

断电时,由于wifi必然中断,那么手机上可以设置自动任务:当断开wifi,则连接4G网,且打开移动热点。
那么树莓派只需要保证在断网时自动连上热点,并在连上热点之后开始拍照发消息就好了。

断网告警

先不考虑后续自动调查以及搭建临时线路等等后续的问题,首先考虑告警的问题。

不同于断电,此时由于wifi正常工作,只是没有网络。所以Android和树莓派并不一定会主动断开Wifi。

对于Android手机,主动断开Wifi是容易的。我们只需要通过ADB关闭掉手机的Wifi即可。剩下的问题是如何处理树莓派的网络以及如何发送消息。

对于树莓派,我的思路是:不做任何主动的网络操作,保持和Wifi或者路由的连接,也就是保持断网状态。
理由是:如果树莓派主动断开和路由器的连接,或者主动切换到Android的热点网络(因为断电那里的有自动任务,断开Wifi会自动开热点)。那么后续一定会需要重新切换回一般线路,而这个切换并不是从一个稳定的网络到一个稳定的网络。一旦重新切换回一般的线路失败,那么就不得不需要额外的人工介入。
因此,对于树莓派,只要能和路由连接,就尽可能不做任何操作。只有当和路由无法连接(也就是断电的场景)才由操作系统自动连接其他低优先级的网络(手机热点)。

因为树莓派还是会尝试通过路由器联网,所以树莓派无法发送消息。所以唯一能联网的设备就是Android手机。所以发送消息就需要由Android手机直接发送。
如果Android手机有预安装好curl,那自然是用ADB操作shell执行curl命令即可。
但我的手机只有netcat,连HTTPS也比较困难,也不想对手机做太多的定制。所以我的做法是云上的服务器开了一个额外的Get API(当然是带认证鉴权的),然后用ADB操作浏览器访问这个API。

断网分析

对于断网的场景,可能不只有一个原因。有可能是运营商的问题,也可能是光猫的问题,也可能是路由本身的问题。

如果是路由的问题,那么整个内网应该都无法正常工作,而不仅仅是无法连接公网。那么这个可以根据内网的状态很容易判断出来。
如果不是路由的问题,那么在上游的就只有光猫和运营商了。这个我确实没想到什么判断的方式。

总之至少我们可以判断到底是路由本身的问题,还是宽带相关的问题。


一些小的技术点

这里有一些我实现时用到的命令,也放在这里。

ADB 相关

# 唤醒和休眠
adb shell input keyevent KEYCODE_WAKEUP
adb shell input keyevent KEYCODE_SLEEP
# 按Home键
adb shell input keyevent KEYCODE_HOME

# 开关wifi和移动数据
adb shell svc wifi disable
adb shell svc wifi enable
adb shell svc data disable
adb shell svc data enable

# 打开拍照
adb shell am start -a android.media.action.STILL_IMAGE_CAMERA
# 执行拍照
adb shell input keyevent KEYCODE_CAMERA

# 从手机上复制文件到本地
adb pull <android_path> <local_path>

# 在浏览器中打开某个url(新标签页)
adb shell am start -a android.intent.action.VIEW -d "'$url'" --ez create_new_tab true

网络相关

# curl访问网址并返回response code
curl --silent --output /dev/null --write-out "%{http_code}" https://example.com

# 获取当前连接到的Wifi的SSID(也就是Wifi名称)
iwgetid -r