1. 先把Part 1的底子说清楚1.1 为什么Part 1还不够用Part 1写完之后我把那套“树莓派Pico W 红外LED 一个能发几个按键码的简单Web页面”的方案在客厅里用了将近一个月。说实话单设备测试的时候一切都很美好电视开机、关机、音量加减都灵发一个NEC码也没啥延迟。但一旦真正放到日常使用场景里问题立刻就暴露出来了——电视、空调、机顶盒、风扇、投影仪五个遥控器摆在茶几上而我的iPad只能控制其中一台设备每次切设备还得重新烧录固件或者改配置文件这种体验连“半成品”都算不上。Part 2要解决的事情很清楚让iPad变成一个真正的“万能遥控中心”。不是再做一个测试用的玩具而是具备多设备切换、红外码库管理、红外学习、场景联动、基础定时这些实用功能的完整工具。如果你手上已经有一套能用的红外发射硬件哪怕是ESP32而不是Pico W这篇文章里的软件架构和代码思路也完全能搬过去。1.2 Part 2要做的三件事我给自己定了三个核心目标整个Part 2的代码和调试都是围绕这三件事展开的第一件事是码库结构化。Part 1里我是把每一个按键当成一个独立函数写的广播一个红外码就写一个方法换设备简直噩梦。Part 2的码库必须变成数据驱动的JSON结构新增设备只需要往配置里加一段数据不需要改代码。第二件事是让iPad上的操作界面像一个真正的遥控器。既然用的是iPad那就得把iPad的大屏和触控优势用起来左侧设备列表、右侧按键面板支持横竖屏切换按键要有按下反馈空调这种需要温控调节的设备还要有加减按钮和温度显示。第三件事是加红外学习功能。市面上的万能遥控器贵不说很多品牌的空调码在公开库里根本找不到最靠谱的办法就是自己拿原装遥控器对着接收头“录一遍”。这部分是Part 2里工程量最大、也最容易踩坑的地方后面我会详细说。2. 红外遥控的原理与红外码库设计2.1 红外的“敲门暗号”载波与协议在写码库之前得先把红外遥控的底层原理理清楚不然你录回来的码是什么格式都分不清。红外遥控本质上是利用波长940nm左右的红外光来传输信号。但有个关键的物理特性如果直接让红外LED亮灭来传信号在室内环境光下接收端几乎无法区分这是遥控器还是太阳反射所以所有红外遥控都会先把信号调制到38kHz也有个别用36kHz或40kHz的载波上接收端只认这个频率的脉冲其他光统统忽略。打个比方红外遥控就像两个人隔着很远用灯光交流如果直接开灯关灯环境里其他光源会干扰如果约定一个固定的闪烁频率比如一秒钟闪38000次接收端只盯着这个闪烁频率看就能从嘈杂环境里准确抓到信号。这个“闪烁频率”就是载波而遥控器要表达的实际数据是靠载波脉冲的长短组合来编码的。编码方式有很多种NEC协议、SONY SIRC协议、Philips RC5协议、日系空调常用的NEC变种等等。好消息是市面上绝大多数家电用的都是NEC协议或者它的变种所以我们把NEC吃透就能覆盖80%以上的场景。2.2 NEC协议信号结构NEC协议的一个完整帧长这样引导码9ms的高电平载波 4.5ms的低电平空载相当于对接收端喊一声“注意我要开始发数据了”8位地址码客户码8位地址反码8位命令码按键码8位命令反码如果按键一直按着会每隔110ms发送一个重复码9ms高 2.25ms低编码的规则也很简单逻辑“1”是560微秒高 1690微秒低逻辑“0”是560微秒高 560微秒低。简单记就是“高电平都是560微秒关键看后面的低电平多长”——长的是1短的是0。这个结构对码库设计极其重要。因为NEC协议是“先解码成16进制码再存储”还是“存原始脉冲时序”会直接决定你码库的通用性。我的做法是两种都存已知协议标准的情况下存NEC码比如0x00FF00FF这种格式并标注协议类型为NEC如果是录制的未知码可能是不完整的NEC变种或者空调的长时序码就存原始脉冲数组数组里每个元素是微秒级的电平持续时间。2.3 码库数据结构设计码库的JSON结构我设计成了三层设备层、按键层、信号层。一个设备下面挂多个按键每个按键对应一个信号描述。以下是我在Part 2里实际使用的结构{ devices: [ { name: 客厅电视, brand: Sony, protocol: SIRC, remote_code: 1, keys: { power: { code: 0x15, type: hex }, volume_up: { code: 0x12, type: hex }, volume_down: { code: 0x13, type: hex } } }, { name: 主卧空调, brand: Gree, protocol: custom, remote_code: 0, keys: { power: { type: raw, raw: [9000, 4500, 560, 560, 560, 1690, 560, 560, 560, 1690] } } } ] }type为hex时直接存协议参数由设备端根据协议类型重新编码成红外波形type为raw时存原始脉冲数组设备端原样发送。这样的好处很明显NEC设备占空间极小一个按键就一行配置空调这种复杂长码不需要费力去解析直接录制原始波形成功率反而更高。3. 整体方案选型为什么我坚持用Web App3.1 技术栈对比做iPad上的控制界面有几种技术路线可以走原生iOS App、微信小程序、Web AppPWA。我最后选了Web App理由很实在。原生App最大的问题是门槛需要Apple开发者账号一年不便宜需要用Mac做签名打包而且写Swift/Objective-C的工程量对一次周末项目来说太重了。微信小程序更麻烦iPad上的微信小程序的硬件接口限制多局域网socket能力也受限而且你总不能让用户为了控制个电视先打开微信。Web App的好处在于iPad自带Safari就是一等公民浏览器HTMLCSSJavaScript写完直接访问不需要安装任何证书配合PWA的manifest用“添加到主屏幕”功能图标和全屏窗口都像原生App而且局域网内任何设备——手机、电脑——都能打开同一个控制面板。也就是说同一套代码iPad能用你的手机也能用这就是Web技术的天然优势。3.2 设备端与iPad端的通信设计通信这块我选用了最朴素的HTTP JSON。设备端Pico W跑一个轻量TCP服务器监听80端口iPad用fetch请求就能发命令。为什么不用WebSocket因为红外遥控是典型的事件型、低频率控制不需要实时双通道HTTP请求一次一来回干净利落也不容易断线重连出问题。API设计就两个核心接口POST /api/send Body: {device: 客厅电视, key: power} 作用发送指定设备指定按键的红外码 POST /api/learn Body: {duration: 1000} 作用让设备端进入红外学习模式采样指定时长的红外信号返回脉冲数组码库不直接存在设备端。设备端只保留一份最基本的NEC编码器和原始波形发送器完整的码库JSON放在iPad本地用localStorage持久化iPad每次发命令时把要发的信号描述一起POST给设备端。这样设计的好处是设备端固件几乎不用改所有遥控器逻辑都在iPad端以后换一个接收设备iPad端配置基本不变。4. 设备端实操从Wi-Fi到红外发射4.1 硬件准备与驱动电路计算Part 2的发射电路我沿用了Part 1的方案但做了两个改进增加了一个NPN三极管S8050做电流放大并把红外LED从1颗增加到了2颗并联。直接拿GPIO驱动红外LED不是不行但Pico W的GPIO只能输出大约3.3V、几mA电流驱动距离通常不到2米放客厅根本不够用。加三极管的原理很简单GPIO引脚只负责提供基极信号小电流开关真正的发射电流由3.3V或5V电源通过三极管提供。IR LED的正向压降Vf大概是1.2V到1.5V如果供电电压是5V限定工作电流IF取80mA到100mA那么限流电阻阻值就是R (VCC - Vf) / IF (5 - 1.3) / 0.09 ≈ 41欧姆取标称值33欧姆实际电流略高于100mA在PWM发射通常占空比低于50%时完全能承受。注意不要用1欧姆或者直接不加限流电阻不然LED会过流烧掉我Part 1就烧过两颗。Pico W的PWM输出频率设置为38000Hz占空比我用50%。控制逻辑是需要发载波时PWM输出38kHz方波空闲时输出低电平。4.2 Pico W服务器代码实现设备端我用的是MicroPython。核心逻辑分三块Wi-Fi连接、HTTP服务器、红外发射。import network import socket import machine import json import time # 使用 PWM 输出 38kHz 载波 IR_PIN machine.Pin(16) ir_pwm machine.PWM(IR_PIN) ir_pwm.freq(38000) ir_pwm.duty_u16(0) # 默认不输出 WIFI_SSID your_wifi WIFI_PASSWORD your_password # 连接 Wi-Fi wlan network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(WIFI_SSID, WIFI_PASSWORD) while not wlan.isconnected(): time.sleep(0.5) print(Connected:, wlan.ifconfig())红外发送的核心是处理NEC编码。我写了一个send_nec函数把十六进制码转成高低电平脉宽序列def send_nec(addr, cmd): # 引导码9ms 高 4.5ms 低 pulse_ir(9000) time.sleep_us(4500) # 地址码 地址反码 data ((addr 0xFF) 24) | (((~addr) 0xFF) 16) | \ ((cmd 0xFF) 8) | ((~cmd) 0xFF) for i in range(31, -1, -1): bit (data i) 0x01 pulse_ir(560) if bit: time.sleep_us(1690) else: time.sleep_us(560) # 结束脉冲 pulse_ir(560) time.sleep_us(10000) def pulse_ir(us): ir_pwm.duty_u16(32768) # 50% 占空比开始发载波 time.sleep_us(us) ir_pwm.duty_u16(0) # 停止载波原始波形发送也很简单接收到的raw数组里偶数位是高电平时间、奇数位是低电平时间轮流pulse和sleep即可。HTTP服务器用socket模块监听80端口解析请求路径简单分发def handle_request(client): request client.recv(1024).decode() path request.split( )[1] if len(request.split( )) 1 else / if path /api/send: # 读取 body解析 JSON body_start request.find(\r\n\r\n) 4 body request[body_start:] data json.loads(body) # 根据 type 发码 if data.get(type) hex: addr data.get(addr, 0) cmd data.get(cmd, 0) send_nec(addr, cmd) elif data.get(type) raw: send_raw(data.get(raw)) client.send(bHTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n) client.send(json.dumps({status: ok})) client.close()一个容易忽略的坑MicroPython的time.sleep_us在长时间比如引导码的9000微秒下计时还算准但如果你把整段码分成过小的片段频繁调用协议栈和GPIO切换的开销可能导致帧间距过大接收端解析失败。实测下来NEC这种短码影响不大但空调的raw长码一旦超过几百个脉冲最好把整个序列一次性放进数组用一个循环连续发射不要中途处理网络请求。5. iPad端实操遥控面板与学习功能5.1 遥控器页面实现iPad端我用纯HTMLCSSJavaScript实现没有引入任何框架。为什么不用Vue或者React因为这个项目的复杂度没有到需要框架的程度一个HTML文件加一个CSS文件加一个JS文件就够了纯手写反而更容易理解每一行逻辑也方便以后塞到iPad本地存储里离线使用。界面布局参考了iOS系统自带的遥控器设计底部有一排设备Tab电视、空调、机顶盒、风扇点击切换设备时上方按键区域根据当前设备的keys字段动态渲染。每个按键都是一个大按钮至少60像素高因为要用手指直接按不能按手机那种小按钮的习惯来设计。核心的发送函数长这样async function sendKey(deviceName, keyName) { const device config.devices.find(d d.name deviceName); const key device.keys[keyName]; let payload { device: deviceName, key: keyName, type: key.type }; if (key.type hex) { payload.addr key.addr; payload.cmd key.cmd; } else if (key.type raw) { payload.raw key.raw; } const resp await fetch(http://192.168.1.100/api/send, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }); const result await resp.json(); if (result.status ! ok) { alert(发送失败); } }按钮的按下反馈是给每个按键添加CSS的active伪类做背景变色同时用navigator.vibrate触发轻震iPad不支持震动所以这段逻辑要捕获异常不影响其他设备使用。按键布局通过CSS Grid实现电视是3x4网格空调是2x5网格加一个温度显示区域根据不同设备类型的keys自动适配。5.2 红外学习功能实现红外学习是Part 2重头戏也是我在调试阶段流汗最多的地方。硬件上需要一个IR接收头VS1838B接Pico W的GPIO15学习原理就是采样引脚上的电平变化记录每个高电平或低电平的持续时间形成raw数组。MicroPython采样这块要特别小心。Python解释器不像C语言那样有硬实时保证电平中断响应可能会抖动所以采样循环必须用纯汇编级的中断和机器计时。我实际使用的方案是用machine.time_pulse_us()函数它能精确测量引脚上单个脉冲的持续时间虽然不能同时捕捉高和低但配合逻辑判断可以完整记录def learn_raw(max_duration_ms500): ir_pin machine.Pin(15, machine.Pin.IN) raw [] start time.ticks_ms() last_value ir_pin.value() while time.ticks_diff(time.ticks_ms(), start) max_duration_ms: pulse_us machine.time_pulse_us(ir_pin, last_value, 10000) if pulse_us 0: break raw.append(pulse_us) last_value 1 - last_value return raw这段代码会记录500毫秒内的所有电平脉冲长度然后iPad端拿到raw数组后直接存到码库。录制空调码的时候500毫秒可能不够空调温度/模式的信号通常较长我把duration参数暴露出来录制界面可以选500ms、1000ms、2000ms三档。学习功能最容易踩的坑是环境光干扰和学习过程误触。如果接收头附近有阳光直射或者其他红外遥控器在发码录制回来的raw数组里面会混入大量垃圾信号。我的经验是学习的时候把被录的遥控器贴着接收头1到5厘米按下按键后等0.5秒再松手确保完整录到引导码和所有数据位。5.3 场景与定时功能码库稳定之后我发现切换设备一个个按键操作还是不够顺手。比如晚上看电影的流程是开电视、关灯、开音响、电视切HDMI1、投影仪展开幕布五个动作要按五下。于是我在iPad端加了一个“场景”功能预设一组动作序列每个动作包含设备和按键以及一个间隔延迟。const scenes { 电影模式: [ { device: 客厅电视, key: power, delay: 300 }, { device: 客厅灯, key: off, delay: 500 }, { device: 客厅音响, key: power, delay: 400 }, { device: 客厅电视, key: hdmi1, delay: 300 } ] }; async function runScene(name) { const actions scenes[name]; for (const action of actions) { await sendKey(action.device, action.key); await sleep(action.delay); } }定时功能更简单直接用setTimeout或者定时器组件到点后执行场景。我做了一个“晚安模式”每天23:30自动关电视、关灯、关空调。iPad需要保持浏览器页面打开不然休眠后定时器会挂掉。这个限制我接受了毕竟纯粹用iPad做定时中枢本来就不是它的强项但能用一个简单的Web页面实现“伪定时”已经比拿一堆遥控器强。定时这块还衍生出一个实用功能按时间区分信号。比如空调的“制热模式”在早上和晚上需要的温度不同我可以定义“早上7点自动发送26℃制热”、“晚上睡前发送28℃制热”本质上就是场景加时间条件。6. 常见问题与排查技巧实录6.1 iPad连不上设备怎么办这是被问得最多的问题。症状是iPad浏览器打开控制页面点按键无反应fetch报错“NetworkError”。排查顺序基本是固定的先看iPad和设备是不是同一Wi-Fi网段。如果路由器开了“AP隔离”很多双频路由器的访客网络默认开这个无线设备之间是不允许互相访问的这是最常见的原因。解决方法是关闭AP隔离或者把设备端改接到网线且iPad连接同一个主SSID。再看设备端的Wi-Fi连接是否还活着。Pico W这类开发板对Wi-Fi断线重连处理得很弱路由器一重启或者信道变化板子可能就默默离线了。日志打印是个好东西每次请求都在串口打印一次IP和路径iPad连不上时先看串口有没有收到报文。最后别忘了iPad的“本地网络”权限。iOS 14之后App访问局域网设备会弹权限请求但Safari里的网页有时不会弹你需要在系统设置 - 隐私 - 本地网络中检查Safari的开关。这个权限问题非常隐蔽很多人改了半天代码结果只是Safari没权限访问局域网。6.2 信号发射距离短或没反应发射距离短是最能直接暴露电路设计问题的症状。如果你站在设备旁边能控稍微走远一步就不行多半是红外LED驱动电流不够。检查三极管基极的限流电阻我用的是1k欧姆如果用了10k欧姆基极电流太小三极管无法完全导通LED电流会远低于预期。也有可能是载波频率不准。Pico W的PWM是基于系统时钟分频的如果Wi-Fi功能开启导致处理器频率变化38kHz可能会有几百赫兹的偏差这个偏差对于大多数接收头在可容忍范围内但如果你的接收头老化了就会出现“看起来在闪但设备没反应”的情况。我习惯用手机摄像头对着遥控器按一下肉眼看不到的红外光在手机CMOS传感器上会显示成紫白色这是最快速验证LED是否在发射的方法。6.3 录制信号“有码但没反应”能录到码回放时设备却不响应这是学习模式最让人抓狂的问题。我自己碰到过三种情况。第一种是录制的raw数组采样时间精度不够。MicroPython的time_pulse_us函数在高负载时会丢精度导致录回来的脉冲长度整体偏小或偏大。解决办法是录完后对比已知NEC码的raw看引导码的9000微秒是否准确如果偏差超过10%说明采样环境有问题可以尝试降低CPU负载或者用C语言重写采样逻辑。第二种是空调的高电平脉冲很宽需要额外放大。很多空调的红外信号里单个载波脉冲的宽度可能超过8毫秒如果接收头或者采样代码对长脉冲处理不当会在中途误判为结束信号导致录回来的数据被截断。把录制时长拉长到2000ms并且确保接收头供电稳定能解决大部分问题。第三种是回放时使用了错误的电平极性。接收头输出的电平定义高、低电平时间在录回来时可能是反的因为接收头是反相输出如果发送时按原样发相当于把整个信号翻转了。最简单的方法是录制时加一个“是否反相”的开关如果发现录制数据明显异常把每个脉冲长度翻转一下再看效果。6.4 开发工具和文件管理上的坑最后说说使用iPad做开发时遇到的两个工具问题都是踩过坑才知道的第一个是HBuilderX无法直接运行在iPad上。很多人在电脑上用HBuilderX写前端项目顺手想复制到iPad里跑结果发现iPad的iPadOS根本没有安装桌面级IDE的入口。这其实不是某个软件的问题而是iPadOS不开放传统桌面软件的运行环境。替代思路有两个一是用RunJS、Play.js这类的在线代码运行器直接在iPad里写JavaScript和Python脚本二是把开发好的HTML文件通过文件App或云盘同步到iPad然后本地打开。但注意如果你在iPad上打开一个本地HTML文件这个页面的fetch请求可能会受到跨域限制访问局域网设备需要额外处理CORS所以更省事的做法是把页面放到设备端的Web服务器上iPad浏览器直接访问局域网IP。第二个是iPad编辑Windows上的文件很麻烦。你电脑上挂着一个码库JSON想在iPad上改两行代码发现iPad文件App默认看不到Windows的本地目录。我的办法是把码库文件放到一个局域网HTTP服务器上就是Pico W设备端的/static路径Windows端用任意编辑器改完上传iPad端通过一个“刷新配置”按钮拉取新码库。另外如果是局域网共享文件夹方案用支持SMB协议的文件管理App比如文件App自带的“连接服务器”功能也能访问Windows共享目录但格式要求是SMB://IP/共享名别用中文路径。6.5 关于iPad已停用/恢复的提醒最后想特别说一句网上有很多“iPad已停用连接iTunes解锁教程”我不建议大家照着类似教程乱操作。iPad如果因为多次输错密码进入停用状态正规流程就一个——用之前同步过的电脑做恢复或者直接找官方支持。不要去试那些绕过激活锁的所谓“解锁服务”既容易把数据整没也可能违反使用条款。与其等到出问题再找教程不如提前做好备份。iPad在设置里开启iCloud云备份平时重要的照片、文件都会自动同步如果还想多一层保障超过一年以上的数据定期用电脑做加密备份。这个习惯比任何教程都更有用。我在这篇文章里写的所有内容都是日常使用的软件和硬件方案不包含任何旁门左道。7. 写在最后的实操心得整个Part 2从码库重构到学习功能调通断断续续花了我两个周末。回头去看最有价值的部分其实不是某个具体代码而是确定了“码库驱动一切”的架构思路。现在的系统里新买一台设备只需要对着原装遥控器录一遍码存进JSONiPad端的控制面板就能自动多出一个设备。家里来客人问这iPad上的遥控面板怎么弄出来的我说原理就一句话红外光脉冲的长短组合加上一台能按Wi-Fi指令发光的小板子。最后再分享一个小技巧发送红外码之后最好等50毫秒再发下一条。很多设备在接收完一帧数据后需要时间处理连续两条命令间隔太短会导致第二条被忽略。这个时间在NEC协议的重试帧间隔110ms基础上我取了个经验值——所有场景动作之间延迟300毫秒起步实测下来稳定很少丢命令。如果后续有空我打算把码库做成一个可导入导出的文件格式再接入Home Assistant让iPad控制面板和智能家居自动化联动起来。这个项目目前的状态我已经很满意至少家里的茶几上再也看不到那一堆乱七八糟的遥控器了。