前阵子调一块开发板的待机功耗来回插拔USB折腾了一下午。每次想看电压、电流、功率就得凑近那块小OLED屏或者打开配套的上位机软件人肉记录数字再手动整理进测试报告。我突然想到一个问题AI既然能写代码、能查文档、能调用各种接口为什么不能让它自己去读功耗计这个项目的核心就是给一块叫 IoT Power 的 USB 功耗计写一个 MCP 服务端。MCP 全称 Model Context Protocol模型上下文协议相当于给 AI 装了一个“外接工具箱”。AI 客户端通过 MCP 里的工具tools调用就能直接读取功耗计的实时读数、历史趋势和会话统计而且整个过程对用户来说只是在对话框里发一句话而已。这篇文章我会从硬件侧的数据格式讲起再到 MCP 工具怎么拆分设计、具体代码怎么写、客户端怎么配最后分享联调过程中踩到的一些坑。适合两类人看一类是做智能硬件、嵌入式调试想把手边仪器接入 AI 工作流的开发者另一类是玩 AI Agent想知道 MCP 服务端落到真实设备上应该怎么设计的同学。我不打算讲太多概念直接给能运行的代码和能复现的步骤。1. 为什么非要让 AI 自己上手去读功耗计1.1 人肉搬运数据的断层调试一块低功耗设备的时候最大的麻烦不是问题本身而是反复插拔仪表、记录数据、比对变化。比如你在测一块板子的深睡眠电流正常应该在 50mA 左右结果发现一会儿 83mA、一会儿 41mA漂来漂去。这时候你大概率会怀疑是板子某个外设没关干净但你要做的动作依然是每隔几秒看一眼屏幕、抄一下数字、画个粗略的趋势。这中间存在一个断层机器产生数据的速度远远快于人眼记录数据的速度。OLED 屏一刷新上一帧的电流值可能就变了。人看到的是一个跳动的数字很难抓住一段时间的完整规律。把功耗计接入 MCP 之后数字的采集、记录、趋势观察这件事就交给了 AI。用户在对话框里发一句“帮我连续读 5 次功耗看看稳不稳定”AI 就会自己调用工具把读数拉回来再对照字段做判断。这个体验完全不一样你从“盯仪器的人”变成了“提需求的人”。1.2 MCP 补上的是决策闭环很多人会把“AI 能读功耗计”理解成“写了个串口读取脚本”这完全是两码事。串口脚本解决的是“我能读到数据”MCP 服务端解决的是“AI 能按需读到数据并且基于数据做决策”。MCP 的运作方式可以这样理解AI 客户端启动你的服务端进程服务端把自己提供的工具描述暴露给模型。模型在对话中判断“当前情况需要什么信息”然后发起工具调用拿到结果之后继续推理。等于把硬件数据真正接进了模型的思考链路。这就带来一个此前没有的选择同样是想知道这块板子是否漏电以前你需要自己截数据、自己算平均值、自己下结论。现在你可以让 AI 来完成只要你把判据说清楚空闲电流超过 80mA 视为异常、反复跳动超过 15% 说明有外设频繁唤醒。模型读两次数据之后就会自己给出“疑似漏电建议检查 WIFI 芯片的电源轨”这类结论。它读到的数据和你读到的数据一模一样但处理速度和处理方式完全不同。1.3 这套方案适合什么场景不是所有功耗测量场景都需要上 MCP。如果你的需求就是给一块板子偶尔量一次静太电流那串口小工具足够了。但如果存在下面几个特征就值得做需要反复对比多次测量结果比如调整代码后反复验证电流变化。需要长时间观察比如测一整晚待机功耗靠人盯不现实。有多个设备或多种状态需要切换测量人工记录容易混淆。希望 AI 参与判断比如让模型根据功耗曲线定位是哪个模块在异常唤醒。我是在第三次反复插拔功耗计之后决定动手的。因为那台设备不仅需要看电流还需要按一定的采样节奏记录并且最后要形成一份对比结论。这些动作如果交给 AI整个流程就顺畅得多。2. 先把功耗计的“脾气”摸透硬件侧数据从哪来2.1 这类 USB 功耗计的基本构成市面上非常流行的一类 USB 功耗计外观上看就是一个小小的转接头USB 公头插电源 USB 母座接负载板子中间有一颗采样电阻和主控。主控负责采集电压、电流、功率、累计容量、累计能量再驱动一块小 OLED 屏显示。这类设备有一个非常关键的特性它可以通过 USB 虚拟串口对外输出数据。也就是说插到电脑上之后不仅 OLED 屏幕上能看到数字系统里还会多出一个串口设备。电脑上的上位机软件就是靠读取这个串口来刷屏显示的。我管手头这块叫 IoT Power它默认输出的是一行 JSON 格式的文本。不同厂家的设备输出格式可能不同有些输出 CSV有些需要先发命令才回复但底层思路都一样。做 MCP 服务端之前第一步就是搞清楚你手头设备输出的原始格式。2.2 从串口里抓到的那行数据长什么样用任意一个串口调试工具打开对应端口大概率能看到这样一行{voltage_v:5.12,current_a:0.83,power_w:4.25,capacity_mah:326.5,energy_mwh:1672.0,temperature_c:35.2}这行 JSON 就已经把大多数想做功耗分析时需要的信息都给了字段含义典型单位voltage_v输入电压伏特Vcurrent_a当前电流安培Apower_w当前功率瓦特Wcapacity_mah累计容量毫安时mAhenergy_mwh累计能量毫瓦时mWhtemperature_c设备温度摄氏度℃如果你的设备输出的是 CSV比如5.12,0.83,4.25,326.5,1672.0,35.2也没关系解析函数后面会做兼容。有一点值得注意很多设备输出的power_w是瞬时值噪声不小。 OLED 刷新率大约 1~10Hz一个低功耗设备如果进入睡眠周期功率可能在 0.1W 和 0.8W 之间频繁跳变。直接把瞬时值丢给 AI它会被吓到容易误判“有异常大电流”。所以后面实现里我专门加了平滑处理。2.3 一次有效读取的关键接线、波特率和超时串口读取有三个坑初学者经常卡住。第一是端口名。Windows 里一般是COM3或者更高一点Linux 里常见ttyUSB0macOS 上一般是cu.usbserial-xxx。最烦人的是如果功耗计驱动不稳定同一个设备的端口编号可能变。我的做法是把端口做成环境变量服务端启动时从环境变量读取避免每次改代码。第二是波特率。大多数这类功耗计的串口默认是 115200也有部分是 9600。对不上波特率你读到的就是乱码甚至什么都读不到。拿到设备之后先用串口工具测一次确认波特率再往下走。第三是超时。设备上电之后的几百毫秒里可能不输出数据你得给串口读取一个合理的超时时间比如 2 秒。如果用默认的阻塞式读取AI 调用一次工具可能会挂很久整个对话体验会非常差。我封装的基础读取函数思路很简单打开串口、等一行数据、解析、关闭串口。优先选择读取一行而不是读全部数据因为功耗计是持续输出的读全部会永远读不完等不到 EOF。3. MCP 服务端的设计取舍工具拆分与状态保存3.1 三个工具还是一个工具接口设计的核心问题很多人第一次写 MCP 服务端习惯把所有字段塞进一个工具里。比如做一个get_all_data把电压、电流、功率、容量、能量、温度一次性返回。这能用但实际用起来模型的表现会变差。为什么因为模型在对话中调工具时会对工具名和描述做匹配。当信息太杂模型往往不知道该不该调用。更麻烦的是返回结果太长之后模型上下文窗口会被无用的重复字段占满。几次查询之后模型甚至开始复述旧数据不再调用新工具。我最终把工具拆成了四个按“意图”划分而不是按“字段”划分工具名职责get_power_now实时采样一次返回电压、电流、功率、平滑功率、温度get_history返回最近一段时间内的历史采样记录session_statistics返回本次会话的统计信息时长、平均功率、累计能量等reset_session清空会话统计并重置缓存这样拆的好处是每个工具对应一个明确的用户问题。“现在功耗多少”就调get_power_now“最近半小时趋势”就调get_history“从开始到现在总共耗了多少”就调session_statistics。模型很容易学会什么时候该调哪个工具。3.2 连续查询最容易踩的坑串口不是随便能打开的资源MCP 服务端和 AI 客户端之间是通过标准输入输出通信的。 AI 可能会在同一轮对话里连续发起多次工具调用每次调用都去开关串口。这暴露了一个非常重要的问题pyserial在 Windows 上反复开关串口没问题但在 Linux 上偶尔会报Resource busy因为上一次的句柄还没来得及完全释放。解决思路不是在同一个进程里保持长期打开而是做好三件事每次读取都用with serial.Serial(...)语法确保函数退出后端口一定被关闭。如果打开失败等待 200ms 之后重试最多重试三次。如果连续多次打开失败就直接把错误信息返回给模型让模型在对话里提示用户排查端口占用。还有一个小细节读取时不要用read_all()要用readline()。因为功耗计的数据是按行推送的readline()读取一行数据就够解析了。read_all()会一直读到设备缓冲区为空这个等待时间完全不可控。3.3 会话统计的状态管理基线法比清零指令更通用设备本身通常有累计容量和累计能量但这些数字是自设备上电以来就一直累加的。如果你想让 AI 报告“本次测量用了多少电”你得有一个基准点。有的功耗计支持串口清零指令但不同品牌指令不一样而且还可能误操作。我选择了一种更通用的做法基线法。第一次使用测量之前记录设备当前的累计能量作为基线之后每次读取用当前累计能量减去基线就是本次会话的消耗量。状态放到服务端进程的全局变量里就够了。因为 AI 客户端每启动一个会话通常就会重新拉起 MCP 服务端进程状态天然跟着会话走不需要额外的持久化。如果哪天需要跨会话保留统计再考虑 SQLite 落库。这个后面再展开。平滑处理也是放在会话级别。我用了一个指数移动平均EMA公式smooth 0.8 * last_smooth 0.2 * current_power系数 0.8 是我实测调出来的。功耗计瞬时值噪声大平滑系数取太大虽然曲线很干净但真实的大电流变化会被抹平取太小又遮挡不住波动。0.8 是一个折中值既能压住 OLED 刷新时带来的跳变又不至于让模型误以为功耗永远不变。4. 从零实现 FastMCP 服务端的核心代码4.1 环境准备写 MCP 服务端目前最顺手的是官方 Python SDK 里的 FastMCP 封装。它把协议细节都藏起来了你只需要定义工具函数剩下的注册、返回、错误处理都由框架完成。安装依赖就那么两个pip install pyserial mcppyserial负责和功耗计串口通信mcp提供 FastMCP 框架。装完之后先在命令行里测试一下串口能不能正常读取确认端口号。比如python -c import serial; print(serial.tools.list_ports.comports())这段代码会列出所有可用串口找到你的 IoT Power 对应的端口号。Linux 用户可能还需要检查一下当前用户有没有访问串口设备的权限否则PermissionError会让你卡在第一步。4.2 核心代码串口读取与数据解析整个服务端文件我建议保持单一文件方便 AI 客户端直接拉起。下面是串口读取部分的完整实现import json import os import time from collections import deque from typing import Any import serial SERIAL_PORT os.environ.get(SERIAL_PORT, /dev/ttyUSB0) BAUD_RATE int(os.environ.get(BAUD_RATE, 115200)) READ_TIMEOUT float(os.environ.get(READ_TIMEOUT, 2.0)) RETRY_COUNT int(os.environ.get(RETRY_COUNT, 3)) RETRY_INTERVAL float(os.environ.get(RETRY_INTERVAL, 0.3)) # 最近30条采样记录作历史查询用 HISTORY_CACHE deque(maxlen30) def _parse_line(line: str) - dict: 解析功耗计输出的一行数据兼容 JSON 和 CSV 两种格式。 line line.strip() if not line: raise ValueError(空行) if line.startswith({): data json.loads(line) required [voltage_v, current_a, power_w] for key in required: if key not in data: raise ValueError(f字段缺失: {key}) return data # CSV 格式: voltage, current, power, capacity, energy, temperature parts [p.strip() for p in line.split(,)] if len(parts) 5: raise ValueError(CSV 字段不足) return { voltage_v: float(parts[0]), current_a: float(parts[1]), power_w: float(parts[2]), capacity_mah: float(parts[3]), energy_mwh: float(parts[4]), temperature_c: float(parts[5]) if len(parts) 5 else None, } def _read_frame() - dict: 读取一帧功耗数据失败自动重试。 last_err for _ in range(RETRY_COUNT): try: with serial.Serial(SERIAL_PORT, BAUD_RATE, timeoutREAD_TIMEOUT) as ser: line ser.readline().decode(utf-8, ignore) if line: return _parse_line(line) except serial.SerialException as exc: last_err str(exc) time.sleep(RETRY_INTERVAL) raise ValueError(f读取功耗计失败: {last_err})这段代码值得注意的点有三处。一是解析函数做了 JSON 和 CSV 双兼容。你手头设备如果不是 JSON 输出改一下这里就行MCP 工具部分完全不用动。二是每次读取都重新打开串口读完立刻关闭。有人会觉得反复开关串口浪费性能但在 MCP 场景下AI 发起一次工具调用本身就有毫秒到秒级的开销串口的开关损耗完全可忽略。换来的是资源不独占其他工具也能正常访问串口。三是重试逻辑。设备刚上电、驱动没就绪、串口被其他程序占用都可能导致第一次读取失败。重试三次间隔 300ms绝大多数临时问题都能被扛过去。如果三次都失败就直接抛异常让模型在对话里告诉用户“功耗计读取异常请检查连接”。4.3 核心代码MCP 工具注册与回调接下来是 MCP 工具部分同样是全文件的核心。这里我直接注册前面设计的那四个工具from mcp.server.fastmcp import FastMCP mcp FastMCP(iot-power) # 会话状态 SESSION_START_TIME time.time() BASELINE_ENERGY_MWH None BASELINE_CAPACITY_MAH None LAST_EMA_POWER None def _push_history(data: dict, power_ema: float): record { ts: time.strftime(%H:%M:%S), voltage_v: data.get(voltage_v), current_a: data.get(current_a), power_w: data.get(power_w), power_ema: power_ema, } HISTORY_CACHE.append(record) def _update_ema(power_w: float) - float: global LAST_EMA_POWER if LAST_EMA_POWER is None: LAST_EMA_POWER power_w else: LAST_EMA_POWER 0.8 * LAST_EMA_POWER 0.2 * power_w return LAST_EMA_POWER mcp.tool() def get_power_now() - dict[str, Any]: 实时读取 IoT Power 功耗计当前的电压、电流、功率和温度并返回平滑后的功率。 适合回答“现在功耗多少”“当前电流正不正常”这类问题。 data _read_frame() ema _update_ema(data[power_w]) _push_history(data, ema) return { voltage_v: data[voltage_v], current_a: data[current_a], power_w: data[power_w], power_ema: round(ema, 3), temperature_c: data.get(temperature_c), read_at: time.strftime(%Y-%m-%d %H:%M:%S), } mcp.tool() def get_history(limit: int 10) - dict[str, Any]: 返回最近一段时间内缓存的功耗采样记录。 limit 控制返回条数默认 10 条最多 30 条。适合回答“功耗趋势怎么样”这类问题。 limit max(1, min(limit, 30)) records list(HISTORY_CACHE)[-limit:] if not records: return {message: 还没有采样记录请先调用 get_power_now} return {count: len(records), records: records} mcp.tool() def session_statistics() - dict[str, Any]: 返回本次会话的统计信息时长、平均功率、累计能量、平均电压。 适合回答“从开始到现在一共用了多少电”“平均功耗是多少”这类问题。 now time.time() data _read_frame() elapsed now - SESSION_START_TIME global BASELINE_ENERGY_MWH, BASELINE_CAPACITY_MAH if BASELINE_ENERGY_MWH is None: BASELINE_ENERGY_MWH data[energy_mwh] BASELINE_CAPACITY_MAH data[capacity_mah] energy_used data[energy_mwh] - BASELINE_ENERGY_MWH capacity_used data[capacity_mah] - BASELINE_CAPACITY_MAH avg_power energy_used / max(1e-6, elapsed / 3600) # 平均功率 能量 / 时长 return { elapsed_seconds: round(elapsed, 1), samples_in_cache: len(HISTORY_CACHE), avg_power_w: round(avg_power, 3), energy_used_mwh: round(energy_used, 3), capacity_used_mah: round(capacity_used, 3), latest_voltage_v: data[voltage_v], } mcp.tool() def reset_session() - dict[str, Any]: 重置会话基线将当前累计能量和容量记为基准并清空历史缓存。 适合开始一轮新测量之前调用确保后续统计只统计新时间段内的数据。 global SESSION_START_TIME, BASELINE_ENERGY_MWH, BASELINE_CAPACITY_MAH, LAST_EMA_POWER data _read_frame() SESSION_START_TIME time.time() BASELINE_ENERGY_MWH data[energy_mwh] BASELINE_CAPACITY_MAH data[capacity_mah] LAST_EMA_POWER None HISTORY_CACHE.clear() return {message: 会话基线已重置, baseline_energy_mwh: BASELINE_ENERGY_MWH} if __name__ __main__: mcp.run(transportstdio)代码实现之后用一条命令手动启动测试python iot_power_mcp.pyFastMCP 默认以 stdio 模式启动也就是从标准输入读指令、往标准输出写调试日志。这个时候你看到的画面是黑窗口、没有任何输出其实它在等待 AI 客户端连接。可以用 MCP 标准调试入口连接也可以在客户端侧配置好之后直接对话测试。4.4 跑通自测用最小验证脚本确认串口读取没问题在接 AI 客户端之前我强烈建议先用一个独立的最小脚本单独验证串口链路import os import sys sys.path.insert(0, os.path.dirname(__file__)) from iot_power_mcp import _read_frame print(_read_frame())如果你能看到一帧 JSON说明串口链路、解析逻辑全部正常接下来出问题只可能出在 MCP 配置层。这一步能省掉后面联调时至少一半的排查时间。5. 联调实录让 AI 自己去看一整晚的待机功耗5.1 客户端的 MCP 配置方法服务端写好了接下来就是把服务端挂到 AI 客户端上。目前主流的 AI 客户端都支持标准 MCP 配置格式大致相同都是在对应配置文件里加一个mcpServers字段{ mcpServers: { iot-power: { command: python, args: [/absolute/path/to/iot_power_mcp.py], env: { SERIAL_PORT: COM3, BAUD_RATE: 115200 } } } }配置里最关键的是command和args。客户端会以子进程的方式拉起你的服务端脚本然后通过标准输入输出通信。env里传入串口配置这样脚本就能在不对代码做修改的情况下适配 Windows 和 Linux 两种环境。配置完成之后重启客户端然后在对话里直接发一句“查看一下当前功耗计读数”AI 如果成功调用了get_power_now说明整条链路已经通了。5.2 实战让 AI 检查一整晚的待机稳定性链路通了之后我就拿它做了第一件正事让 AI 判断一块开发板在“假睡眠”状态下的功耗稳定性。操作流程是这样的。我先让 AI 调用reset_session清空基线把开发板切到睡眠模式然后连续对话“再读一次”“隔几秒再读一次”“现在呢”。每读一次AI 都会自动调用get_power_now把结果存进历史缓存。测了大约十分钟之后我问了一句“分析一下这几次读数有没有异常”。AI 做的第一件事是调用get_history取出最近的全部记录然后开始分析。它给出的结论是这样的采样序号电压 (V)电流 (A)功率 (W)平滑功率 (W)15.100.0120.0610.06125.110.0110.0560.05735.100.0880.4490.13545.110.0130.0660.12155.100.0130.0660.110AI 注意到第三次采样突然出现了一个 0.449W 的尖峰结合 0.088A 的电流判断是某个外设周期性唤醒。如果没有平滑功率这个字段仅凭瞬时功率人眼很容易误读成持续的大电流。但有了 EMA模型一眼就能看出“只有一次异常尖峰不是持续漏电”并且建议我去查睡眠模式下的定时唤醒组件。这个结论和我在逻辑分析仪上看到的现象对上了。确实有一个看门狗定时器在每 30 秒左右唤醒一次只是之前我没注意。5.3 联调中遇到的三个典型坑第一类是串口被占用。我在联调过程中开着上位机软件没关结果 MCP 里读了几次全部报Resource busy。排查方法很简单把所有可能占用串口的软件全部关掉然后用最小验证脚本单独测一次。如果最小脚本能读到数据说明服务端逻辑没问题问题一定在串口占用上。第二类是设备上电瞬间的乱码。功耗计的 3.3V 主控在 USB 刚上电的时候串口输出可能会有一段乱码。如果程序在这个时间窗口内读到乱码JSON 解析会直接失败。我的方案是依赖抛出异常后重试第二次重试的时候设备已经稳定数据就正常了。如果你手头设备乱码窗口特别长可以把RETRY_COUNT调大一点、RETRY_INTERVAL也调大一点。第三类不是程序问题是模型的问题。模型并不知道“某种设备正常功耗应该是多少”。第一次联调时我让它分析一块充电中的开发板的功耗它复述了“电流 0.82A、功率 4.1W”然后补了一句“数值看起来正常”没有给出任何有价值的信息。后来我学乖了在提示词里明确写清楚判据待机状态下电流低于 0.05A 视为正常充电状态下 0.6A 到 1.2A 视为正常超过 1.5A 判定异常。模型拿到阈值之后判断准确多了。这个经验你应该记住MCP 工具给 AI 的是眼睛但判断标准还是得靠你在提示词里给它。6. 回到起点这套方案还能怎么延伸6.1 从“读”到“控”继电器闭环目前这套 MCP 服务端只做了数据读取但很多功耗测试场景是要求控制的。比如你发现异常功耗想直接断电重启设备。那就可以在硬件链路上加一个可控的 USB 继电器然后给 MCP 服务端增加一个工具mcp.tool() def set_output(state: int 1) - dict[str, Any]: 控制功耗计输出端的通断。state1 接通state0 断开。 # 通过 GPIO 或串口指令控制继电器 ...加了控制工具之后AI 的判断就不再只是“发现了问题”而是“发现问题之后主动断电复位”。这个闭环才是真正意义上的 AI 硬件运维助手。6.2 把服务端改成网络模式多设备与远程读取串口模式受限于物理距离功耗计必须插在跑 AI 客户端的那台电脑上。如果想把功耗计放在实验室的某个角落或者同时监控多台设备可以把 MCP 服务端拆成两部分硬件端用一个常驻程序读串口数据、暴露 HTTP 接口MCP 服务端通过 HTTP 调用硬件端拿数据。工具签名可以改成带设备参数mcp.tool() def get_power_now(device_id: str default) - dict[str, Any]: 读取指定设备号的实时功耗数据。一个 MCP 服务端挂多台功耗计之后价值会明显提升。你可以让 AI 对比两台设备在同一负载下的功耗差异或者轮询多个设备判断谁在异常漏电。6.3 持久化是有必要的一次意外重启的教训别看进程内缓存简单真用起来有个隐患。我在一次长时间测试中AI 客户端崩溃重启整个缓存直接没了。之前积累的十几个小时采样数据全部丢失统计基线也从零开始。如果不想重蹈覆辙建议尽早把采样记录落库。方案不复杂用一个 SQLite 文件存两个表一张存采样点字段和时间戳一张存会话统计记录每次reset_session的基线和结束时间。然后在这个基础上再加两个 MCP 工具log_query查历史、export_csv导出数据。这样 AI 就能回答“过去 24 小时平均功耗是多少”这类跨会话问题了。6.4 最后说几句真心话其实把 IoT Power 接入 MCP 之后我最大的感受并不是“AI 变得多聪明”而是“终于有人能在我调试硬件的时候帮我盯数字了”。以前我一边改代码一边抬头看屏幕两边手忙脚乱。现在所有读数都自动进上下文我想查的时候只需要说话AI 会告诉我当前状态、趋势、异常甚至给出排查方向。如果你手头也有一块 USB 功耗计并且平时经常需要做电流记录我的建议很直接先按第 4 章的代码把服务端跑通然后用第 5 章的配置方法接入客户端再在对话里连续问几次功耗数字。看着 AI 自己摸索着去读数据、记录趋势、判断异常的时候你会觉得之前那一下午的反复插拔都值了。