研究智能床BLE连接记录

在上一个话题的基础上完善了基础的四种控制方法。补上了逆向时候的代码部分和控制帧。
家里最近添置了一张 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())

  • 关键点:持续发送指令帧,保持运动;停止发送即停止

七、结论

  1. 控制帧格式固定为 6E 01 00 XX CHECKSUM。
  2. 校验为 SUM8(末字节为所有字节求和低 8 位)。
  3. motor1/motor2 指令码已在 strings.xml 明文给出。
  4. 实测:持续发送指令帧才会持续抬升,停止发送即停止,无需 STOP 帧。

1 个赞