Fyfar/ms5-wifi-microscope
GitHub: Fyfar/ms5-wifi-microscope
一个纯 Python 编写的 WiFi 显微镜开源查看器,通过逆向工程 i4season 协议将设备原生视频流以高帧率推送到浏览器或播放器。
Stars: 0 | Forks: 0
# MS5 WiFi 显微镜 — 开源查看器与协议
这是一个为廉价的 **MS5 WiFi 数字显微镜**(以及可能许多类似的基于 i4season 的 WiFi 摄像头/耳镜/内窥镜)从头全新编写的干净客户端。它通过 WiFi 连接到摄像头,并将其原生的 **1280×720 MJPEG** 视频流传输到你的**浏览器、VLC、ffplay 或 OBS** —— 帧率大约在 **20+ fps**。
官方应用(`DLscope`)已经损坏,勉强只能达到 **<2 fps**。该项目释放了摄像头的全部帧率,可以在任何运行 Python 的地方运行,并**记录了完整的逆向工程 WiFi 协议**,以便其他拥有者可以构建自己的工具。
- ✅ 纯 **Python 3 标准库** —— 无需 `pip install`,没有原生依赖。
- ✅ 完全**离线**工作(仅在 `192.168.1.1` 与摄像头通信)。
- ✅ 在浏览器或任何支持 MJPEG 的播放器(VLC/ffplay/OBS)中进行实时查看。
- ✅ 下方提供完整的**协议文档**。
## 目录
- [兼容性](#compatibility)
- [环境要求](#requirements)
- [快速开始](#quick-start)
- [端点](#endpoints)
- [工作原理](#how-it-works)
- [协议参考](#protocol-reference)
- [故障排除](#troubleshooting)
- [路线图 / 寻求帮助](#roadmap--help-wanted)
- [逆向工程过程](#how-it-was-reverse-engineered)
- [鸣谢](#credits)
- [许可证](#license)
- [免责声明](#disclaimer)
## 兼容性
已在 **MS5**(一款基于 JieLi 的 WiFi 显微镜;该摄像头识别自己的厂商为 `MKL`,产品为 `MS5`,固件为 `ver1221`;AP SSID 为 `wifi_camera_MS5_XXXX`)上确认。
MS5 通过 **i4season `libWifiCamera.so`** 协议栈驱动其摄像头,许多廉价的 WiFi 摄像头、**耳镜/采耳设备以及内窥镜**都共享该协议栈,这些设备以众多品牌销售并使用 **`com.i4season.*`** 移动应用(例如“Suear”采耳设备系列)。这里描述的协议 —— 并且很可能这个查看器无需更改或稍作修改 —— 应该也适用于这些设备。
**如果你在其他设备上尝试,请提出 issue 并附上结果**(以及 `GetDeviceInfo` 的输出),以便我们扩充兼容性列表。
## 环境要求
- Python **3.7+**(仅限标准库)。
- 一台能够加入摄像头 WiFi 接入点的计算机。
- 如果你不想使用浏览器,可以使用任意 MJPEG 查看器:VLC、`ffplay`、OBS 等(可选)。
## 快速开始
1. 打开显微镜电源并**加入其 WiFi 网络**(`wifi_camera_MS5_XXXX`)。你的机器将获得一个
`192.168.1.x` 地址;摄像头地址为 `192.168.1.1`。
2. 运行查看器:
python3 ms5_viewer.py
3. 打开视频流:
- **浏览器:** http://127.0.0.1:45100
- **VLC / ffplay / OBS:** `http://127.0.0.1:45100/stream`
- **单张快照:** http://127.0.0.1:45100/snapshot
就是这样。该摄像头没有互联网上行链路,因此加入其 AP 会使你的机器断网 —— 这是
预期的且完全正常;查看器只需要本地链路。
## 端点
| URL | 描述 |
|-----|-------------|
| `/` | 包含实时 `
` 流和设备信息的 HTML 页面 |
| `/stream` | `multipart/x-mixed-replace` MJPEG 流(在 VLC/ffplay/OBS 中使用此项) |
| `/snapshot` | 单张当前 JPEG 帧 |
查看器在运行时会打印实时的帧率 (fps) 读数。
## 工作原理
```
MS5 camera (UDP) ms5_viewer.py you
192.168.1.1 ┌───────────────────────┐
│ GetDeviceInfo │ receive thread │
│◀──────────────────▶│ - handshake │
│ OpenVideo(+port) │ - reassemble JPEG │ browser /
│◀──────────────────▶│ - publish latest frame │ VLC / ffplay
│ MJPEG chunks │ │◀─ HTTP MJPEG ──▶
│ ──────────────────▶│ HTTP server (MJPEG) │
└────────────────────┴───────────────────────┘
```
一个后台线程执行 WiFi 握手,并将摄像头分块的 JPEG 流重新组装成
完整的帧;一个小型 HTTP 服务器将这些帧作为 MJPEG 重新提供给任意数量的查看者。
## 协议参考
所有通信均通过 **UDP** 进行。摄像头 IP 为 `192.168.1.1`。这里有一小段连续的端口块:
| 端口 | 作用 |
|------|------|
| `:10005` | 命令通道(请求 → 响应) |
| `:10006` | "OpenVideo" / 初始化流 |
| `:10007` | 摄像头到客户端的控制推送(一个 `type 0x09` 描述符;接收视频不需要) |
| 客户端选择的临时端口 | 摄像头发送**视频**的端口(你在 OpenVideo 中声明) |
### 命令帧结构(12 字节头部,小端序)
```
offset size field
0 4 magic = 0xFFEEFFEE (on the wire: EE FF EE FF)
4 2 id increments per message; the camera echoes it back
6 2 type message type (see below)
8 1 unk = 1 in requests
9 1 err = 0 means OK (in responses)
10 2 length number of data bytes after this header
12 … data `length` bytes
```
**消息类型**
| type | 名称 | 备注 |
|------|------|-------|
| `0x01` | GetDeviceInfo | 返回厂商 / 产品 / 固件 / ssid |
| `0x02` | GetLicense | 返回序列号 + 设备上的许可证(非客户端限制) |
| `0x03` | SetLicense | |
| `0x04` | **OpenVideo** | 开始流传输(见下文) |
| `0x06` | UpdateFirmware | |
| `0x0A` | SetLed | (MS5 有一个*物理* LED 旋钮;无需 App 控制) |
| `0x0C` | CameraCommand | 摄像头控制 |
| `0x0D` | GetCameraConfig | |
| `0x0E` | SetCameraConfig | 分辨率设置极有可能在此处 —— 见路线图 |
**GetDeviceInfo 响应数据**(128 字节):`unk0` `u8`、`vendor` `char[32]`、`product` `char[32]`、
`fw_version` `char[16]`、`ssid` `char[32]`,然后是电源/容量/工作模式字段。
### 启动视频流(核心握手)
这是唯一一个不太直观的部分。发送带有空消息体的 OpenVideo 会得到确认,但**不会产生视频**,
因为摄像头不知道将其发送到哪里。你必须**告诉它你的接收端口**:
1. 将一个 UDP socket 绑定到操作系统分配的**临时端口 `P`**(通过 `getsockname` 读取它)。
2. 将 **OpenVideo**(`type 0x04`)发送到 `:10006`,并将 `P` 作为 2 字节小端序的有效载荷:
EE FF EE FF | id(2) | 04 00 | 01 | 00 | len(2) | P_lo P_hi
(参考库将头部的 `length` 字段写为 `0`,但仍然追加了这 2 个端口字节,
总计 14 字节。摄像头回复 `err=0`。)
3. 摄像头现在会将视频流传输到 **`your_ip:P`**。
### 视频流格式
数据报(约 1416 字节)到达 `P`:包含一个 **16 字节的分块头部 + JPEG 有效载荷**。
```
offset size field
0 1 0x01 (constant for every chunk on this firmware)
1 1 n_chunk global rolling counter (wraps at 256)
2 1 n_frame frame id ← a frame ends when this value changes
3 1 last_chunk flag
4 1 chunk index within the frame (1-based)
5 1 0
6 6 position (3 × u16; orientation/coords, ~0)
12 2 width (u16 LE, e.g. 0x0500 = 1280)
14 2 height (u16 LE, e.g. 0x02D0 = 720)
16 … JPEG bytes
```
**重组:** 按 `n_frame`(第 2 字节)对数据报进行分组,按分块索引(第 4 字节)对它们进行排序,并拼接 `payload[16:]` 即可得到一个 JPEG(`FF D8 … FF D9`)。当 `n_frame` 发生变化时完成一帧的组装。
结果:**1280×720 MJPEG,约 21 fps**(约 330 个数据报/秒)。
## 故障排除
- **无响应 / `connect timeout`:** 确保你已连接到摄像头的 WiFi AP 并且可以访问
`192.168.1.1`。空闲后的第一次请求经常会被丢弃 —— 查看器会自动重试。
- **连接摄像头后断开了互联网:** 预期行为。摄像头 AP 没有上行链路。查看器仅在本地运行。
- **流在运行一段时间后卡住:** 如果大约 1 秒内没有看到数据,查看器会重新发送 OpenVideo。如果在极长的会话中仍然
掉线,固件还支持一个我们目前
尚不需要实现的可靠 UDP ACK 层(见路线图)。
- **端口 45100 被占用:** 编辑 `ms5_viewer.py` 顶部的 `HTTP_PORT`。
## 逆向工程过程
1. 官方 App 已经损坏,但**摄像头本身没问题** —— 早期的 Android 抓包看起来像是
另一种协议,但那是一个不起作用的快照 *而且* 抓包工具(一个免 root 的 VPN)在结构上
对真实的视频是盲区的,因为 App 将其摄像头 socket 绑定到了 WiFi 网络上,从而
绕过了 VPN 路由。
2. App 通过 **`libWifiCamera.so`** (i4season) 驱动摄像头。开源的
[Suear-Web-Viewer](https://github.com/SeanPesce/Suear-Web-Viewer) 是针对*同一个*
库的干净客户端,并解码了大部分的帧结构。
3. 向 UDP `:10005` 发送的实时 `GetDeviceInfo` 得到了回复 —— 摄像头识别自己为 **MS5**,
证实了它使用该协议。
4. 没有有效载荷的 OpenVideo 得到了确认,但从未进行流传输。反汇编 `proOpenVideo` (arm64) 揭示了它会
**在 OpenVideo 消息内部声明一个由客户端选择的接收端口** —— 这就是缺失的关键部分。
5. 发送*带有端口的* OpenVideo 立即产生了 1280×720 MJPEG 流;解码 16 字节
分块头部即可得到纯净的帧。
**被排除的路径(忽略):** 该 App 还打包了一个 JoyHonest "GP/GK" SDK(`libjh_wifi.so`:`JHCMD` → UDP
`:20000`,MJPEG-HTTP `:8080`)。MS5 **并不**使用它。
## 许可证
[MIT](LICENSE)。随意使用、派生、发布。
## 免责声明
这是针对作者所拥有的硬件进行的独立**互操作性研究**。它不隶属于
设备制造商,也未获得其认可。名称和商标归各自所有者所有。使用风险
自负。
标签:MJPEG, Python, 云资产清单, 内核驱动, 无后门, 物联网, 硬件客户端, 网络协议, 视频流媒体, 逆向工具, 逆向工程