我花 ¥100 给沙发装了个 AI 驱猫系统:闲置手机 + Java 服务器 + ESP32,断网也能用

0 阅读14分钟

一、先说一个养猫人的真实痛点

我家那只狸花猫,叫"西兰花"。

西兰花什么都好,除了一个毛病——认定我们家三千块的布艺沙发是它的专属猫抓板。沙发扶手已经被它挠出了流苏,靠背上的猫毛能搓成毡,最气人的是白天我们上班不在家,它根本没人管。

买过成品驱猫器,要么是喷水的(把沙发喷得全是水渍),要么是插电超声波(贵,且号称的"智能感应"实际就是红外热释电,动不动误触,夜里自己响,邻居都来敲门)。

也想过装摄像头 + 云 AI 识别:一查,云摄像头月费 + 隐私全传到厂商服务器,直接劝退。而且我们小区晚上偶尔会断外网,断网=监控失灵,这怎么行?

于是决定自己造一个:

一个纯局域网运行的"猫咪禁区哨兵"——AI 识别猫咪跳上沙发 → 发射人耳听不到的超声波驱赶。全程离线,断外网照样干活,视频不出家门,总成本不到 ¥150。

这就是今天要介绍的 SofaCatGuard(沙发猫咪守卫) 项目,全部源码开源。


二、方案选型:为什么是「手机推流 + Java 服务器」?

动手之前我调研了市面上的技术路线,纠结了很久,也踩过坑。最终定型为 v3 架构,演进过程很值得一说:

版本方案问题
v1树莓派 + USB 摄像头 + 本地推理树莓派贵(要几百块)、摄像头另买,总成本下不来
v2手机本地跑 AI(端侧推理)旧手机 CPU 弱,识别帧率低、发热大;7×24 运行不现实
v3(本项目)闲置手机只推流,Java 服务器统一分析算力分离、手机负载极低、扩展性好,成本≈0

v3 的核心思路:手机只出画面,算力交给服务器。

  • 📱 闲置安卓手机:CameraX 采集画面 → 内置 MJPEG 服务器推流(纯采集编码,负载极低,发热小)
  • 🖥️ Java 分析服务器:旧电脑 / NAS / 树莓派 7×24 常驻,负责拉流 → 预处理 → YOLOv8 推理 → 业务判定 → 下发指令(算力远超手机,识别又快又准)
  • 🔌 ESP32 执行端:收指令 → 继电器 → 超声波驱猫

这个分工带来一个隐藏福利:以后想多路监控,只需多加一台手机推流,服务器统一分析,成本接近为零。


三、系统总览

┌─────────────────────┐   HTTP MJPEG    ┌──────────────────────────────┐
│   闲置安卓手机        │ ──────────────▶ │  Java 分析服务器(旧电脑/NAS)  │
│  CameraX 采集        │  640×480        │  FFmpeg 拉流(断线自动重连)     │
│  JPEG 编码           │  局域网 1~3Mbps │  CLAHE 暗光增强(夜间关键)      │
│  内置 MJPEG 服务器    │                 │  YOLOv8-Nano ONNX 推理          │
│  前台服务保活+开机自启 │                 │  ROI+停留+冷却 状态机判定         │
└─────────────────────┘                 │  MQTT 发布 ON/OFF               │
                                        └───────────────┬──────────────┘
                                                        │ MQTT (sofacatguard/cmd)
                                        ┌───────────────▼──────────────┐
                                        │  ESP32-WROOM-32               │
                                        │  订阅主题 → 继电器 → 超声波驱猫  │
                                        │  硬件兜底:最长工作 8 秒自动关    │
                                        └──────────────────────────────┘

数据流闭环:猫跳上沙发 → 手机画面经 MJPEG 推到服务器 → 服务器本地推理识别到 cat → 状态机判定"停留 ≥1.2s 且在禁区内" → MQTT 发 ON → ESP32 继电器闭合 → 超声波响起(人耳听不到)→ 8 秒后硬件兜底自动关闭 → 进入 30 秒冷却,防止扰民。

关键点:全程局域网,不依赖公网。断外网?无所谓,路由器还在就行。


四、硬件清单:淘宝关键词 + 总预算

全部硬件都是淘宝能买到的普通模块,我把采购关键词都整理好了(详见仓库 docs/淘宝购物清单.md):

#物品淘宝搜索关键词参考价
1红外补光灯(夜间必备)USB 5V 940nm红外补光灯 无红暴¥15~40
2ESP32-WROOM-32 开发板ESP32-WROOM-32 CH340开发板¥15~30
3光耦隔离继电器模块5V继电器模块 光耦隔离 低电平触发¥3~8
4超声波驱猫模块5V超声波驱猫器 人耳听不到¥10~40
5杜邦线杜邦线 母对母 20cm¥3~6

