咱就是说,买了红米的Redmi Buds 5,手机端需要下一个叫做小米耳机的软件,电脑端就不知道如何操作,每次连到电脑,都要用手去按耳机切换模式,太不优雅了,于是我决定分析下这个耳机的协议。
1.抓取蓝牙数据包
我的手机是Realme手机,系统自带了日志抓取工具,这里正好用于抓原始数据包。
抓取过程就不多演示了,连接上耳机,打开手机端小米耳机软件,反复做一些切换模式操作,记录下操作的间隔时间,和相应的效果,以便于和报文对应上。
2.分析数据包
CFA文件就是我们要的原始数据包,具体提取方式在Realme的反馈工具箱,我的反馈,Bluetooth/btsnoop_hci/CsLogxxxxx里可以找到。
拿到电脑上之后,使用Wireshark打开。简单的过滤一下手机和耳机的交互。
可以发现,手机和耳机是通过RFCOMM协议,28通道交互的,具体的报文看起来格式都差不多,和手机端对应操作时间相对应一下,就能发现具体控制命令。
以切换为降噪模式举例,fedcbac70e000409020401ef,FEDCBA是头,c70e00应该是私有协议,04代表后面09020401的长度,09是一个自增变量,02代表后面0401的长度,04应该是模式切换的功能码,01是降噪,00是关闭,03是通透。EF是尾。所有报文格式都是这套。
那接下来就要开始尝试了,用Deepseek简单写了个Windows端RFCOMM交互的工具,和耳机尝试收发一些数据,成功切换降噪,通透!
3.总结和代码
经过很多次测试,耳机也会主动发一些数据包到电脑,戴上,摘下都会发数据包同步,具体也很容易看出来耳机的电量等状态,有兴趣的可以自己分析下,(小米数据包里的自增变量不增也没事,没有校验)。
具体上位机代码如下,不保证适配所有小米耳机。记得修改MAC地址对应上你的耳机
#!/usr/bin/env python
# -*- coding: gb2312 -*-
import tkinter as tk
from tkinter import messagebox
import socket
import threading
class BluetoothApp:
def __init__(self, root):
self.root = root
self.root.title("蓝牙控制面板")
# 蓝牙连接状态
self.connected = False
self.sock = None
# 设备配置
self.device_address = "9C:41:12:11:11:1E" # 请修改为你的设备地址
self.port = 28
# 创建UI
self.create_widgets()
# 自动连接
self.connect_bluetooth()
def create_widgets(self):
# 连接状态显示
self.status_label = tk.Label(self.root, text="状态: 未连接", fg="red")
self.status_label.pack(pady=5)
# 按钮1
self.btn1 = tk.Button(
self.root,
text="发送命令1 (FE DC BA C4 08 00 04 0F 02 04 00 EF)",
command=lambda: self.send_hex("FE DC BA C4 08 00 04 0F 02 04 00 EF"),
state=tk.DISABLED
)
self.btn1.pack(pady=5, padx=20, fill=tk.X)
# 按钮2
self.btn2 = tk.Button(
self.root,
text="发送命令2 (FE DC BA C4 08 00 04 0F 02 04 01 EF)",
command=lambda: self.send_hex("FE DC BA C4 08 00 04 0F 02 04 01 EF"),
state=tk.DISABLED
)
self.btn2.pack(pady=5, padx=20, fill=tk.X)
# 按钮3
self.btn3 = tk.Button(
self.root,
text="发送命令3 (FE DC BA C4 08 00 04 0F 02 04 02 EF)",
command=lambda: self.send_hex("FE DC BA C4 08 00 04 0F 02 04 02 EF"),
state=tk.DISABLED
)
self.btn3.pack(pady=5, padx=20, fill=tk.X)
# 接收框
self.receive_text = tk.Text(self.root, height=10, state=tk.DISABLED)
self.receive_text.pack(pady=10, padx=20, fill=tk.BOTH, expand=True)
# 清空按钮
self.clear_btn = tk.Button(
self.root,
text="清空接收区",
command=self.clear_receive
)
self.clear_btn.pack(pady=5)
# 退出按钮
self.exit_btn = tk.Button(
self.root,
text="退出",
command=self.on_close
)
self.exit_btn.pack(pady=5)
def connect_bluetooth(self):
try:
self.sock = socket.socket(socket.AF_BLUETOOTH,
socket.SOCK_STREAM,
socket.BTPROTO_RFCOMM)
self.sock.connect((self.device_address, self.port))
self.connected = True
self.status_label.config(text="状态: 已连接", fg="green")
# 启用按钮
self.btn1.config(state=tk.NORMAL)
self.btn2.config(state=tk.NORMAL)
self.btn3.config(state=tk.NORMAL)
# 启动接收线程
self.receiving = True
receive_thread = threading.Thread(target=self.receive_data)
receive_thread.daemon = True
receive_thread.start()
except Exception as e:
messagebox.showerror("连接错误", f"无法连接蓝牙设备: {e}")
def send_hex(self, hex_str):
if not self.connected:
messagebox.showwarning("未连接", "蓝牙未连接,无法发送数据")
return
try:
bytes_to_send = bytes.fromhex(hex_str.replace(" ", ""))
self.sock.send(bytes_to_send)
self.append_receive(f"[发送] {bytes_to_send.hex(' ').upper()}")
except Exception as e:
messagebox.showerror("发送错误", f"发送数据失败: {e}")
def receive_data(self):
while self.receiving:
try:
self.sock.settimeout(0.5)
data = self.sock.recv(1024)
if data:
hex_data = data.hex(' ').upper()
self.append_receive(f"[接收] {hex_data}")
except socket.timeout:
continue
except Exception as e:
if self.receiving: # 避免关闭时的错误提示
self.append_receive(f"[错误] 接收数据出错: {e}")
break
def append_receive(self, text):
self.receive_text.config(state=tk.NORMAL)
self.receive_text.insert(tk.END, text + "\n")
self.receive_text.see(tk.END)
self.receive_text.config(state=tk.DISABLED)
def clear_receive(self):
self.receive_text.config(state=tk.NORMAL)
self.receive_text.delete(1.0, tk.END)
self.receive_text.config(state=tk.DISABLED)
def on_close(self):
self.receiving = False
if self.sock:
try:
self.sock.close()
except:
pass
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = BluetoothApp(root)
root.protocol("WM_DELETE_WINDOW", app.on_close)
root.mainloop()