ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用MCP服务端让AI实时读取USB功耗计数据

用MCP服务端让AI实时读取USB功耗计数据

最近在折腾自动化测试设备的时候,突然冒出一个很有趣的想法:既然现在 AI 都能自己写代码、调函数了,那我能不能让它直接去读实验室那台 USB 功耗计的实时数据?监听串口、解析协议、算平均功耗这些杂活,全都交给 AI 自己完成。顺着这个想法,我给手头的 IoT Power 写了一个 MCP(Model Context Protocol)服务端,让 AI 能像调用一个普通函数那样,实时查电流、电压、功率,还能自己记录数据、做简单分析。整个过程踩了不少坑,但也确实省掉了大量重复的脚本代码,今天把完整的思路、实现和坑都整理出来。

这篇文章适合两类人:一类是平时经常用功耗计测嵌入式设备、做电池续航测试的硬件开发者,另一类是玩 AI Agent、想把真实世界的数据接进大模型生态的软件开发者。不需要懂太深的前端或算法,但最好有一定的 Python 基础和串口调试经验。我会把那套从串口裸数据到 MCP 工具调用的完整链路拆开讲清楚,包括协议怎么解析、服务端怎么写、常见的坑怎么排,最后还会聊聊几个很有意思的扩展玩法。

1. 为什么我要给功耗计写 MCP 服务端

1.1 MCP 到底是个什么“万能插头”

先聊聊 MCP。模型上下文协议(Model Context Protocol)本质上是一套标准化的接口约定,它解决了“让 AI 使用外部工具”这件事怎么统一、怎么安全地暴露能力。你可以把 MCP 想象成 USB 接口:以前不同的设备要用不同的线、不同的驱动,现在大家统一用 Type-C,插上就能用。MCP 就是给 AI 和“外部世界”之间定义了一个统一的数据管道。

MCP 服务端可以暴露三类东西:工具(Tools)、资源(Resources)和提示词(Prompts)。工具是让 AI 主动执行动作,比如“读取当前功耗”“保存一组数据”;资源是让 AI 读取数据,比如“浏览某段时间的功耗日志”;提示词则是给 AI 预设行为模式。对于功耗计这种设备,最核心的就是把“读电流电压”这个硬件能力封装成一个工具,让 AI 可以随时随地调用。

当时很多人写 MCP 都是去接文件系统、接数据库,但我立刻想到了一个更实用的场景——接真实硬件。因为软件世界里的数据都是干净的、结构化的,而真实硬件的数据是脏的、有延迟的、带噪声的,这恰恰是 AI 能发挥价值的地方。让 AI 自己去看功耗计,等于把一只“眼睛”借给了大模型。

1.2 IoT Power 这类功耗计的痛点

我手头这块 IoT Power 是一块非常经典的 USB 功耗监测小板,用起来很简单:供电走 USB,被测试设备也走 USB,小板上有一个 OLED 屏幕,实时显示电压、电流和功率。通过串口转 USB 连接到电脑,可以读到实时的数据流。

但这块板子的数据读取有一个典型的痛点:它输出的是一串带帧头帧尾的二进制或字符串数据,需要自己查通讯协议、自己写解析脚本。今天要统计电池放电曲线,明天要测待机功耗,每次都要重新写一遍 Python 串口脚本,改几个参数再跑一轮。如果只是单次测量还好,但一旦涉及长期监测、批量测试,那点写脚本的时间就会变成很烦人的负担。更别提还要处理串口波特率、数据帧对齐、校验丢失这些破事。

MCP 服务端的意义在于,把“解析功耗计数据”这件事从程序员手里拿掉,直接交给 AI。你只需要告诉 AI:“用 it_power_get_metrics 工具,测一下当前电压”,AI 就会自己完成串口读取、解析、返回结构化结果的全过程。

1.3 方案选型:串口直读 vs MCP 封装

当时我有两个选择。第一个是直接写一个 Python 脚本,用 pyserial 读串口,然后定期输出数据到终端。这很简单,但它只是把数据贴到了屏幕上,AI 依然无法理解这些数据的含义,更没法主动介入判断。

第二个选择就是把串口读数的能力封装成 MCP 工具。这样,AI 客户端在对话中就能直接调用工具,并且会根据返回值自行决策,比如电压低于 3.7V 就提醒“电池快没电了”,或者电流波动超过阈值就建议检查测试环境。这才是把“数据”变成了“智能诊断”。用 MCP 封装还有一个隐藏好处——所有的硬件交互都收束到几个语义明确的工具里,AI 不需要知道底层协议细节,你也不需要担心 AI 在代码里乱搞串口参数。

我最后选择了第二种方案。虽然前面多了一道 MCP 服务端的开发工作,但后续的效率和灵活性完全不一样,而且这套服务端不只给 AI 用,也可以给其他支持 MCP 的工具链复用。