手机和服务器都用家里的闲置设备,不算钱。

  • 最低配置(复用旧电脑当服务器):约 ¥50~130
  • 完整配置(含 OTG 夜视摄像头 + 散热 + 电源):约 ¥120~280

对比一下:成品智能驱猫器动辄三四百,还带云服务月费;我这个成本不到它一半,还断网可用、零隐私风险、开源可魔改

⚠️ 两个硬性避雷点(我踩过的坑,文末还会细说):

  1. 红外补光灯必须 940nm(人眼完全不可见),别买 850nm 的——夜里有个刺眼的红点,猫没赶走先把人吓一跳;
  2. 超声波模块要买 5V 直驱的裸模块,让继电器控制通断;别买插 220V 的成品驱猫器,那东西没法安全地用继电器控制。

五、技术拆解:三端如何协作

5.1 手机端:一个"抠门"到极致的 MJPEG 推流服务器

手机端用 Jetpack CameraX 采集,JPEG 压缩后由 App 内置的 HTTP 服务器以 multipart/x-mixed-replace 方式推流。这部分代码很值得讲,因为它是为"7×24 运行在旧手机上"这个目标服务的,处处都在省电、防阻塞:

亮点一:非阻塞写 + 帧等待锁,零忙轮询

采集线程每来一帧就 pushFrame 更新最新帧并 notifyAll;每个客户端一个写线程,没帧可发时在锁上挂起等待,而不是空转轮询——旧手机长时间跑也不怎么耗电:

/** 采集线程每帧调用:更新最新帧,唤醒等待中的客户端线程(不阻塞采集) */
public void pushFrame(byte[] jpeg) {
    latestFrame = jpeg;
    synchronized (frameLock) {
        frameLock.notifyAll();
    }
}

亮点二:僵尸客户端剔除

慢客户端 / FFmpeg 探测后遗留的死连接,会拖垮发送线程。处理策略是:单帧 2 秒写不完就断开;客户端数超 8 个时踢掉最早的连接,优先保证新连接能接入:

while (clients.size() >= MAX_CLIENTS) {
    // 客户端过多:踢掉最早的连接(通常是 FFmpeg 探测后遗留的僵尸连接)
    SocketChannel oldest = clients.get(0);
    clients.remove(oldest);
    oldest.close();
}

亮点三:无客户端时不编码

hasClients() 返回 false 时,上层可以直接跳过 JPEG 编码——没人看就别干活,把每一毫安时都省下来。

App 还做了前台服务 + WakeLock + BOOT_COMPLETED 开机自启,装好后插电放沙发对面,就再也不用手动管它了。

5.2 服务器端:Java 版"AI 大脑"(本文主角)

服务器是 Maven 工程,技术栈:JavaCV(FFmpeg 拉流 + OpenCV 处理)+ ONNX Runtime(YOLOv8-Nano)+ Eclipse Paho(MQTT),一行 Python 都没有,纯 Java 跑 AI,对 Java 开发者极其友好。

完整流水线:拉流 → 相似度跳帧 → CLAHE 暗光增强 → letterbox 640 → ONNX 推理 → NMS → 业务状态机 → MQTT 下发

① 拉流:把重连做成"傻瓜式"

用 FFmpegFrameGrabber 拉 MJPEG 流,断流返回 null,主循环按指数退避重连(2s 起步、最长 30s、连上后复位),还做了两件贴心小事:

  • errno 翻译:把 FFmpeg 的 error -138 翻译成"连接超时:请确认 stream.url 是手机的 IP(不是服务器 IP)"这种能看懂的话;
  • 回环地址警告:如果你把推流地址填成了 127.0.0.1 或本机 IP,启动时直接红字警告——这是新手最容易犯的错(手机 IP 才是对的)。
// 低延迟配置
grabber.setOption("fflags", "nobuffer");
grabber.setOption("probesize", "1024");
// 连接超时与读写超时各 5 秒,避免推流无数据时永久挂起
grabber.setOption("timeout", "5000000");
grabber.setOption("rw_timeout", "5000000");

② 性能优化:画面没变,就别让 CPU 白烧

这是整个服务器端我最得意的一个优化——相似度跳帧

把当前帧缩小到 160×120 和基准帧做帧差,均值低于阈值就认为画面没变化,跳过预处理和模型推理,直接复用上一次的检测结果

boolean changed = isFrameChanged(frame, lastSmall, cfg.motionThreshold);
boolean forceRefresh = t - lastInferenceAt >= cfg.motionRefreshMs;
if (firstFrame || changed || forceRefresh) {
    // 画面有变化 / 首次 / 强制刷新:完整推理
    ...
} else {
    // 画面无变化:复用上次检测结果,跳过预处理与模型推理
    detections = lastDetections;
}

