在上一个话题的基础上完善了基础的四种控制方法。补上了逆向时候的代码部分和控制帧。
家里最近添置了一张 M6 智能床,官方仅支持原厂 App 和遥控器控制。
作为折腾党,想通过 ESP32 模拟蓝牙指令,最终接入米家实现小爱同学语音控制。
现在先实现了通过Python控制,后期有空会补上接入Home Assistant的ESPHome
一、分析对象
- APK:
XXX智能床_1.0.13.apk
二、BLE 设备参数(UUID)
来源:
-
cn/com/heaton/blelibrary/ble/Options.java
-
com/richmat/rm/project/common/utils/Const.java
核心 UUID(本项目实际使用)
- Service:
0000FEE9-0000-1000-8000-00805F9B34FB - Write:
D44BC439-ABFD-45A2-B575-925416129600 - Notify:
D44BC439-ABFD-45A2-B575-925416129601 - Notify Descriptor:
00002902-0000-1000-8000-00805F9B34FB
(Options.java 行 29-33)
三、指令帧格式 + 校验
帧格式:6E 01 00 <CODE> <CHECKSUM>
来源:
-
发送组帧:
com/richmat/rm/project/common/view/RichView.java
sendCode()里拼包:[0x6E, 0x01, 0x00, code, 0x00]
-
校验算法(SUM8):
com/richmat/rm/project/common/utils/TxUtil.java
- 最后一字节 = 所有字节求和取低 8 位
-
发送前缀:
com/richmat/rm/project/mlily0/common/enums/TxEnum.java
CMD = "6e0100"
停止帧(常见):6E01006EDD
四、关键指令码(CODE → 功能)
来源:res/values/strings.xml(指令定义)
以下为核心电机指令(行号见后):
| 功能 | CODE | 说明 |
|---|---|---|
| 头部上升 | 24 |
motor1_up |
| 头部下降 | 25 |
motor1_down |
| 脚部上升 | 26 |
motor2_up |
| 脚部下降 | 27 |
motor2_down |
| 头升脚降 | 21 |
head_up_foot_down |
| 头降脚升 | 22 |
head_down_foot_up |
| 头升脚升 | 23 |
head_up_foot_up |
| 头脚同时升 | 29 |
head_foot_both_up |
| 头脚同时降 | 2A |
head_foot_both_down |
| 仅头升 | 3D |
head_up_only |
| 仅脚升 | 3E |
foot_up_only |
| 平躺 | 31 |
flat |
| 头脚开关 | 6F |
head_foot_on_off |
五、握手帧(抓包确认)
这是从 btsnoop 抓包中提取的握手序列,先订阅通知再发。
6E15010185
6E13152BC1 (或 6E131530C6)
6E1430FFB1 (或 6E141BFF9C)
6E2008060300009F
六、Python控制脚本 已经实现了头部上升\下降 脚部上升\下降操作
import asyncio
from bleak import BleakClient
# ==== 设备参数(按需修改)====
ADDRESS = "AA:BB:CC:DD:EE:FF" # 你的设备 MAC
WRITE_UUID = "d44bc439-abfd-45a2-b575-925416129600"
NOTIFY_UUID = "d44bc439-abfd-45a2-b575-925416129601"
# ==== 可选:握手序列(来源于抓包)====
USE_HANDSHAKE = True
HANDSHAKE_FRAMES = [
bytes.fromhex("6E15010185"),
bytes.fromhex("6E13152BC1"),
bytes.fromhex("6E1430FFB1"),
bytes.fromhex("6E2008060300009F"),
]
# ==== 可选:保活(默认关闭)====
KEEP_ALIVE = False
KEEP_ALIVE_INTERVAL = 2.0 # 秒
# 使用 STOP 帧作为保活(相对安全),如不需要可保持 KEEP_ALIVE=False
STOP_FRAME = bytes.fromhex("6E01006EDD")
# ==== 控制指令码(旧 APK res/values/strings.xml)====
CODE_HEAD_UP = 0x24 # 头升
CODE_HEAD_DOWN = 0x25 # 头降
CODE_FOOT_UP = 0x26 # 脚升
CODE_FOOT_DOWN = 0x27 # 脚降
# 组合/姿态
CODE_HEAD_UP_FOOT_DOWN = 0x21
CODE_HEAD_DOWN_FOOT_UP = 0x22
CODE_HEAD_UP_FOOT_UP = 0x23
CODE_HEAD_FOOT_BOTH_UP = 0x29
CODE_HEAD_FOOT_BOTH_DOWN = 0x2A
CODE_HEAD_UP_ONLY = 0x3D
CODE_FOOT_UP_ONLY = 0x3E
CODE_FLAT = 0x31
CODE_HEAD_FOOT_ON_OFF = 0x6F
def make_frame(code: int) -> bytes:
"""生成 6E 01 00 <CODE> <CHECKSUM> 帧"""
frame = bytearray([0x6E, 0x01, 0x00, code, 0x00])
checksum = sum(frame) & 0xFF
frame[-1] = checksum
return bytes(frame)
async def notify_handler(sender, data: bytearray):
# 仅打印通知数据(可按需解析)
print(f"[NOTIFY] {data.hex()}")
async def send_frame(client: BleakClient, frame: bytes, label: str = ""):
await client.write_gatt_char(WRITE_UUID, frame)
if label:
print(f"[SEND] {label}: {frame.hex()}")
async def keep_alive_loop(client: BleakClient):
while True:
await send_frame(client, STOP_FRAME, "KEEP_ALIVE_STOP")
await asyncio.sleep(KEEP_ALIVE_INTERVAL)
async def run_for_seconds(client: BleakClient, code: int, seconds: float, label: str, interval: float = 0.2):
"""运行电机指定秒数:持续发送指令帧,不主动发送 STOP"""
seconds = max(0.0, float(seconds))
interval = max(0.05, float(interval))
end_time = asyncio.get_event_loop().time() + seconds
while asyncio.get_event_loop().time() < end_time:
await send_frame(client, make_frame(code), label)
await asyncio.sleep(interval)
async def head_up(client: BleakClient, seconds: float, interval: float = 0.2):
"""头部上升指定秒数"""
await run_for_seconds(client, CODE_HEAD_UP, seconds, "HEAD_UP", interval)
async def head_down(client: BleakClient, seconds: float, interval: float = 0.2):
"""头部下降指定秒数"""
await run_for_seconds(client, CODE_HEAD_DOWN, seconds, "HEAD_DOWN", interval)
async def foot_up(client: BleakClient, seconds: float, interval: float = 0.2):
"""脚部上升指定秒数"""
await run_for_seconds(client, CODE_FOOT_UP, seconds, "FOOT_UP", interval)
async def foot_down(client: BleakClient, seconds: float, interval: float = 0.2):
"""脚部下降指定秒数"""
await run_for_seconds(client, CODE_FOOT_DOWN, seconds, "FOOT_DOWN", interval)
async def main():
async with BleakClient(ADDRESS) as client:
print("连接成功")
# 订阅通知
await client.start_notify(NOTIFY_UUID, notify_handler)
# 可选:握手序列
if USE_HANDSHAKE:
for i, f in enumerate(HANDSHAKE_FRAMES, 1):
await send_frame(client, f, f"HANDSHAKE_{i}")
await asyncio.sleep(0.2)
# 可选:保活
if KEEP_ALIVE:
asyncio.create_task(keep_alive_loop(client))
# ==== 示例:按秒控制 ====
#await head_up(client, 8.0, interval=0.2)
#await asyncio.sleep(0.5)
#await head_down(client, 8.0, interval=0.2)
#await asyncio.sleep(0.5)
#await foot_up(client, 8.0, interval=0.2)
#await asyncio.sleep(0.5)
await foot_down(client, 8.0, interval=0.2)
await client.stop_notify(NOTIFY_UUID)
if __name__ == "__main__":
asyncio.run(main())
- 关键点:持续发送指令帧,保持运动;停止发送即停止
七、结论
- 控制帧格式固定为
6E 01 00 XX CHECKSUM。 - 校验为 SUM8(末字节为所有字节求和低 8 位)。
- motor1/motor2 指令码已在
strings.xml明文给出。 - 实测:持续发送指令帧才会持续抬升,停止发送即停止,无需 STOP 帧。