2. 核心设计:从串口协议到 MCP 工具的映射

2.1 手里的这台功耗计的通信协议

先讲一下我这台 IoT Power 的通信基础。它通过 CH340 这类 USB 转串口芯片和电脑通信,默认波特率是 115200,串口参数一般是 8N1(8 位数据位,无校验,1 位停止位)。设备上电后会一直往外吐数据帧,每一帧的格式大致长这样:

AA 55 01 0C 02 00 00 00 00 00 5A 10 27 00 3C ...

这里AA 55是固定的帧头,01是长度字段,后面跟着不同的数据区。实际电压值占用 2 个字节,按大端排列,乘以固定系数(比如 0.001V)得到真实电压。电流同样如此,而功率值则是由电压和电流计算出来的。

不同厂家、不同型号的功耗计协议会有差异,但基本都是“帧头 + 长度 + 类型 + 数据 + 校验”。拿到新设备的第一步,不是写 MCP 服务端,而是先用串口调试助手抓一份原始数据,再对着协议文档确认字段偏移量。这步千万不能省,否则后面所有解析都是拿着错误的地图找宝箱。

2.2 MCP 服务端的结构:三个核心概念

在设计 MCP 服务端时,我按照官方规范把能力分成三类。

工具(Tools)是本次的主角,我定义了三个工具:

  • power_get_metrics:获取当前一次的电压、电流、功率和时间戳。
  • power_start_monitor:启动一个持续若干秒的监测任务,结束后返回平均值、最大值、最小值。
  • power_save_record:把当前数据追加到一个本地 CSV 文件里。

资源(Resources)用来暴露“可供读取的内容”,我注册了一个日志资源,路径类似power://logs/latest,AI 可以直接读取最近 100 行的原始数据记录。

提示词(Prompts)则是给 AI 准备好的“任务模板”,比如“帮我测一下这块板子的待机功耗”,AI 遇到这个提示词就会自动调用监测工具。

这三类东西通过 MCP 服务端暴露出来,AI 客户端在对话过程中会自动发现并选择调用。我用的客户端支持 MCP 工具注册,连接时只需要告诉它“SSE URL 或者 stdio 命令”,它就能拿到完整的工具清单。

2.3 工具定义与参数设计:让 AI 好理解

工具定义是 MCP 服务端质量的关键。我见过很多同行写的工具名晦涩难懂,参数定义乱七八糟,AI 完全不知道怎么用。我的原则是:工具名用“动词+名词”格式,描述里写清楚“是什么、能做什么、适合什么场景”,参数尽量少,且都要有默认值。

比如power_get_metrics的输入参数有:

参数类型是否必填说明
channelint否通道号,默认为 1
timeoutfloat否读取超时秒数,默认 2.0

这个工具返回一个 JSON 对象,包含voltage、current、power、timestamp四个字段,单位分别标注好。AI 拿到这个 JSON 就知道电压是 5.04V,电流是 0.123A,功率是 0.616W。

参数设计还有一个容易忽略的点:范围限制。因为这是真硬件,不能让 AI 传一个负数超时,更不能让它传一个荒谬的通道号。我在工具函数内部做了合法性校验,防止坏参数把串口读进程搞崩。这也是硬件场景和纯软件场景最大的不同:软件挂了重启就行,硬件接口一旦被搞挂,可能需要重新插拔 USB。

3. 实操:搭建一个可用的 MCP 服务端

3.1 环境准备与依赖

我的开发环境是 Python 3.11,操作系统用日常开发用的 Linux。基础依赖只需要三个:

pip install pyserial mcp uvicorn

这里拆一下用途:

  • pyserial:串口读写库,用来和IoT Power 通信。
  • mcp:官方 Python SDK,用来构建 MCP 服务端。
  • uvicorn:如果走 HTTP + SSE 方式暴露服务,可以选它,但本地调试时我最推荐用 stdio 模式,后续会说明原因。

推荐用一个独立的 venv 虚拟环境,避免把系统的 Python 环境搞乱。如果你打算长期跑,可以用 supervisor 或 systemd 托管进程。

3.2 串口读写与协议解析实现

先写一个独立的模块处理串口协议,这样后续换设备型号时,只需要改这一层。下面是我的核心代码示例:

import serial import struct import time class IotPowerDriver: def __init__(self, port='/dev/ttyUSB0', baud=115200): self.ser = serial.Serial(port, baud, timeout=1) self.buf = b'' def _read_frame(self): # 读取新数据并拼接缓冲,按帧头 AA 55 查找完整帧 data = self.ser.read(64) if not data: return None self.buf += data # 简易处理:找帧头位置 while True: idx = self.buf.find(b'\xaa\x55') if idx == -1 or len(self.buf) < idx + 2 + 12: if idx == -1: self.buf = self.buf[-1:] break frame_len = self.buf[idx + 2] if len(self.buf) >= idx + 2 + frame_len: frame = self.buf[idx: idx + 2 + frame_len] self.buf = self.buf[idx + 2 + frame_len:] return frame break return None def get_metrics(self): frame = None for _ in range(10): frame = self._read_frame() if frame: break time.sleep(0.05) if not frame: raise TimeoutError('no frame received from power meter') # 举例:电压字段在偏移 4~5,电流字段在偏移 6~7,比例系数 1000 voltage_raw = struct.unpack('>H', frame[4:6])[0] current_raw = struct.unpack('>H', frame[6:8])[0] voltage = voltage_raw / 1000.0 current = current_raw / 1000.0 return { 'voltage': round(voltage, 3), 'current': round(current, 3), 'power': round(voltage * current, 4), 'timestamp': time.time() }

这段代码有几个细节:

  • 缓冲拼接:串口数据不一定每次都严丝合缝对齐,所以必须用一个缓冲把不完整的数据衔接起来。我每次最多读 64 字节,然后不停找帧头。
  • 帧长度判断:一定要根据协议里的长度字段精确取值,别一次性把所有内容都截断。
  • 超时机制:如果连续 10 次都没拿到完整帧,就抛出异常让上层感知。

实际测试时,我用这个驱动在前台跑了几百次读取,校验了电压值与 OLED 上显示的数值一致,误差在一个毫伏以内,这才确认底层解析逻辑没问题。如果你手里的设备协议不同,建议先用一个简单的循环脚本打印原始帧,确认偏移和系数后再写解析。

3.3 注册 MCP 工具并暴露给 AI

接下来把底层驱动包到 MCP 工具里去。下面的代码展示了如何利用FastMCP快速注册工具。

from mcp.server.fastmcp import FastMCP from contextlib import asynccontextmanager driver = IotPowerDriver() mcp = FastMCP("iot-power-server") @mcp.tool() def power_get_metrics(channel: int = 1, timeout: float = 2.0) -> dict: """获取 IoT Power 当前的电压、电流和功率值。 适合做单点采样,返回结果包含电压(V)、电流(A)、功率(W)和时间戳。 """ if channel not in (1, 2): raise ValueError("channel must be 1 or 2") return driver.get_metrics() @mcp.tool() def power_start_monitor(duration: float = 10.0, interval: float = 1.0) -> dict: """启动连续监测,duration 为秒数,interval 为采样间隔秒数。返回统计结果。""" import time samples = [] end_time = time.time() + duration while time.time() < end_time: samples.append(driver.get_metrics()) time.sleep(interval) # 统计电流和功率 currents = [s['current'] for s in samples] powers = [s['power'] for s in samples] return { "samples": len(samples), "avg_current": round(sum(currents) / len(currents), 4), "max_current": round(max(currents), 4), "min_current": round(min(currents), 4), "avg_power": round(sum(powers) / len(powers), 4), } @mcp.resource("power://logs/latest") def latest_logs() -> str: """返回最近一段时间的原始日志摘要""" return driver.read_latest_logs()

然后在终端里启动即可:

python iot_power_mcp_server.py

默认的FastMCP服务端会以 stdio 方式启动,AI 客户端可以在启动时通过npx mcp-remote或内置的 MCP 配置连接。如果你的客户端只支持 SSE 模式,可以改成这样启动:

mcp.run(transport="sse", host="0.0.0.0", port=8000)

这时客户端会连接http://localhost:8000/sse。

3.4 在 AI 客户端里配置并验证

下面的配置截图我就不放了,直接讲关键步骤。以我常用的几乎兼容 MCP 的客户端为例,需要修改客户端配置文件,添加一个 MCP 服务:

{ "mcpServers": { "iot-power": { "command": "python", "args": ["/path/to/iot_power_mcp_server.py"], "env": {} } } }

配置完成后重启客户端,应该能在工具列表里看到power_get_metrics和power_start_monitor。然后我直接在对话框里输入:

请帮我看一下当前这块开发板的实时电压和电流,顺便算一下功耗。

客户端自动调用了power_get_metrics,紧接着返回:

电压:5.042V 电流:0.123A 功耗:0.615W

那一刻的感觉就像是 AI 真的“长出了眼睛”。随后我又试了让它每 1 秒测一次,连测 30 秒,然后告诉我平均电流。它准确调用了power_start_monitor,并给出了结果。到这里,整个 MCP 服务端就算跑通了。

4. 常见问题与排查技巧实录

4.1 串口被占用、权限不足