加上 forceRefresh(默认 10 秒强制推理一次)兜底,防止"缓慢变化"漏检。夜间猫卧着不动时,CPU 占用能降一大截——毕竟服务器要 7×24 常驻,能省一点是一点。

③ 夜间识别:全项目最大的技术难点

夜间关灯是刚需(猫就是趁黑行动的),而 YOLOv8 在纯黑画面里啥也认不出。解决方案分两层:

硬件层:940nm 红外补光。 人眼不可见的红外光照亮沙发区域,手机摄像头能拍到红外画面(判断标准:手机相机能拍到电视遥控器的红外光点,就能用;拍不到就得加 USB OTG 夜视摄像头)。

软件层:CLAHE 暗光增强。 注意细节——只对 YCrCb 空间的亮度通道 Y 做 CLAHE,而不是对整个 RGB 图做,否则颜色会被搞花:

/** 夜间暗光增强:BGR → YCrCb,仅对亮度通道做 CLAHE */
public static Mat nightEnhance(Mat bgr) {
    Mat ycrcb = new Mat();
    cvtColor(bgr, ycrcb, COLOR_BGR2YCrCb);
    MatVector channels = new MatVector(3);
    split(ycrcb, channels);
    CLAHE clahe = createCLAHE(2.0, new Size(8, 8));
    clahe.apply(channels.get(0), channels.get(0));
    clahe.close();
    merge(channels, ycrcb);
    cvtColor(ycrcb, bgr, COLOR_YCrCb2BGR);
    ...
    return bgr;
}

增强后再走标准流程:letterbox 等比缩放 + 灰边填充到 640×640 → 转 CHW float 张量 → ONNX Runtime 推理。

④ 推理:ONNX Runtime 跑 YOLOv8-Nano

模型用官方 COCO 预训练的 yolov8n.pt,一行 Python 导出成 ONNX(仓库里有现成脚本 scripts/export_yolov8_onnx.py):

python export_yolov8_onnx.py   # imgsz=640, opset=12, simplify=True
# 输出 yolov8n_640.onnx → 放到 server/models/

推理层有个很实用的兼容处理:同时兼容 YOLOv8 的两种输出布局[1,84,8400][1,8400,84]),不同版本 ultralytics 导出的模型都能直接跑,不用返工:

boolean rowMajor = outShape.length == 3 && outShape[1] == NUM_PREDS;

然后标准 NMS 去重,把 640 坐标系的框换算回原图归一化坐标。

⑤ 业务判定:一个 80 行的状态机,防误触的灵魂

识别到猫只是第一步,真正的工程难点是别误触发——夜里一个光斑、一片阴影、猫快速路过,都不该启动驱赶。状态机 GuardLogic 做了四重过滤:

  1. 只看 cat(COCO class 15),其他类别直接忽略;
  2. 面积过滤:目标框面积占比低于阈值(默认 0.01)就丢弃,滤掉光斑、阴影噪点;
  3. ROI 禁区:只有目标中心点进入你框选的沙发区域才算数;
  4. 停留阈值:必须持续停留 ≥1.2 秒才触发,快速路过不响。

触发后进入 30 秒冷却;如果猫赖着不走,每轮冷却结束会重复警告,直到它离开——这个"持续驱赶"的细节很多人会漏掉:

} else if (nowMs >= coolingUntil) {
    // 猫持续逗留:每轮冷却结束重复警告,持续驱赶直到猫离开
    coolingUntil = nowMs + cooldownMs;
    listener.onCommand(true);
}

整个状态机就 80 行,清晰好读,是我全项目最满意的类之一。

⑥ 下发:MQTT + 断线补发

触发后经 Paho 向 sofacatguard/cmd 发布 ON / OFF,MQTT 断线自动重连,恢复后补发未送达的指令。所有参数都在 config.properties 里可调(阈值、ROI、冷却时长、帧间隔……),不用改代码就能调参。

5.3 ESP32 端:硬件兜底是最后的底线

ESP32 用 Arduino 框架 + PubSubClient,逻辑非常简单:连 WiFi(带重连)→ 订阅 MQTT 主题 → 收到 ON 闭合继电器 → 收到 OFF 断开。

但有一个设计我必须单独拎出来讲——硬件兜底

// 硬件兜底:超声波最长工作 8 秒自动关闭(即使 App/网络失联)
if (alarmUntil > 0 && (long)(millis() - alarmUntil) >= 0) {
    alarmUntil = 0;
    relayOff();
    Serial.println("[兜底] 8 秒到时,超声波已自动关闭");
}

