1. 项目缘起为什么需要BLE来执行Python脚本最近在折腾一个智能家居的传感器网络遇到了一个挺有意思的场景我有几个部署在房间角落的树莓派上面跑着一些数据采集的Python脚本。每次想修改一下脚本逻辑或者只是更新一个配置参数都得要么SSH远程登录要么就得跑过去插上显示器键盘。对于固定设备这还能忍但如果是那些移动的、嵌入在设备里的或者干脆没留网络接口的硬件节点这种“物理接触式”的运维就太痛苦了。这时候蓝牙低功耗BLE就进入了我的视线。它功耗低、普及率高几乎是个智能设备就有。如果能通过手机或者一个中央网关用BLE无线“喊话”给这些硬件节点让它们执行指定的Python任务那不就灵活多了吗比如让一个仓库里的巡检机器人临时执行一个数据打包脚本或者让一个环境监测设备调整一下采样频率都不用去动它的本体。市面上BLE模块很多但很多都需要你从底层协议开始啃光是搞明白GATT服务、特征值读写就够喝一壶的。直到我遇到了BleuIO。这玩意儿号称是“为开发者简化BLE”它提供了一个类似串口的AT命令接口让你可以用超级简单的文本命令来控制BLE连接和数据收发。更重要的是它支持Dongle模式可以直接插在运行Linux比如树莓派或Windows的电脑上变成一个蓝牙适配器并通过虚拟串口与主机通信。那么一个很自然的想法就产生了我能不能在电脑主机上写一个Python脚本这个脚本通过串口向BleuIO发送AT指令让BleuIO去连接另一个BLE设备并把一段Python代码脚本“发送”过去在那个设备上执行呢或者说反过来让BleuIO作为外围设备接收来自手机App的指令触发主机执行某个Python脚本经过一番摸索我发现这个想法完全可行而且BleuIO让整个过程变得出乎意料地简单。下面我就把自己从环境搭建、原理搞懂到最终跑通Demo的完整过程以及中间踩过的坑和总结的经验详细分享出来。2. BleuIO快速上手硬件与核心AT指令解析工欲善其事必先利其器。在写代码之前我们得先搞清楚手里的“器”——BleuIO——到底能干什么。2.1 BleuIO硬件简介与模式选择BleuIO常见的是一个USB Dongle长得跟普通U盘差不多。它核心的芯片是 Nordic Semiconductor 的 nRF52832这是一颗非常流行的BLE SoC。BleuIO的妙处在于它出厂就烧录好了自己的固件把复杂的BLE协议栈封装成了一组AT命令。它主要有两种工作模式我们的项目主要用到第一种Dongle模式这是默认模式。将BleuIO插入电脑USB口它会被识别为一个CDC/ACM设备在Linux下是/dev/ttyACMx在Windows下是COMx。你的主程序我们的Python脚本通过这个串口与BleuIO通信发送AT指令来控制它。此时BleuIO可以作为中央设备Central主动扫描连接其他BLE设备或外围设备Peripheral被其他设备连接。Standalone模式通过特定AT指令切换BleuIO可以独立运行预先编写的脚本它支持一个简化的类Basic脚本语言不再需要通过USB与主机通信。这个模式更适用于嵌入式场景但本文聚焦于通过主机Python控制故不深入讨论。对于“Execute Python Scripts via BLE”这个目标我们的架构通常是方案ABleuIO作为网关BleuIO以Dongle模式插在主机A上。主机A的Python脚本通过串口控制BleuIO让它去连接设备B另一个BLE设备如传感器并传输数据或指令。这里的“执行Python脚本”发生在主机A上BLE只是触发或传递参数的通道。方案BBleuIO作为接收端BleuIO以Dongle模式插在主机例如树莓派上。手机App作为Central连接BleuIO并通过GATT特征值写入一个“执行脚本X”的指令。主机上的一个常驻Python服务监听串口从BleuIO读到这个指令后调用本地对应的Python脚本执行。本文将以方案A为主要脉络进行讲解因为它更通用也更能体现“通过BLE控制”的精髓。2.2 你必须掌握的几条核心AT指令BleuIO的AT指令很丰富但入门只需这几条建议你先手动用串口工具如screen、minicom、PuTTY试试找找感觉。注意AT指令以\r\n回车换行结尾。发送AT如果返回OK说明串口通信和模块基础功能正常。ATADVSTART/ATADVSTOP作用启动/停止广播。当你想让BleuIO作为外围设备被手机扫描到时就用ADVSTART。实战ATADVSTART。成功后用手机蓝牙扫描就能看到一个叫“BleuIO”的设备。ATSCANSTART/ATSCANSTOP作用启动/停止扫描。当BleuIO作为中央设备去发现周围的BLE外围设备时使用。实战ATSCANSTART。它会持续扫描并将扫描到的设备信息通过串口打印出来格式如[DEVICE]: AA:BB:CC:DD:EE:FF, RSSI-65, NAMEMySensor。这个MAC地址AA:BB:CC:DD:EE:FF就是我们后续连接的关键。ATCONNECTAA:BB:CC:DD:EE:FF作用连接指定MAC地址的BLE设备。实战从扫描结果里复制一个设备的MAC地址发送ATCONNECTAA:BB:CC:DD:EE:FF。如果连接成功会返回CONNECTED。这里有个大坑MAC地址中的字母必须大写否则连接会失败。我当初就因为小写折腾了半天。ATDISCONNECT作用断开当前连接。ATWRITECHA作用向已连接设备的某个特征值Characteristic写入数据。这是实现“远程执行”的关键。格式ATWRITECHAhandle,data解释handle是特征值的句柄一个数字data是要写入的十六进制字符串。如何知道句柄和数据格式这需要目标BLE设备的GATT配置文件通常设备厂商会提供。例如一个LED灯的服务可能有一个特征值用于控制开关句柄是0x0024写入01开灯写入00关灯。实战模拟场景假设我们连接了一个智能开关其“命令特征值”句柄为0x0012写入字符串RUN_SCRIPT_1的十六进制形式52554e5f5343524950545f31可以触发它执行任务1。那么指令就是ATWRITECHA18,52554e5f5343524950545f31注意句柄10进制是18。ATREADCHA作用读取已连接设备的某个特征值。格式ATREADCHAhandleATIMME1与ATNOTI1作用这是一对高级组合拳。ATIMME1设置模块为“立即执行”模式使得后续的ATCONNECT等指令会立即执行而不需要再发ATENTM退出命令模式。ATNOTI1则开启串口通知当模块收到BLE设备发来的通知Notification或指示Indication时会自动通过串口上报数据。这在监听传感器数据流时非常有用。手动测试成功后你就会信心大增。接下来就是用Python把这些手动操作自动化。3. Python环境搭建与串口通信基础要让Python脚本和BleuIO对话我们需要一个桥梁PySerial库。同时为了管理项目依赖和版本强烈推荐使用虚拟环境。3.1 创建虚拟环境与安装依赖打开你的终端Linux/macOS或命令提示符/PowerShellWindows。# 1. 为项目创建一个新目录并进入 mkdir bleuio_python_controller cd bleuio_python_controller # 2. 创建Python虚拟环境假设你已安装Python3.8 # 方式一使用 venv (Python3内置) python3 -m venv venv # 方式二如果你使用conda # conda create -n bleuio_env python3.9 # conda activate bleuio_env # 3. 激活虚拟环境 # Linux/macOS: source venv/bin/activate # Windows: # venv\Scripts\activate # 激活后命令行提示符前通常会显示 (venv) # 4. 安装核心库pyserial pip install pyserial为什么用虚拟环境因为它能为你的项目创建一个独立的Python包安装空间避免与系统级或其他项目的包版本冲突。比如系统里装的是pyserial3.4但这个项目需要3.5的某个新特性在虚拟环境里单独安装3.5即可互不影响。3.2 找到你的BleuIO串口这是第一步也是容易卡住的一步。BleuIO插入电脑后系统会给它分配一个串口设备名。Linux (如树莓派 Raspbian/Ubuntu)# 插入BleuIO前先看看有哪些tty设备 ls /dev/ttyACM* # 插入BleuIO后再次执行多出来的那个就是它 ls /dev/ttyACM*通常会是/dev/ttyACM0。如果同时插了多个串口设备可以通过拔插对比来确定。你需要有权限访问这个设备通常需要将用户加入dialout组sudo usermod -a -G dialout $USER然后注销并重新登录生效。Windows 打开“设备管理器”展开“端口COM和LPT”。插入BleuIO后会新增一个“USB串行设备”或类似条目后面括号里就是COM号例如COM3。macOSls /dev/cu.usbmodem*通常是/dev/cu.usbmodemXXXX格式。记下这个端口名我们将在Python代码中使用它。3.3 编写第一个“Hello BLE”脚本连接与断开让我们写一个最简单的脚本实现扫描设备 - 连接第一个找到的设备 - 断开。这个脚本不执行具体功能但验证了整个通信链路是否通畅。import serial import time import re class BleuIOController: def __init__(self, port, baudrate115200): 初始化串口连接。 参数 port: 串口设备路径如 /dev/ttyACM0 或 COM3 baudrate: 波特率BleuIO默认115200 self.ser serial.Serial( portport, baudratebaudrate, timeout1 # 读超时1秒 ) # 清空可能的残留数据 self.ser.reset_input_buffer() time.sleep(0.1) print(f已连接到串口 {port}) def send_at_command(self, command, wait_time0.5): 发送AT指令并读取返回。 参数 command: 字符串如 AT 或 ATSCANSTART wait_time: 发送后等待的时间秒用于接收响应 返回 从串口读取到的响应字符串 # AT指令需要以 \r\n 结尾 full_command command \r\n self.ser.write(full_command.encode(utf-8)) time.sleep(wait_time) # 等待模块响应 response self.ser.read(self.ser.in_waiting).decode(utf-8, errorsignore) return response def scan_and_get_first_device(self, scan_duration5): 扫描BLE设备并返回第一个发现的设备MAC地址。 参数 scan_duration: 扫描持续时间秒 返回 设备MAC地址字符串如 AA:BB:CC:DD:EE:FF如果没找到则返回None print(开始扫描BLE设备...) resp self.send_at_command(ATSCANSTART) if OK not in resp: print(启动扫描失败) return None time.sleep(scan_duration) # 等待扫描结果 resp self.send_at_command(ATSCANSTOP) # 从扫描停止的响应中或者从之前积累的响应中解析设备信息 # 更健壮的做法是在扫描期间持续读取串口数据 # 这里为了简化我们假设设备信息在 ATSCANSTOP 的响应里 print(扫描响应:, resp) # 使用正则表达式查找MAC地址简化版实际响应可能需要更复杂的解析 mac_pattern r([0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}) matches re.findall(mac_pattern, resp.upper()) # 转为大写匹配 if matches: first_mac matches[0] print(f找到设备: {first_mac}) return first_mac else: print(未找到任何设备) return None def connect_to_device(self, mac_address): 连接到指定MAC地址的设备。 参数 mac_address: 字符串如 AA:BB:CC:DD:EE:FF 返回 bool, 连接是否成功 # 确保MAC地址大写 mac_address mac_address.upper() command fATCONNECT{mac_address.replace(:, )} # 连接指令需要去掉冒号 print(f尝试连接: {command}) resp self.send_at_command(command, wait_time2) # 连接需要更长时间 print(f连接响应: {resp}) return CONNECTED in resp def disconnect(self): 断开当前连接 resp self.send_at_command(ATDISCONNECT) return OK in resp def close(self): 关闭串口连接 self.ser.close() print(串口连接已关闭) # 主程序 if __name__ __main__: # 请将这里的端口改为你电脑上BleuIO的实际端口 PORT /dev/ttyACM0 # Linux示例 # PORT COM3 # Windows示例 controller BleuIOController(PORT) # 1. 测试基础AT指令 print(测试AT指令...) resp controller.send_at_command(AT) print(fAT响应: {resp}) # 2. 扫描并获取第一个设备 target_mac controller.scan_and_get_first_device(scan_duration3) if not target_mac: print(未找到设备程序退出。请确保周围有BLE设备在广播。) controller.close() exit() # 3. 尝试连接 if controller.connect_to_device(target_mac): print(连接成功) # 这里可以插入后续的读写操作 time.sleep(1) # 保持连接1秒 # 4. 断开连接 if controller.disconnect(): print(断开成功) else: print(连接失败) controller.close()运行这个脚本前请确保将PORT变量修改为你的实际端口。周围有一个正在广播的BLE设备比如另一个BleuIO在广播或者一个蓝牙心率带、智能灯泡等。如果遇到权限错误Linux请确认已按前文所述将用户加入dialout组并重新登录。这个脚本跑通意味着你的Python已经能通过串口指挥BleuIO进行最基本的BLE操作了。接下来我们要玩点真的通过BLE传递指令触发Python脚本执行。4. 核心实现设计一个通过BLE触发脚本执行的系统现在进入最核心的部分。我们的目标是用手机或其他BLE中央设备发送一个指令让连接着BleuIO的电脑执行一个特定的Python脚本。这需要两端配合服务端电脑 BleuIO运行一个Python守护程序监听BleuIO串口。当收到特定指令时调用本地脚本。客户端手机App连接BleuIO并向其某个特征值写入指令数据。这里的关键在于BleuIO在Dongle模式下可以作为GATT服务器Peripheral预定义一些服务和特征值供手机读写。幸运的是BleuIO固件已经内置了一个简单的“通用透传”服务。4.1 理解BleuIO的透传服务与数据流BleuIO在广播时会暴露一个服务其中包含一个可写的特征值Characteristic。当手机向这个特征值写入数据时BleuIO会直接将数据通过串口发送给主机。同样主机通过串口发送特定格式的数据BleuIO也会将其通过这个特征值通知Notify给手机。这个数据流是双向透明的所以我们不需要去纠结复杂的GATT配置只需要知道手机连接BleuIO。手机向那个特定的特征值写入字符串比如run:myscript.py。BleuIO通过串口把run:myscript.py原样发给我们的Python守护程序。Python守护程序解析这个字符串执行对应的脚本myscript.py。那么第一步是让BleuIO进入可被连接、并开启透传的模式。4.2 配置BleuIO为带透传服务的外围设备我们需要通过串口给BleuIO发送一系列初始化指令。最好将这些指令写在一个配置函数里每次启动服务时都执行。def setup_bleuio_as_peripheral(controller): 将BleuIO配置为带透传服务的外围设备。 步骤 1. 恢复出厂设置可选确保状态干净 2. 设置设备名称 3. 设置广播数据 4. 启动广播 # 1. 恢复出厂设置谨慎使用会清空自定义脚本等 # controller.send_at_command(ATRESTORE) # 2. 设置设备名称手机扫描时看到的名字 controller.send_at_command(ATNAMEBLEUIO_SERVER) # 3. 设置广播数据。这里我们使用BleuIO预定义的“透传服务”UUID。 # ATADVDATA指令用于设置广播包中的数据。 # 格式很复杂通常我们只需要确保它包含了对应的服务UUID即可。 # BleuIO的透传服务UUID通常是 6E400001-B5A3-F393-E0A9-E50E24DCCA9E # 但设置广播数据可能需要完整的AD结构。一个更简单的方法是使用AT指令开启默认服务。 # 经过测试发送 ATIBEACONOFF 和 ATADVSTART 通常就能以默认配置启动广播。 # 4. 停止可能正在进行的广播或扫描 controller.send_at_command(ATADVSTOP) controller.send_at_command(ATSCANSTOP) time.sleep(0.5) # 5. 启动广播 resp controller.send_at_command(ATADVSTART) if OK in resp: print(BleuIO已启动广播设备名BLEUIO_SERVER) print(现在可以用手机蓝牙扫描并连接它了。) return True else: print(启动广播失败) return False将这个函数加入之前的BleuIOController类中。运行这个配置后你的手机蓝牙扫描列表里应该会出现“BLEUIO_SERVER”。4.3 编写服务端守护程序监听、解析与执行服务端程序需要持续监听串口当收到特定格式的指令时就执行相应的操作。这里设计一个简单的协议指令以run:开头后面跟着脚本名或命令。import subprocess import threading class ScriptExecutorDaemon: def __init__(self, controller): self.controller controller self.running False # 定义一个简单的指令到脚本路径的映射 self.script_map { get_temp: /home/pi/scripts/get_temperature.py, reboot: /home/pi/scripts/soft_reboot.py, log_status: /home/pi/scripts/log_system_status.py, } def parse_and_execute(self, command_str): 解析从BLE收到的命令字符串并执行。 命令格式示例 run:get_temp 或 cmd:ls -la command_str command_str.strip() print(f收到原始命令: {command_str}) if command_str.startswith(run:): script_key command_str[4:] # 去掉run: script_path self.script_map.get(script_key) if script_path: print(f执行脚本: {script_path}) try: # 使用subprocess运行脚本并捕获输出 result subprocess.run( [python3, script_path], capture_outputTrue, textTrue, timeout30 # 设置超时防止脚本卡死 ) output fSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}\nRETURN CODE: {result.returncode} print(f脚本执行结果:\n{output}) # 这里可以将output通过BleuIO发回给手机需要更复杂的处理 except subprocess.TimeoutExpired: print(f错误: 脚本 {script_path} 执行超时) except FileNotFoundError: print(f错误: 脚本文件未找到 {script_path}) except Exception as e: print(f执行脚本时发生未知错误: {e}) else: print(f未知的脚本指令: {script_key}) elif command_str.startswith(cmd:): # 直接执行shell命令慎用有安全风险 shell_cmd command_str[4:] print(f执行Shell命令: {shell_cmd}) # 生产环境应避免直接执行任意shell命令或做严格的白名单过滤 # 此处仅作演示 # try: # os.system(shell_cmd) # except: # pass else: print(f无法识别的命令格式: {command_str}) def listen_serial(self): 持续监听串口数据的线程函数 buffer while self.running: if self.controller.ser.in_waiting 0: # 读取数据 data self.controller.ser.read(self.controller.ser.in_waiting).decode(utf-8, errorsignore) buffer data # 按行处理假设指令以换行符结尾 while \n in buffer: line, buffer buffer.split(\n, 1) line line.strip() if line: # 忽略空行 # 假设从手机发来的数据就是纯指令字符串 # 实际中BleuIO透传的数据可能带有前缀或特定格式需要根据实际情况解析 # 例如可能是 WRITE: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E, 72756E3A6765745F74656D70 # 这里是十六进制的 run:get_temp。我们需要先解析出特征值UUID和数据部分再进行十六进制解码。 # 下面是一个简化处理假设串口收到的是直接解码后的字符串。 self.parse_and_execute(line) time.sleep(0.01) # 短暂休眠避免CPU占用过高 def start(self): 启动守护程序 self.running True self.listener_thread threading.Thread(targetself.listen_serial) self.listener_thread.daemon True # 设置为守护线程主程序退出时自动结束 self.listener_thread.start() print(BLE指令监听守护程序已启动。) def stop(self): 停止守护程序 self.running False if self.listener_thread: self.listener_thread.join(timeout2) print(BLE指令监听守护程序已停止。) # 整合主程序 if __name__ __main__: PORT /dev/ttyACM0 # 修改为你的端口 controller BleuIOController(PORT) daemon ScriptExecutorDaemon(controller) # 配置BleuIO为外围设备并开始广播 if not setup_bleuio_as_peripheral(controller): print(初始化BleuIO失败退出。) controller.close() exit() try: daemon.start() print(服务运行中。按 CtrlC 停止。) # 主线程保持运行直到被中断 while True: time.sleep(1) except KeyboardInterrupt: print(\n接收到中断信号正在停止...) finally: daemon.stop() # 停止广播 controller.send_at_command(ATADVSTOP) controller.close()这个守护程序已经具备了核心功能。它在一个单独的线程中监听串口当收到run:get_temp这样的指令时就会在预定义的映射表里找到对应的Python脚本路径并执行。4.4 数据格式的坑十六进制与字符串的转换上面代码中我留了一个伏笔“假设串口收到的是直接解码后的字符串”。但实际情况往往更复杂。BLE特征值写入的数据通常是十六进制Hex格式的字节流。BleuIO从手机收到数据后可能会以十六进制字符串的形式通过串口发送。例如手机App向特征值写入字符串run:get_temp的UTF-8字节BleuIO串口收到的可能是WRITE: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E, 72756E3A6765745F74656D70其中72756E3A6765745F74656D70就是run:get_temp的十六进制表示。我们的守护程序需要解析这个WRITE:行提取出十六进制数据部分然后将其解码为字符串。修改listen_serial函数中的解析部分def listen_serial_improved(self): buffer while self.running: if self.controller.ser.in_waiting 0: data self.controller.ser.read(self.controller.ser.in_waiting).decode(utf-8, errorsignore) buffer data while \n in buffer: line, buffer buffer.split(\n, 1) line line.strip() # 解析BleuIO的WRITE通知 if line.startswith(WRITE:): # 格式: WRITE: UUID, hex_data parts line.split(,) if len(parts) 2: hex_data parts[1].strip() try: # 将十六进制字符串转换为字节再解码为普通字符串 command_bytes bytes.fromhex(hex_data) command_str command_bytes.decode(utf-8) print(f解析到BLE指令: {command_str}) self.parse_and_execute(command_str) except ValueError as e: print(f十六进制数据解析失败: {hex_data}, 错误: {e}) # 也可以处理其他类型的通知如连接断开等 elif line CONNECTED: print(手机已连接) elif line DISCONNECTED: print(手机已断开) elif line and not line.startswith(WRITE:): # 其他非WRITE的文本行可能是AT命令响应或日志 # 可以根据需要处理这里简单打印 print(f串口日志: {line}) time.sleep(0.01)这样我们的服务端就能正确解析从手机发来的十六进制格式指令了。5. 客户端实践用手机App发送执行指令服务端准备好了我们需要一个客户端来触发它。对于快速测试有几种简单方法5.1 使用通用的BLE调试App在手机应用商店搜索“BLE Scanner”、“nRF Connect”、“LightBlue”等。这些App可以扫描、连接BLE设备并查看和操作其服务与特征值。手机安装并打开App如nRF Connect。扫描设备找到“BLEUIO_SERVER”并连接。连接后App会列出该设备的所有服务Services和特征值Characteristics。寻找一个具有“写”Write权限的特征值。对于BleuIO的默认透传服务它通常是一个UUID类似6E400001-B5A3-F393-E0A9-E50E24DCCA9E的服务下面有一个UUID为6E400002-B5A3-F393-E0A9-E50E24DCCA9E的特征值用于写入从手机到BleuIO另一个6E400003-B5A3-F393-E0A9-E50E24DCCA9E的特征值用于通知从BleuIO到手机。点击用于写入的特征值进入写操作界面。在输入框中输入你要发送的指令。关键点来了输入框可能有“Text”、“Hex”等格式选项。如果你选择“Text”直接输入run:get_tempApp会自动将其转换为十六进制发送。如果你选择“Hex”则需要手动输入72 75 6E 3A 67 65 74 5F 74 65 6D 70每个字节之间空格分隔或72756E3A6765745F74656D70连续十六进制字符串。点击“Send”或“Write”。如果一切正常你的电脑终端上运行的服务端程序应该会打印出“解析到BLE指令: run:get_temp”并开始执行对应的脚本。5.2 编写一个简单的Python客户端用于另一台电脑测试你也可以用另一台电脑或同一个电脑上的另一个Python进程模拟手机客户端。这需要该电脑也有蓝牙功能并安装bleak库一个跨平台的BLE客户端库。# client_test.py import asyncio from bleak import BleakClient # 这是BleuIO透传服务的写入特征值UUID WRITE_CHAR_UUID 6E400002-B5A3-F393-E0A9-E50E24DCCA9E async def send_script_command(target_address, command): 向指定的BLE设备发送脚本执行命令。 参数 target_address: BleuIO的蓝牙MAC地址如 AA:BB:CC:DD:EE:FF command: 命令字符串如 run:get_temp print(f正在连接 {target_address}...) async with BleakClient(target_address) as client: if client.is_connected: print(连接成功) # 将命令字符串编码为字节 data_to_send command.encode(utf-8) print(f发送命令: {command} (Hex: {data_to_send.hex()})) # 写入特征值 await client.write_gatt_char(WRITE_CHAR_UUID, data_to_send) print(命令发送完毕。) else: print(连接失败。) if __name__ __main__: # 替换为你的BleuIO的MAC地址 BLEUIO_MAC AA:BB:CC:DD:EE:FF # 注意格式通常用冒号分隔 COMMAND run:get_temp asyncio.run(send_script_command(BLEUIO_MAC, COMMAND))运行这个客户端脚本前需要先安装bleakpip install bleak。注意在Windows上使用bleak可能需要额外的蓝牙权限配置。6. 安全加固、错误处理与生产环境考量上面的Demo虽然跑通了但离一个健壮的生产级服务还有距离。在实际部署前务必考虑以下几点6.1 安全是第一要务指令白名单绝对不要直接执行任意Shell命令cmd:。我们的script_map就是一个简单的白名单机制只允许执行预先注册的脚本。脚本权限限制被执行的Python脚本应该以最低必要权限运行。可以考虑用一个专门的、权限受限的系统用户来运行守护进程和脚本。连接认证目前的BleuIO配置是开放连接的。在生产环境中你应该启用BLE的配对绑定Pairing/Bonding功能或者至少要在服务端验证连接设备的MAC地址白名单。BleuIO支持通过AT指令设置静态密码或进行配对管理如ATAUTH相关指令。指令签名/加密对于高安全场景可以考虑对指令进行数字签名或加密。例如手机App发送{“cmd”: “run:get_temp”, “signature”: “xxx”}的JSON服务端验证签名通过后才执行。6.2 健壮的错误处理与日志串口重连USB可能松动串口连接会断开。守护程序需要检测串口异常并尝试重新初始化连接。脚本执行超时与隔离使用subprocess.run的timeout参数非常重要防止恶意或 buggy 脚本无限运行。更进一步可以考虑用docker run或systemd-run在容器或临时作用域内运行脚本实现资源隔离。完整的日志系统不要只print。使用Python的logging模块将不同级别的日志INFO, WARNING, ERROR输出到文件和控制台便于问题追踪。BLE连接状态管理监听CONNECTED和DISCONNECTED事件更新内部状态避免在设备未连接时进行写入操作。6.3 性能与扩展性异步架构当前的守护程序使用一个阻塞的串口读取循环和一个线程。对于高并发或低延迟需求可以考虑使用asyncio配合pyserial-asyncio库进行异步串口操作使整个服务更具响应性。指令队列如果短时间内收到多个指令可以考虑引入一个任务队列如queue.Queue由工作线程按顺序处理避免阻塞监听线程。结果回传目前脚本执行结果只打印在服务端。很多时候客户端需要知道执行结果。这可以通过BleuIO的“通知”特征值UUID通常为6E400003-B5A3-F393-E0A9-E50E24DCCA9E将结果发回给手机。需要在服务端脚本执行完毕后将输出结果通过串口以特定AT指令如ATNOTIF格式发送BleuIO会自动转发给手机。6.4 将服务变成系统守护进程在树莓派这类设备上长期运行最好将其设置为系统服务如systemd服务。创建一个服务文件例如/etc/systemd/system/ble-script-server.service[Unit] DescriptionBLE Python Script Execution Server Afternetwork.target [Service] Typesimple Userpi WorkingDirectory/home/pi/bleuio_python_controller ExecStart/home/pi/bleuio_python_controller/venv/bin/python /home/pi/bleuio_python_controller/server_daemon.py Restarton-failure RestartSec10 [Install] WantedBymulti-user.target然后启用并启动它sudo systemctl daemon-reload sudo systemctl enable ble-script-server.service sudo systemctl start ble-script-server.service # 查看状态 sudo systemctl status ble-script-server.service这样你的服务就能在树莓派启动时自动运行并且在崩溃后自动重启。从手动发送AT指令测试到用Python封装串口操作再到设计完整的客户端-服务端指令协议最后考虑安全、健壮性和部署这就是一个完整的“通过BLE执行Python脚本”项目的构建链路。BleuIO的价值在于它极大地降低了BLE开发的门槛让我们可以专注于业务逻辑即“执行什么脚本”而不是陷在蓝牙协议的细节里。当然每个实际项目都会有其独特的需求和挑战希望这个详细的指南能为你提供一个坚实的起点和清晰的排错思路。