最常见的坑就是 Python 进程报SerialException: could not open port /dev/ttyUSB0。这通常有两种原因:

  • 端口被其他程序占用:比如你之前打开的串口助手或另一个调试脚本没退出。
  • 当前用户没有访问串口的权限:Linux 下通常要把用户加入dialout组,或者用sudo运行。

排查命令:

ls -l /dev/ttyUSB0 ps aux | grep -i python

如果确认没被占用,就执行sudo usermod -aG dialout $USER然后重新登录。Windows 下则要注意端口号是否对,通常通过设备管理器查看 COM 号。

我在测试时还遇到过 USB 转串口芯片被静电或瞬间短路搞挂的情况,现象是ls看不到端口。这时候把 USB 重新插一下,或者换一个 USB 口,往往就好了。如果是长期项目,建议加一个 USB 隔离模块。

4.2 数据解析丢失与超时

MCP 服务端运行过程中最让我头疼的问题是:偶尔返回的数据是空的,或者电压值明显错误。后来定位到原因是设备在启动预热阶段输出不稳定,或者串口缓冲区被旧数据污染了。

解决办法有二:

  • 启动后先清空缓冲区:在初始化串口后调用self.ser.reset_input_buffer()。
  • 连续两次读数对齐校验:如果电压和电流的数值误差超过 20%,就丢弃这一帧并重新读取。这样基本能过滤掉错位帧。

还有一点,如果设置的timeout太短,AI 调用时容易触发超时。我在工具定义里把默认timeout设成 2 秒,同时在解析层做了最多 10 次的重试。实测下来,除了设备刚插上的那一两秒,后续都是稳定秒回。

4.3 AI 调用失败与返回格式问题

有一次 AI 一直提示调用工具失败,但服务端日志显示函数已经执行了。后来发现是因为我返回了非 JSON 序列化的对象(比如包含了bytes类型)。MCP 工具返回值必须是可格式化的 JSON 对象,否则客户端在反序列化时会挂掉。

所以所有返回值都要强制转成基础类型,并且把浮点数做round()处理,避免出现超长小数导致 token 浪费。我还遇到过一次工具名称被客户端缓存下来的情况,服务端更新工具后客户端仍调用旧版本。解决办法是重启客户端,或者在开发阶段给工具名加版本后缀,比如power_get_metrics_v2。

4.4 安全边界:让 AI 只读,不要随便写

这个点值得单独拿出来说。MCP 服务端暴露给 AI 后,AI 可能会像“好奇的孩子”一样乱尝试。我一开始还暴露了一个power_set_output的写工具,结果 AI 在测试时竟然想去切换输出通道。虽然没出事,但把我吓出一身冷汗。

硬件控制类工具,默认只暴露只读能力。我后来直接把写能力删掉了,只保留读工具和“追加写日志文件”的工具。如果确实需要让 AI 控制输出,也必须加上二次确认机制,或者严格限定可写通道范围。

另外,不要在工具描述里写类似“你可以尝试关闭输出”这种引导性文字。AI 对描述的遵从度很高,你写了它真就敢去做。安全设计要始终放在第一位。

5. 扩展思路:把这套东西玩出花来

服务端跑通之后,我很快想到几个可以接着玩的方向。第一个是把功耗计和自动化测试框架结合,让 AI 在跑测试用例时自动采集功耗数据,然后生成最终的能耗报告。以前我都是测试完再手动导数据、画图,现在 AI 直接在这次对话里就把报告生成了,整个流程至少节省了半小时。

第二个方向是做异常告警。让 AI 持续监测电流,如果发现电流突然超过正常范围,就判断是否存在短路风险或者外设异常,通过消息通知推给你。这个场景对推理能力要求更高,但 MCP 返回数据质量好,AI 判断也就更准确。

第三个方向是跨设备协同。我在同一台机器上再跑几个 MCP 服务端,分别对应温度传感器、逻辑分析仪和功耗计,AI 可以同时调用多个服务,把温度与功耗关联起来分析。比如“当设备温度升高到 45 度时,功耗是不是也上去了”这种问题,AI 完全可以自己做数据聚合。

写这个服务端给我最大的感受是,硬件开发里最枯燥的“数据搬运”工作被 AI 接管后,真正省下来的时间可以投入到更有创造性的架构设计和问题排查上面。当然 MCP 服务端也有自己的边界,它不能改变设备本身的采样精度,也不能替代真实环境中的可靠性测试。它更像是给 AI 配了一套好用的“传感器大脑”,至于怎么用,还需要我们这些做实际系统的人来定规矩。

如果你手边正好也有一块类似的功耗计或者别的串口设备,建议花半天时间照上面的流程试一下。先把一层串口驱动写好,再注册两个只读工具,你的 AI 就从“只会聊天的文字脑”,进化成“能感知物理世界数据”的智能助手了。

返回列表