即使服务器挂了、WiFi 断了、App 失联了,超声波最多响 8 秒一定自动关。这是安全底线:万一哪天逻辑出 bug,也不至于让超声波一直响着折磨猫。

接线就 4 根线:ESP32 GPIO4 → 继电器 IN5V/GND 供电,继电器 COM/NO 串进超声波供电回路。光耦隔离继电器默认低电平触发(RELAY_ACTIVE_LOW=true),高电平触发的板子改一个常量即可。


六、部署:8 步走,周末就能跑起来

整个流程我在 README 里写了详细版本,这里给速通版:

  1. 接线:ESP32 → 继电器 → 超声波,通电测试通断;
  2. 手机端:Android Studio 打开 android/ 工程,Build APK 装到旧手机,授权相机/通知,启动推流,记下界面显示的推流地址
  3. 导出模型(PC 端一次性的):跑 scripts/export_yolov8_onnx.py,把 yolov8n_640.onnx 放进 server/models/
  4. 构建服务器cd server && mvn package,改 config.properties(推流地址、MQTT、ROI、阈值),java -jar target/sofacatguard-server-1.0.0.jar config.properties 启动,日志出现「模型加载成功」即就绪;
  5. 部署 MQTT Broker:EMQX 或 ESP32 内置简易 Broker(二选一,仓库有说明);
  6. 烧录 ESP32,测试 MQTT 收发;
  7. 整体联调:先白天测试,再夜间关灯 + 红外补光灯复测;
  8. 固化运行:App 加电池白名单、开自启动;服务器进程用 systemd / 计划任务托管,崩溃自动重启。

七、踩坑清单:帮你的猫和邻居感谢我

这些都是真实踩过的坑,全部写进了仓库的避坑规范:

正确姿势
红外灯买成 850nm必须 940nm 无红暴,人眼完全不可见
继电器买成高电平触发优先选「低电平触发」,或把固件 RELAY_ACTIVE_LOW 改为 false
模型输入尺寸和 letterbox 不一致导出 ONNX 必须 imgsz=640、opset≥12,与服务器 letterbox 一致,否则推理结果错位
推流地址填成 127.0.0.1那是手机自己的 IP,服务器上要填手机的局域网 IP
手机屏幕锁了/后台被杀关自动锁屏、关省电、加电池白名单、开自启动(国产 ROM 还要手动允许自启动管理)
红外灯直射镜头别对着镜头照,否则画面曝光发白全废
忘了兜底ESP32 端 8 秒硬件兜底必须保留,这是安全底线

参数调优口诀:夜间把 conf.thres 从 0.35 降到 0.25,同时调高 min.area.ratio,识别率和防误判两手都要硬。


八、成本与体验对比:凭什么选它

对比项成品智能驱猫器云摄像头 + AISofaCatGuard
成本¥300+设备 + 月费¥50~130(复用闲置设备)
断网感应失灵直接瘫痪照常工作
隐私视频上传云端全程局域网,零上传
夜间红外热释电,误触多夜视一般940nm 红外 + CLAHE,专门优化
可玩性不可改封闭生态全开源,Java/Android/C++ 任你魔改

作为一个开发者,最爽的点其实是最后一行——这套系统从相机采集到 AI 推理到硬件执行,每一行代码都是自己的。想加"猫离开自动关"?想存抓拍截图?想只晚上 22 点到早 7 点开启警戒?都只是加几个 case 的事。


九、二期展望

仓库里已经规划了二期方向,欢迎来提 PR:

  1. 触发时自动保存抓拍截图、记录闯入时间(服务端 Mat 直接存图,条件已具备);
  2. 定时警戒:仅夜间 22:00~07:00 自动开启;
  3. 手动开关:人在家时一键关闭驱赶;
  4. 触发次数统计日志(grep 已发布 就能数);
  5. 多路监控:多台手机推流,服务器多实例扩展。

十、写在最后

这个项目的初衷很简单:养猫和养家具,不该是单选题。而作为一个程序员,解决生活痛点最爽的方式,就是自己动手写一套系统——顺便把 Android、JavaCV、ONNX Runtime、MQTT、ESP32 这些技术全部串起来练了一遍。

如果你也养猫、也有闲置手机和旧电脑、也想玩物联网 + 端侧 AI,强烈建议动手复刻一个。项目地址:

GitHub:github.com/sakka6868/S…

如果觉得这个项目对你有用:

  • 点个 Star,让更多铲屎官看到它;
  • 🐛 遇到问题提 Issue,或者加个 feature 提 PR
  • 💬 评论区聊聊你家猫干过什么"沙发惨案",以及你想加什么功能。