
1. 从串口到终端为什么我要把温湿度传感器接进 Codex Skill手头有一块 ESP32 加一个 DHT22数据一直在串口里刷但每次想看趋势都得开串口监视器、手动抄数字时间一长就懒得看了。我想要的其实很简单让 Codex 通过一个自定义 Skill 自己去读串口把温湿度解析出来然后在终端里画成能一眼看懂的图表。这样我敲一句自然语言它就能把当前环境状态和最近一段时间的曲线一起甩给我。这个场景适合三类人一是做嵌入式或 IoT 原型、天天跟串口打交道但不想写重复解析代码的开发者二是想让 AI Agent 真正“感知”物理世界、而不只是处理文本的玩家三是手上已经有 DHT11/DHT22、SHT30、BME280 这类传感器想快速搭一个本地监控面板的人。核心检索词就是 Codex Skill 读取串口温湿度数据并在终端渲染图表整条链路我会拆成可复制的配置和脚本。Codex 的 Skill 本质上是一个带元信息的可执行能力包它告诉模型“什么时候该调用我、调用时传什么参数、我返回什么结构”。和普通函数调用不同的是Skill 需要一份描述文件让模型理解意图边界否则它不知道“看看现在多热”应该触发串口读取。所以我们要做两件事写一个稳定的串口读取脚本再写一份 Skill 描述把它注册进去。数据流是这样的传感器通过 UART 每 2 秒吐一行 JSON 或 CSVPython 脚本用 pyserial 打开端口、按行读取、解析出温度和湿度做一次简单的滑动平均去抖然后把结果交给终端渲染层。渲染层用 rich 的 Live 和 Sparkline 或者 plotext 画曲线Codex 负责在对话里触发这个 Skill 并把图表文本回显给你。整条链路不依赖任何云服务本地闭环。我踩过的坑主要集中在串口权限和编码上。Linux 下普通用户默认没有 /dev/ttyUSB0 的读写权限得把用户加进 dialout 组Windows 下端口号会变不能写死 COM3。另外传感器刚上电的前几帧经常是乱码或空值脚本必须能跳过无效行而不是直接崩掉。这些细节后面都会给到具体处理。统一 Key 和 API 通道这块我用 TaoToken 来托管模型调用凭证这样 Skill 里不需要硬编码任何密钥换模型或换环境只改一处配置。它的 API 地址是 https://taotoken.net/api兼容常见的 OpenAI 风格请求格式接入成本很低。下面先把前置准备讲清楚再进入可复制的配置环节。2. TaoToken 前置统一 Key 与 Codex Skill 的接入准备在写 Skill 之前先把模型调用这条线理顺。Codex 这类编码 Agent 在触发 Skill 时背后往往还要调用一次模型来做意图判断或结果润色如果每个脚本各自管理 Key很快就会乱。TaoToken 的作用就是提供一个统一的 API 入口你只维护一份 Key所有 Skill 和脚本都从这里取。第一步是拿到 API Key。访问 https://taotoken.net/api-keys 创建注意这个页面是控制台里的密钥管理入口创建后立刻复制保存页面刷新后就不再完整显示。Key 的形态通常是一串以特定前缀开头的字符串把它写进环境变量而不是代码里这是最基本的安全习惯。第二步是确认 Base URL。TaoToken 的 API 根地址是 https://taotoken.net/api注意这里不带任何查询参数。如果你用的是 OpenAI 兼容的 SDK通常只需要把 base_url 指向它模型名按平台文档填。对于 Codex 的 Skill 场景我建议把这两个值放进一个统一的配置文件比如 ~/.codex/env.json让所有 Skill 共享。第三步是理解 Skill 的注册位置。Codex 的 Skill 一般放在项目根目录的 .codex/skills/ 下每个 Skill 一个子目录里面至少有一个描述文件常见是 skill.json 或 SKILL.md和一个可执行入口。描述文件里要写清楚 name、description、触发示例和参数 schema模型靠这些判断是否调用。入口脚本可以是 Python、Node 或 shell只要能被当前环境执行。这里给一份最小可用的 Skill 描述结构路径是 .codex/skills/serial-env/skill.json。注意 JSON 里不要写注释字段名按你实际使用的 Codex 版本为准下面这份是通用形态{ name: serial-env-monitor, description: 读取串口温湿度传感器数据并在终端渲染图表适用于 DHT22/SHT30 等通过 UART 输出数据的场景, entry: monitor.py, runtime: python3, triggers: [ 看看现在的温湿度, 读取串口传感器并画图, 环境数据趋势 ], parameters: { port: { type: string, description: 串口设备路径如 /dev/ttyUSB0 或 COM3, default: /dev/ttyUSB0 }, baud: { type: integer, description: 波特率, default: 115200 }, window: { type: integer, description: 图表展示的最近采样点数, default: 30 } } }描述里的 triggers 很关键它决定了你用自然语言能不能唤起这个 Skill。写得越贴近日常说法模型命中率越高。parameters 里的 default 让 Skill 在没有显式传参时也能跑起来这对小白特别友好。环境变量方面建议在 shell 的启动文件里加两行把 Key 和 Base URL 导出。Linux/macOS 写进 ~/.bashrc 或 ~/.zshrcWindows 用系统环境变量界面设置。这样 Skill 脚本里用 os.environ 读取即可永远不出现明文密钥。export TAOTOKEN_API_KEY你的Key export TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你更习惯用配置文件也可以在项目里放一个 .env然后用 python-dotenv 加载。但要注意 .env 必须进 .gitignore否则密钥会跟着仓库泄露。这一点很多人第一次都会忘。依赖安装也要提前做。串口读取需要 pyserial终端图表需要 rich 和 plotext模型调用需要 openai 或 httpx。一条命令装齐pip3 install pyserial rich plotext openai python-dotenv装完后用 python3 -c import serial, rich, plotext 验证一下没有报错就说明环境 OK。如果报 ModuleNotFoundError检查是不是 pip 和 python 版本对不上树莓派上常见的是 pip3 装到了 Python 3.9 而默认 python 是 3.11。最后确认串口设备能被识别。Linux 下插上 USB 转串口后用 ls /dev/ttyUSB* 或 ls /dev/ttyACM* 看设备名Windows 在设备管理器里看端口号。如果设备名每次插拔都变建议用 udev 规则固定或者干脆在 Skill 参数里显式传入。前置准备做到这一步就可以进入真正的配置和脚本环节了。3. 可复制配置串口读取脚本与终端图表渲染参数这一节是整篇的核心所有代码都可以直接复制运行。我会把配置拆成三块串口读取与解析、数据缓冲与滤波、终端图表渲染。每块都给完整片段并说明关键参数为什么这么设。先看串口读取。传感器输出的格式各家不同我按最常见的“每行一个 JSON”来写如果你的设备输出 CSV改一下 parse_line 即可。脚本用 pyserial 打开端口设置超时避免阻塞逐行读取并跳过无效数据。路径和参数都从环境变量或命令行取方便 Skill 调用。import os import json import time import serial from collections import deque PORT os.environ.get(SERIAL_PORT, /dev/ttyUSB0) BAUD int(os.environ.get(SERIAL_BAUD, 115200)) WINDOW int(os.environ.get(CHART_WINDOW, 30)) def parse_line(line: str): line line.strip() if not line: return None try: obj json.loads(line) except json.JSONDecodeError: return None temp obj.get(temperature) hum obj.get(humidity) if temp is None or hum is None: return None try: return float(temp), float(hum) except (TypeError, ValueError): return None def open_serial(port: str, baud: int): return serial.Serial( portport, baudratebaud, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout2.0, )timeout2.0 是必须的否则 readline 在没数据时会一直挂住Skill 调用就会卡死。bytesize、parity、stopbits 显式写出来是为了避免不同平台默认值不一致导致的乱码。接下来是数据缓冲和滤波。传感器原始值会有抖动直接画图会像心电图一样乱跳。我用一个固定长度的 deque 做滑动窗口再算最近 5 个点的移动平均作为展示值。deque 的 maxlen 设成 WINDOW超出自动丢弃最老的内存不会涨。class EnvBuffer: def __init__(self, window: int 30, smooth: int 5): self.temps deque(maxlenwindow) self.hums deque(maxlenwindow) self.times deque(maxlenwindow) self.smooth smooth def add(self, temp: float, hum: float): self.temps.append(temp) self.hums.append(hum) self.times.append(time.strftime(%H:%M:%S)) def smoothed(self): def avg(seq): data list(seq)[-self.smooth:] return sum(data) / len(data) if data else 0.0 return avg(self.temps), avg(self.hums) def latest(self): if not self.temps: return None return self.temps[-1], self.hums[-1]smooth5 是个经验值采样间隔 2 秒时相当于 10 秒的平滑窗口既能压住噪声又不会太迟钝。如果你的传感器本身很稳可以调到 3如果环境波动大调到 8 也行。终端图表渲染用 plotext 画曲线rich 负责布局和实时刷新。plotext 的好处是纯文本输出不依赖 GUISSH 里也能看。下面这个函数把缓冲区的数据画成温度湿度双曲线并返回一个 rich 的 Panel。import plotext as plt from rich.panel import Panel from rich.text import Text def render_chart(buf: EnvBuffer) - Panel: temps list(buf.temps) hums list(buf.hums) if not temps: return Panel(Text(等待传感器数据..., styleyellow), title环境监控) plt.clf() plt.plot(temps, label温度 °C, markerbraille) plt.plot(hums, label湿度 %, markerbraille) plt.title(串口温湿度趋势) plt.xlabel(采样点) plt.ylabel(数值) plt.theme(dark) chart plt.build() t, h buf.smoothed() header Text() header.append(f当前温度 {t:.1f}°C , stylebold green) header.append(f当前湿度 {h:.1f}%, stylebold blue) return Panel(Text(chart) Text(\n) header, title环境监控, border_stylecyan)markerbraille 是 plotext 里比较细腻的字符点阵曲线看起来比默认的方块平滑很多这就是“炫酷”的来源。theme(dark) 适配深色终端如果你用浅色背景改成 light。主循环把上面三块串起来用 rich 的 Live 做实时刷新。采样间隔设 2 秒刷新率 1Hz避免 CPU 空转。同时把每次采样写入 SQLite方便后面回看历史。import sqlite3 from rich.live import Live DB os.environ.get(ENV_DB, ./env_data.db) def init_db(): conn sqlite3.connect(DB) conn.execute( CREATE TABLE IF NOT EXISTS env ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts DATETIME DEFAULT CURRENT_TIMESTAMP, temperature REAL, humidity REAL ) ) conn.commit() return conn def main(): conn init_db() buf EnvBuffer(windowWINDOW) ser open_serial(PORT, BAUD) with Live(render_chart(buf), refresh_per_second1, screenFalse) as live: while True: raw ser.readline().decode(utf-8, errorsignore) parsed parse_line(raw) if parsed: temp, hum parsed buf.add(temp, hum) conn.execute( INSERT INTO env (temperature, humidity) VALUES (?, ?), (temp, hum), ) conn.commit() live.update(render_chart(buf)) time.sleep(0.1) if __name__ __main__: main()decode 时用 errorsignore 是为了防止半帧数据里的非法字节让整个脚本抛异常。screenFalse 让图表在普通终端里内联刷新而不是全屏接管这样你还能同时看日志。如果你想让 Codex 通过 Skill 调用它把入口改成接受参数的形式并在开头读取 TAOTOKEN_API_KEY 做一次模型连通性检查。模型调用部分用 OpenAI 兼容客户端base_url 指向 https://taotoken.net/api模型名按平台文档填。这样 Skill 在返回图表前还能让模型用一句话总结趋势比如“过去一分钟温度上升 0.8 度”。from openai import OpenAI client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ.get(TAOTOKEN_BASE_URL, https://taotoken.net/api), ) def summarize(temps, hums): prompt f温度序列 {temps[-10:]}湿度序列 {hums[-10:]}用一句话总结趋势。 resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], max_tokens80, ) return resp.choices[0].message.content.strip()到这里配置部分就完整了。串口读取、缓冲滤波、图表渲染、模型总结四块各司其职参数都有默认值改环境变量就能适配不同硬件。下一节我们实际跑一次看成功结果长什么样。4. 验证请求从数据采集到图表输出的完整动作配置写完不跑一遍等于没写。这一节我带你走一次完整的验证动作启动脚本、观察终端图表、确认数据库落盘、再通过 Codex Skill 触发一次自然语言调用。每一步都有预期输出对不上就按第五节排查。先确认串口有数据。在跑主脚本之前用一条最简单的命令看原始输出避免脚本背锅。Linux 下用 stty 配好波特率再 catWindows 用串口助手或 PowerShell 的读取命令。这一步的目的是确认传感器真的在吐数据而不是脚本解析错了。stty -F /dev/ttyUSB0 115200 raw -echo timeout 5 cat /dev/ttyUSB0预期能看到类似 {temperature: 24.3, humidity: 56.1} 的行每 2 秒一行。如果全是乱码八成是波特率不对把 115200 换成 9600 再试。如果一行都没有检查接线DHT22 的数据脚要接上拉电阻TX/RX 别接反。确认原始数据正常后启动主脚本。第一次跑建议把 WINDOW 设小一点比如 10这样几十秒就能看到曲线成形。export SERIAL_PORT/dev/ttyUSB0 export SERIAL_BAUD115200 export CHART_WINDOW30 python3 monitor.py启动后终端会出现一个带边框的面板上半部分是温度湿度双曲线下半部分显示当前平滑后的数值。曲线会随着新数据从左往右推进大约 1 分钟后填满整个窗口。如果曲线是平的说明传感器数值没变化可能是读到了缓存或模拟值。成功结果的判断标准有三条曲线随环境变化有起伏、当前数值和传感器实际读数一致、面板每秒刷新一次不卡顿。你可以对着传感器哈一口气湿度曲线应该会在几秒内明显上扬这是最直观的验证。接着验证数据库。另开一个终端用 sqlite3 查最近 10 条记录确认时间戳和数值都在。sqlite3 env_data.db SELECT id, ts, temperature, humidity FROM env ORDER BY id DESC LIMIT 10;预期输出每行一条记录ts 是写入时间temperature 和 humidity 是浮点数。如果表是空的说明 INSERT 没执行成功回去看脚本里 conn.commit() 有没有被跳过。最后验证 Codex Skill 调用。在 Codex 对话里输入“读取串口传感器并画图”模型应该命中 serial-env-monitor 这个 Skill按描述里的默认参数执行 monitor.py并把图表文本回显到对话里。如果模型没触发检查 skill.json 的 triggers 是否包含你用的说法或者手动在对话里点名 Skill 名称。Skill 返回的内容里除了图表还应该有一句模型生成的趋势总结。这句话是通过 TaoToken 的 API 调出来的如果这里报错说明 Key 或 Base URL 有问题但图表本身仍然会正常显示因为渲染是本地完成的。这种解耦设计的好处是模型不可用时监控不中断。验证通过后你可以把 Skill 的默认参数改成你实际的端口和波特率这样以后一句话就能唤起。如果想让图表更“炫酷”可以调 plotext 的 marker 和 theme或者加一条阈值线温度超过 30 度时曲线变红。这些都属于锦上添花核心链路已经跑通了。整个验证过程大概 5 分钟其中 1 分钟等曲线成形其余都是命令执行。跑通一次之后后面换传感器只需要改 parse_line 里的字段名其他部分不用动。5. 本篇常见错排查401、串口占用与图表乱码这一节按真实报错来组织每条都给出触发场景、原因和修复动作。这些错误我在调试时基本都遇到过按顺序排查能省不少时间。第一个高频错误是 401 Unauthorized。触发场景是 Skill 调用模型总结趋势时终端打印 openai.AuthenticationError 或 HTTP 401。原因通常是 TAOTOKEN_API_KEY 没设置、设置成了空字符串、或者 Key 已失效。修复动作先 echo $TAOTOKEN_API_KEY 确认有值再检查是不是复制时带了空格或换行。如果用的是 .env 文件确认 python-dotenv 的 load_dotenv() 在读取环境变量之前调用。还有一种情况是 base_url 写成了带路径的形式正确值就是 https://taotoken.net/api不要在后面加 /v1 或斜杠。第二个错误是串口被占用报错信息类似 serial.serialutil.SerialException: could not open port /dev/ttyUSB0: [Errno 16] Device or resource busy。原因是上一个脚本进程没退干净或者串口监视器还开着。修复动作先 ps aux | grep monitor.py 找到残留进程 kill 掉再确认没有其他程序占用串口。Linux 下可以用 fuser /dev/ttyUSB0 看谁占着。如果权限不足报的是 Permission denied把当前用户加进 dialout 组然后重新登录。sudo usermod -aG dialout $USER # 重新登录后生效 groups | grep dialout第三个错误是图表乱码或错位表现为曲线里出现方块、问号或者面板边框对不齐。原因是终端不支持 braille 字符或字体缺字形。修复动作把 plotext 的 marker 从 braille 换成 dot 或 fhd这两个对字体要求低。另外确认终端编码是 UTF-8Linux 下 locale 命令看 LANG 是否以 UTF-8 结尾不是的话在 shell 配置里设 export LANGC.UTF-8。第四个错误是 reading choices 相关报错信息类似 KeyError: choices 或 AttributeError: NoneType object has no attribute choices。这通常发生在模型返回结构不符合预期时比如请求被限流返回了错误对象或者模型名写错导致返回体里没有 choices 字段。修复动作在 summarize 里加一层防御先判断 resp 和 resp.choices 是否存在不存在就返回默认文案而不是崩掉。同时确认模型名是平台支持的不要凭记忆填。def summarize(temps, hums): try: resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: f温度 {temps[-5:]} 湿度 {hums[-5:]}一句话总结}], max_tokens80, ) if not resp or not resp.choices: return 趋势数据不足 return resp.choices[0].message.content.strip() except Exception as e: return f总结不可用: {type(e).__name__}第五个错误是 OAuth 或鉴权方式不匹配。有些环境里 Codex 走的是 OAuth 流程而 Skill 里用的是 API Key两者混用会报 invalid_grant 或 unauthorized_client。修复动作明确 Skill 内部统一用 API Key 方式不要复用 Codex 自身的登录态。如果你在 Codex 配置里同时配了 OAuth 和 API Key检查优先级确保 Skill 读取的是 TAOTOKEN_API_KEY 而不是别的变量。第六个错误是数据解析全为 None图表一直显示“等待传感器数据”。原因是 parse_line 里的字段名和传感器实际输出对不上。修复动作先把原始行打印出来看确认字段是 temperature 还是 temp、humidity 还是 hum。改 parse_line 时记得同时兼容大小写有些固件输出的是 Temperature。排查顺序建议从外到内先确认串口有原始数据再确认解析成功再看图表渲染最后看模型调用。这样能快速定位问题在哪一层而不是盲目改代码。大部分问题集中在权限、字段名和 Key 配置这三处把这三处守住链路就很稳。6. 把 Skill 用起来从单次监控到长期环境看板跑通单次监控之后下一步是让它真正融入日常工作流。最直接的做法是把 Skill 注册成 Codex 的常驻能力这样你在写代码、查日志的间隙随口一句“现在机房温度多少”就能拿到图表不用切终端。Skill 的价值不在于替代专业监控系统而在于把物理世界的数据拉进你本来就在用的对话界面里。如果你要长期跑建议把采样脚本做成 systemd 服务或后台进程Skill 只负责查询最近数据并渲染而不是每次调用都重新开串口。这样多个 Skill 可以共享同一份数据缓冲也不会因为频繁开关串口导致传感器复位。数据库里的历史数据还能拿来做简单的趋势分析比如按小时聚合平均温湿度。模型调用这块如果你需要更复杂的分析比如“判断过去两小时湿度是否持续高于 70% 并给出建议”可以把 SQL 查询结果喂给模型让它做判断。这时候统一 Key 的优势就体现出来了所有 Skill 共用一份凭证换模型只改一个配置项。需要长期编码或跑 Agent 任务的可以了解下 Coding Plan 这类方案把额度集中管理。接入文档和更多示例在 https://taotoken.net/doc 可以查到模型对话调试入口在 https://taotoken.net/chat密钥管理还是 https://taotoken.net/api-keys。建议先把单传感器链路跑稳再考虑多节点或告警避免一上来就铺太大。最后留一个实用技巧把 Skill 的 triggers 写成你平时说话的习惯比如“看看热不热”“环境怎么样”命中率比正式说法高得多。图表参数也别一次调太多先把曲线跑出来再慢慢加阈值线和颜色。物理世界的数据采集最怕接线和权限问题代码本身反而简单把这两块守住剩下的都是顺水推舟。