315MHz 遥控器逆向复刻:从 USRP 抓包到 ESP8266 Web 遥控
关键字:315MHz遥控器协议分析;USRP抓包;URH OOK解调;1527编码 EV1527;CMT2157B;rc-switch;ESP8266 FS1000A
手里有个四键遥控器,旁边还躺着一块十年前的 ESP8266,想着终于能派上用场:把遥控协议分析出来,用 ESP8266 + 无线模块重放,再挂上串口或 Wi-Fi,手机就能控设备。
前期做功课,了解到市面这类遥控多半在 315 M / 433 M。思路也就清楚了,先用无线电硬件把信号抓下来、把结构拆开,分析清楚协议、再买对应频段模块接到 ESP8266 上重放。
这是笔者第一次正经碰无线电,坑没少踩:两套十六进制其实是同一串 bit 的不同显示,波形跟原机不完全一样却照样能控……收获倒是不少。
这篇文章按「遥控器拆解 → 定频抓包 → 分析协议 → 解码 → 复刻」记主线;芯片型号和 rtl_433 捷径都是重放跑通之后才补上的。
说明
- 分析对象:四键遥控器
- 实现功能:协议还原 + ESP8266 + FS1000A 重放 + Wi-Fi / Web / 串口控制
- 底层原理:USRP 抓包 + URH 解调分析 + 1527 编码还原 + 射频重放
一、拆机
先看实物。四颗按键、一颗裸芯片、一组 LC 匹配网络加 PCB 天线,典型低成本遥控:

芯片 U1 是 14 脚,表面没有任何丝印。旁边的晶振 Y1 写得很清楚:26.250

这个数有意思:
$$ 26.250 \times 12 = 315.000\text{ MHz} $$
猜测载波就是 315 MHz,下面做空口分析。
二、USRP 定频 + 抓包
连接USRP,启动gnuradio-companion:

设置好中心频率,按遥控器按钮,频谱上能看到信号跳起来。

频谱瀑布图:

中心频率锁定 315 MHz。调制方式还不清楚,使用 URH(Universal Radio Hacker)录波形:

每个按钮多按几次:

三、URH 解调
ASK 解调之后,两电平包络很干净:
高电平:315 MHz 载波存在
低电平:载波关闭这就是 OOK(On-Off Keying),ASK 的特例。


量关键时序(采样率 1 MSps,1 sample = 1 µs):
| 内容 | HIGH | LOW |
|---|---|---|
| 逻辑 0 | 1T ≈ 370 µs | 3T ≈ 1110 µs |
| 逻辑 1 | 3T ≈ 1110 µs | 1T ≈ 370 µs |
| Sync | 1T ≈ 370 µs | 31T ≈ 11.47 ms |
写成编码规则:
0 = 1T HIGH + 3T LOW
1 = 3T HIGH + 1T LOW
SYNC = 1T HIGH + 31T LOWT≈370 µs,比例 1:3 / 3:1,Sync 1:31,这就是市面常见的 1527一类固定码基带编码。
后面解码、选 rc-switch Protocol 1,都建立在这一步上。rc-switch Protocol 1 只是软件库给这种脉冲模板起的编号,不是遥控器的官方协议名。
小坑:URH 的Samples/Symbol不是协议参数,只是「用多少个 sample 显示一个符号」。1 MSps 下 370 µs≈370 samples,设成 400 会显示成0→1000 / 1→1110,设成 300 又变成0→10000 / 1→11110。物理波形没变,只是显示不同。对比信号时要用相同 Samples/Symbol,优先比脉冲时间。
四、按 1527 规则解码四键
协议规则已知后,URH 在合适的 Samples/Symbol 下会把 0/1 压成半字节:
逻辑 0 → 1000 → hex `8`
逻辑 1 → 1110 → hex `e`于是:
e → 1
8 → 0四个按键的 HEX 抓包(每按一次发 5 帧,中间 Pause ≈ 11449 samples ≈ 31T):
# 按钮USRP抓包 ASK解码 数据为HEX格式
## 按钮1
8 [Pause: 11449 samples]
e8888888eeeeeee8e88ee8888 [Pause: 11450 samples]
e8888888eeeeeee8e88ee8888 [Pause: 11449 samples]
e8888888eeeeeee8e88ee8888 [Pause: 11450 samples]
e8888888eeeeeee8e88ee8888 [Pause: 11449 samples]
e8888888eeeeeee8e88ee888 [Pause: 567747 samples]
## 按钮2
8 [Pause: 11446 samples]
e8888888eeeeeee8e88e8e888 [Pause: 11443 samples]
...(4 帧重复)
## 按钮3
8 [Pause: 11444 samples]
e8888888eeeeeee8e88e88e88 [Pause: 11446 samples]
...(4 帧重复)
## 按钮4
8 [Pause: 11446 samples]
e8888888eeeeeee8e88e888e8 [Pause: 11447 samples]
...(4 帧重复)开头单独一个 8 [Pause: 11449] 是第一帧之前的 Sync;中间完整行末尾多出来的 8 是下一帧 Sync 的 HIGH;真正 24 bit 数据是前面 24 个十六进制字符。最后一行短一截,原因见后文「波形不一样也能控」。
按键 1(去掉末尾 Sync HIGH)逐步展开:
URH HEX(24 个字符):
e 8 8 8 8 8 8 8 e e e e e e e 8 e 8 8 e e 8 8 8
逐字符替换:
1 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 1 0 0 1 1 0 0 0
按字节切开:
10000000 11111110 10011000
= 0x80 0xFE 0x98
→ 0x80FE98四个按键:
按钮1: e8888888eeeeeee8e88ee888 → 100000001111111010011000 → 0x80FE98
按钮2: e8888888eeeeeee8e88e8e88 → 100000001111111010010100 → 0x80FE94
按钮3: e8888888eeeeeee8e88e88e8 → 100000001111111010010010 → 0x80FE92
按钮4: e8888888eeeeeee8e88e888e → 100000001111111010010001 → 0x80FE91拆开看,正好对上 EV1527 的数据帧格式,20-bit 固定 ID + 4-bit 按键:
KEY1 = 10000000111111101001 1000 → 0x80FE98
KEY2 = 10000000111111101001 0100 → 0x80FE94
KEY3 = 10000000111111101001 0010 → 0x80FE92
KEY4 = 10000000111111101001 0001 → 0x80FE91
20-bit ID = 0x80FE9
KEY = 0x8 / 0x4 / 0x2 / 0x1(one-hot)┌────────────20 bit────────────┐ ┌4 bit┐
10000000111111101001 1000
└────固定遥控器 ID 0x80FE9─────┘ └KEY1─┘| 按键 | 24-bit Code |
|---|---|
| KEY1 | 0x80FE98 |
| KEY2 | 0x80FE94 |
| KEY3 | 0x80FE92 |
| KEY4 | 0x80FE91 |
五、ESP8266 复刻 + Wi-Fi / 串口
硬件:
| 组件 | 用途 |
|---|---|
| ESP8266 | 主控,发基带 + Web / 串口 |
| FS1000A(315 MHz) | ASK/OOK 发射 |
| 约 23.8 cm 导线 | λ/4 天线 |
接线:
ESP8266 FS1000A
--------------------------------
D2 / GPIO4 ---------> DATA
GND ---------> GND
3V ---------> VCC软件用 rc-switch(Protocol 1),PulseLength=370 µs,重复 5 次,常量 0x80FE98 / 94 / 92 / 91。
手机/电脑 → Wi-Fi → ESP8266(Web / 串口)
↓
按键命令 → 发送器 → GPIO4 → FS1000A → 设备ino代码片段:
#include <RCSwitch.h>
// ========== 硬件配置 ==========
constexpr uint8_t RF_PIN = 4; // GPIO4 / NodeMCU D2
constexpr uint16_t PULSE_US = 370; // 脉冲宽度(微秒)
constexpr uint8_t REPEAT_COUNT = 5; // 重复次数
// ========== 遥控器固定码(24bit) ==========
constexpr uint32_t KEY1 = 0x80FE98;
constexpr uint32_t KEY2 = 0x80FE94;
constexpr uint32_t KEY3 = 0x80FE92;
constexpr uint32_t KEY4 = 0x80FE91;
RCSwitch rf; // RCSwitch 对象
// ========== 按键编号 → RF 码 ==========
uint32_t getKeyCode(uint8_t key) {
switch (key) {
case 1: return KEY1;
case 2: return KEY2;
case 3: return KEY3;
case 4: return KEY4;
default: return 0;
}
}
// ========== 发送单次 RF 信号 ==========
void sendKeyRF(uint8_t key) {
uint32_t code = getKeyCode(key);
if (code == 0) return;
Serial.print("TX KEY"); Serial.print(key);
Serial.print(" -> 0x"); Serial.println(code, HEX);
digitalWrite(RF_PIN, LOW); // 确保起始为低电平
rf.send(code, 24); // 发送 24bit 数据
digitalWrite(RF_PIN, LOW); // 结束后拉低
Serial.println("TX DONE");
}
// ========== 初始化 ==========
void setup() {
Serial.begin(115200);
delay(500);
pinMode(RF_PIN, OUTPUT);
digitalWrite(RF_PIN, LOW);
rf.enableTransmit(RF_PIN); // 指定发射引脚
rf.setProtocol(1); // Protocol 1: sync=1:31, 0=1:3, 1=3:1
rf.setPulseLength(PULSE_US); // 脉冲宽度 370µs
rf.setRepeatTransmit(REPEAT_COUNT); // 重复 5 次
Serial.println("RF sender ready");
}
// ========== 主循环示例 ==========
void loop() {
// 例如:每 5 秒依次发送四个按键
static unsigned long lastSend = 0;
static uint8_t currentKey = 1;
if (millis() - lastSend >= 5000) {
lastSend = millis();
sendKeyRF(currentKey);
currentKey = (currentKey % 4) + 1; // 1→2→3→4→1...
}
}1:1复刻原遥控器波形:
六、为什么波形不一样也能控?
先看原遥控器一次按键的真实结构:
SYNC DATA SYNC DATA SYNC DATA SYNC DATA SYNC DATA LOW...
↑帧头 ↑帧头 … ↑发完就停Sync 是下一帧的开头,不是这一帧的结尾。 发完第 5 包没有下一帧了,所以最后一帧 DATA 后面直接进 LOW,不再跟 Sync。
URH 里最后一行比中间行短一截,就是这个现象,少了末尾那个 Sync HIGH,尾部 LOW 还和空闲并在一起。数据本身没少。
复刻时试过三版发送,设备都能控,但 USRP 回采和原机不完全一样:
原遥控器:
SYNC0 DATA1 SYNC1 DATA2 … SYNC4 DATA5
rc-switch(repeat=5):
DATA1 SYNC1 DATA2 … DATA5 SYNC5中间 SYNCₙ + DATAₙ₊₁ 完全合法,前一帧后面的 Sync 正好当下一段的帧头。
差别只在头尾:少一个开头 Sync,或多一个结尾 Sync。rc-switch 每轮 DATA 后固定加 Sync,所以末尾总会多一个。
| 发送方式 | 完整有效帧 | 开头 | 结尾 | 能否控制 |
|---|---|---|---|---|
| 原遥控器 | 5 | Sync | DATA 后直接 LOW | ✅ |
| 手写旧版(DATA 开头) | 4 | 直接 DATA | 多一个 Sync | ✅ |
| rc-switch + 初始 Sync | 5 | Sync | 多一个 Sync | ✅ |
| 最终 1:1 手写版 | 5 | Sync | 与原机一致 | ✅ |
接收端真正在乎的是:载波、OOK 极性、Sync≈1:31、0/1 的 1:3、24 bit、ID=0x80FE9、KEY one-hot、时序容限
不要求整段录音逐边沿相同。重复发包本就是为了抗丢帧。
「波形完全相同」是充分条件,通常不是必要条件;必要的是接收端能恢复出它认可的有效帧。
七、rtl_433 一步到位(后话)
整套控制跑通之后,才试了 rtl_433,发现是条捷径:
rtl_433 -d "driver=uhd" -f 315Mmodel : Smoke-GS558 id : 19448
unit : 1 learn : 0 Raw Code : 197f01
---
Raw Code : 297f01
---
Raw Code : 497f01
---
Raw Code : 897f01被套进烟雾报警器模板 Smoke-GS558,四个 Raw Code 是 19/29/49/89 7F 01。
和第四节解出的 0x80FE98 对不上,但拿去重放也能控,分析:
| 按键 | rtl_433 Raw Code | 24-bit 位反转后 | URH / ESP KEY |
|---|---|---|---|
| 1 | 0x197F01 | 0x80FE98 | 0x80FE98 |
| 2 | 0x297F01 | 0x80FE94 | 0x80FE94 |
| 3 | 0x497F01 | 0x80FE92 | 0x80FE92 |
| 4 | 0x897F01 | 0x80FE91 | 0x80FE91 |
0x197F01
00011001 01111111 00000001
↓ 24 位整体反转
10000000 11111110 10011000
= 0x80FE98原机空中流(MSB-first)是 0x80FE98;rtl_433 只是换了位序打包显示。日常按发 0x80FE98 即可。
八、U1芯片身份:高度疑似 CMT2157B(后话)
重放已经能控设备了,U1 仍然没丝印。后来把 PCB 照片、晶振频率、1527 波形特征丢给 AI,才对到相似度最高的 CMT2157B。
U1 是 SOP14。对照官方 pinout:
SOP14
Pin 1 LED
Pin 2 VDD
Pin 3 GND
Pin 4 RFO
Pin 8 K4
Pin 9 K3
Pin 10 K2
Pin 11 K1
Pin 14 XTALPCB 布局:
U1
┌──────────────┐
LED ← │1 14 │ → 26.250 MHz Y1
VDD ← │2 13 │
GND ← │3 12 │
RF ← │4 11 │ ← KEY
│5 10 │ ← KEY
│6 9 │ ← KEY
│7 8 │ ← KEY
└──────────────┘晶体在右上、RF LC 在左侧、四键进另一侧、14 pin。
① 26.250 MHz 频率指纹
315 MHz → 26.250 MHz crystal
433.92 MHz → 26.2982 MHz crystalUSRP 实测原遥控器 314.989 MHz,就是标称 315。
② 丝印 C1~C4 / L1~L4 = AN155 那套
PCB RF 丝印 C4 L4 C3 L3 C2 L2 C1 L1,与 CMOSTEK AN155 老版 315 MHz BOM(含 Y1=26.25 MHz、U1=CMT2157B/2150L)一致。
③ AN154 官方 1527 配置逐项对上
Sync Size = 32
Sync Value = 0x80000000
Addr Size = 20
Bit Format = 4
Bit Logic 1 = 0xE
Bit Logic 0 = 0x8
Key Size = 4
Stop Size = 00x80000000 → 1 HIGH + 31 LOW
0xE = 1110 → 逻辑 1
0x8 = 1000 → 逻辑 0这就是 URH 里 e / 8 字符串本身;Stop Size=0、一帧 = SYNC + DATA。
④ 默认按键 1/2/4/8
官方默认 K1~K4 = 1/2/4/8;实测得到 8/4/2/1,相当于芯片 K4~K1 顺序(或 EEPROM 反着配)。
总结
| 参数 | 实测值 |
|---|---|
| RF | 约 315 MHz |
| 原遥控器峰值 | 314.989 MHz |
| FS1000A 峰值 | 314.971 MHz |
| 调制 | OOK / ASK |
| 协议 | 1527 / EV1527 |
| 基本单位 T | ≈370 µs |
| 0 | 1T HIGH + 3T LOW |
| 1 | 3T HIGH + 1T LOW |
| Sync | 1T HIGH + 31T LOW |
| 数据长度 | 24 bit |
| 地址长度 | 20 bit |
| 按键长度 | 4 bit |
| 地址 | 0x80FE9 |
| KEY1 | 0x80FE98 |
| KEY2 | 0x80FE94 |
| KEY3 | 0x80FE92 |
| KEY4 | 0x80FE91 |
| 数据位序 | MSB First(rtl_433 Raw Code 为位反转显示) |
| 每次发送 | 5 个 DATA |
| 起始 | 独立 Sync |
| 帧间 | 4 个 Sync |
| 最后一帧 | 无尾部 Sync |
| 硬件疑似 | CMT2157B |
写在最后
前后大概一周。大头耗在前期理论学习和硬件折腾上;真正配好设备之后,软件调试一天就把控制跑通了,串口、Wi-Fi 都是后来添砖加瓦。
固定码(不是滚动码)这条路其实很清楚:采集 → 分析 → 重放。闭环走下来就是:
定频抓包 → 时序认协议 → 按规则解码 → 重放回采OOK 遥控逆向里,抓住这几项就够:
载波频率 脉冲宽度 逻辑编码 bit顺序 帧结构 重复结构芯片一个字都没印,也能从空口把遥控做出来。
另外:第一次焊天线。手抖慎入。技能 +1,硬件工程师?
参考
- CMT2157B datasheet:https://device.report/m/df19d80916848bfab4abee03770d5c85ad5a5a4e4d6228e7fb18b17726054222
附录:完整源码
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <RCSwitch.h>
// =====================================================
// WiFi
// =====================================================
const char* WIFI_SSID = "a";
const char* WIFI_PASSWORD = "88888888";
// =====================================================
// 315 MHz RF
// =====================================================
constexpr uint8_t RF_PIN = 4; // GPIO4 / NodeMCU D2
constexpr uint16_t PULSE_US = 370;
constexpr uint8_t REPEAT_COUNT = 5;
// 原遥控器第一帧前存在一个 Sync。
// rc-switch 自己会在每次 DATA 后追加 Sync。
// 这里设置为false也可以控制
constexpr bool PREPEND_INITIAL_SYNC = false;
// =====================================================
// 遥控器固定码
// =====================================================
constexpr uint32_t KEY1 = 0x80FE98;
constexpr uint32_t KEY2 = 0x80FE94;
constexpr uint32_t KEY3 = 0x80FE92;
constexpr uint32_t KEY4 = 0x80FE91;
// =====================================================
// 对象
// =====================================================
RCSwitch rf;
ESP8266WebServer server(80);
// =====================================================
// RF 命令队列
// =====================================================
constexpr uint8_t QUEUE_SIZE = 8;
uint8_t commandQueue[QUEUE_SIZE];
uint8_t queueHead = 0;
uint8_t queueTail = 0;
uint8_t queueCount = 0;
// =====================================================
// WiFi 重连
// =====================================================
unsigned long lastReconnectAttempt = 0;
constexpr unsigned long RECONNECT_INTERVAL = 5000;
// =====================================================
// 按键 -> RF code
// =====================================================
uint32_t getKeyCode(uint8_t key)
{
switch (key)
{
case 1: return KEY1;
case 2: return KEY2;
case 3: return KEY3;
case 4: return KEY4;
default: return 0;
}
}
// =====================================================
// RF Queue
// =====================================================
bool enqueueKey(uint8_t key)
{
if (key < 1 || key > 4)
return false;
if (queueCount >= QUEUE_SIZE)
{
Serial.println("RF queue full");
return false;
}
commandQueue[queueTail] = key;
queueTail++;
if (queueTail >= QUEUE_SIZE)
queueTail = 0;
queueCount++;
Serial.print("Queued KEY");
Serial.println(key);
return true;
}
bool dequeueKey(uint8_t &key)
{
if (queueCount == 0)
return false;
key = commandQueue[queueHead];
queueHead++;
if (queueHead >= QUEUE_SIZE)
queueHead = 0;
queueCount--;
return true;
}
// =====================================================
// 原遥控器初始 Sync
//
// HIGH 1T
// LOW 31T
// =====================================================
void sendInitialSync()
{
digitalWrite(RF_PIN, HIGH);
delayMicroseconds(PULSE_US);
digitalWrite(RF_PIN, LOW);
delayMicroseconds(
static_cast<uint32_t>(PULSE_US) * 31
);
}
// =====================================================
// RF 发送
// =====================================================
void sendKeyRF(uint8_t key)
{
uint32_t code = getKeyCode(key);
if (code == 0)
return;
Serial.print("TX KEY");
Serial.print(key);
Serial.print(" -> 0x");
Serial.println(code, HEX);
digitalWrite(RF_PIN, LOW);
// 原遥控器第一帧之前的 Sync
if (PREPEND_INITIAL_SYNC)
{
sendInitialSync();
}
// rc-switch:
// Protocol 1
// 370 us
// 24 bit
// repeat 5
rf.send(code, 24);
digitalWrite(RF_PIN, LOW);
Serial.println("TX DONE");
}
// =====================================================
// CORS
// =====================================================
void addCorsHeaders()
{
// 允许任意网页调用
server.sendHeader(
"Access-Control-Allow-Origin",
"*"
);
server.sendHeader(
"Access-Control-Allow-Methods",
"GET, POST, OPTIONS"
);
server.sendHeader(
"Access-Control-Allow-Headers",
"Content-Type"
);
server.sendHeader(
"Access-Control-Max-Age",
"86400"
);
server.sendHeader(
"Cache-Control",
"no-store"
);
}
// =====================================================
// OPTIONS 预检
// =====================================================
void handleOptions()
{
addCorsHeaders();
server.send(
204,
"text/plain",
""
);
}
// =====================================================
// Web 首页
// =====================================================
const char MAIN_PAGE[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width,initial-scale=1,user-scalable=no"
>
<title>315 MHz Remote</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
background: #f3f4f6;
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Arial,
sans-serif;
color: #222;
}
main {
max-width: 460px;
margin: auto;
padding: 24px;
}
h1 {
text-align: center;
margin-bottom: 5px;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 28px;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
button {
height: 105px;
border: 0;
border-radius: 18px;
background: white;
font-size: 25px;
font-weight: 600;
box-shadow:
0 4px 14px rgba(0,0,0,.12);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
button:active {
transform: scale(.96);
background: #e5e7eb;
}
#status {
min-height: 30px;
margin-top: 26px;
text-align: center;
font-size: 17px;
}
.info {
margin-top: 28px;
text-align: center;
color: #777;
font-size: 13px;
line-height: 1.6;
}
</style>
</head>
<body>
<main>
<h1>315 MHz 遥控器</h1>
<div class="subtitle">
ESP8266 + rc-switch
</div>
<div class="grid">
<button onclick="sendKey(1)">
按钮 1
</button>
<button onclick="sendKey(2)">
按钮 2
</button>
<button onclick="sendKey(3)">
按钮 3
</button>
<button onclick="sendKey(4)">
按钮 4
</button>
</div>
<div id="status">
等待操作
</div>
<div class="info">
Protocol 1 · 370 µs · 24 bit
</div>
</main>
<script>
let busy = false;
async function sendKey(key)
{
if (busy)
return;
busy = true;
const status =
document.getElementById("status");
status.innerText =
"发送按钮 " + key + "...";
try
{
const response =
await fetch("/key/" + key);
const result =
await response.json();
if (result.ok)
{
status.innerText =
"按钮 " + key + " 已加入发送队列";
}
else
{
status.innerText =
result.error || "发送失败";
}
}
catch (e)
{
status.innerText =
"请求失败";
}
setTimeout(
() =>
{
busy = false;
},
250
);
}
</script>
</body>
</html>
)rawliteral";
// =====================================================
// HTTP:首页
// =====================================================
void handleRoot()
{
addCorsHeaders();
server.send_P(
200,
"text/html; charset=utf-8",
MAIN_PAGE
);
}
// =====================================================
// HTTP:按键
// =====================================================
void handleKeyRequest(uint8_t key)
{
addCorsHeaders();
if (enqueueKey(key))
{
String json =
"{\"ok\":true,\"key\":" +
String(key) +
",\"code\":\"0x";
uint32_t code = getKeyCode(key);
char hexCode[7];
snprintf(
hexCode,
sizeof(hexCode),
"%06lX",
(unsigned long)code
);
json += hexCode;
json += "\"}";
server.send(
200,
"application/json; charset=utf-8",
json
);
}
else
{
server.send(
503,
"application/json; charset=utf-8",
"{\"ok\":false,\"error\":\"RF queue full\"}"
);
}
}
// =====================================================
// HTTP:状态
// =====================================================
void handleStatus()
{
addCorsHeaders();
String json = "{";
json += "\"ok\":true,";
json += "\"wifi\":";
json +=
(WiFi.status() == WL_CONNECTED)
? "true,"
: "false,";
json += "\"ssid\":\"";
json += WIFI_SSID;
json += "\",";
json += "\"ip\":\"";
json += WiFi.localIP().toString();
json += "\",";
json += "\"rssi\":";
json += String(WiFi.RSSI());
json += ",";
json += "\"queue\":";
json += String(queueCount);
json += "}";
server.send(
200,
"application/json; charset=utf-8",
json
);
}
// =====================================================
// 404
// =====================================================
void handleNotFound()
{
// 对不存在的 URL 的 OPTIONS 也返回 CORS
if (server.method() == HTTP_OPTIONS)
{
handleOptions();
return;
}
addCorsHeaders();
server.send(
404,
"application/json; charset=utf-8",
"{\"ok\":false,\"error\":\"Not found\"}"
);
}
// =====================================================
// 串口控制
// =====================================================
void handleSerial()
{
while (Serial.available())
{
char c = Serial.read();
switch (c)
{
case '1':
enqueueKey(1);
break;
case '2':
enqueueKey(2);
break;
case '3':
enqueueKey(3);
break;
case '4':
enqueueKey(4);
break;
case '\r':
case '\n':
break;
default:
Serial.print("Unknown command: ");
Serial.println(c);
break;
}
}
}
// =====================================================
// WiFi
// =====================================================
void connectWiFi()
{
Serial.println();
Serial.print("Connecting to WiFi: ");
Serial.println(WIFI_SSID);
WiFi.mode(WIFI_STA);
// 不把 WiFi 参数反复写入 Flash
WiFi.persistent(false);
WiFi.begin(
WIFI_SSID,
WIFI_PASSWORD
);
unsigned long start = millis();
while (
WiFi.status() != WL_CONNECTED &&
millis() - start < 20000
)
{
delay(500);
Serial.print(".");
}
Serial.println();
if (WiFi.status() == WL_CONNECTED)
{
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
Serial.print("RSSI: ");
Serial.print(WiFi.RSSI());
Serial.println(" dBm");
}
else
{
Serial.println("WiFi connection failed");
Serial.println("Will retry automatically.");
}
}
// =====================================================
// setup
// =====================================================
void setup()
{
Serial.begin(115200);
delay(500);
// ---------------------------------------------------
// RF
// ---------------------------------------------------
pinMode(RF_PIN, OUTPUT);
digitalWrite(RF_PIN, LOW);
rf.enableTransmit(RF_PIN);
// Protocol 1:
//
// Sync = 1:31
// 0 = 1:3
// 1 = 3:1
rf.setProtocol(1);
// 原遥控器实测
rf.setPulseLength(PULSE_US);
rf.setRepeatTransmit(
REPEAT_COUNT
);
// ---------------------------------------------------
// WiFi
// ---------------------------------------------------
connectWiFi();
// ---------------------------------------------------
// HTTP Routes
// ---------------------------------------------------
server.on(
"/",
HTTP_GET,
handleRoot
);
// KEY1
server.on(
"/key/1",
HTTP_GET,
[]()
{
handleKeyRequest(1);
}
);
server.on(
"/key/1",
HTTP_OPTIONS,
handleOptions
);
// KEY2
server.on(
"/key/2",
HTTP_GET,
[]()
{
handleKeyRequest(2);
}
);
server.on(
"/key/2",
HTTP_OPTIONS,
handleOptions
);
// KEY3
server.on(
"/key/3",
HTTP_GET,
[]()
{
handleKeyRequest(3);
}
);
server.on(
"/key/3",
HTTP_OPTIONS,
handleOptions
);
// KEY4
server.on(
"/key/4",
HTTP_GET,
[]()
{
handleKeyRequest(4);
}
);
server.on(
"/key/4",
HTTP_OPTIONS,
handleOptions
);
// Status
server.on(
"/status",
HTTP_GET,
handleStatus
);
server.on(
"/status",
HTTP_OPTIONS,
handleOptions
);
server.onNotFound(
handleNotFound
);
server.begin();
// ---------------------------------------------------
// 信息
// ---------------------------------------------------
Serial.println();
Serial.println("==============================");
Serial.println("315 MHz WiFi Remote");
Serial.println("==============================");
Serial.print("SSID : ");
Serial.println(WIFI_SSID);
Serial.print("IP : ");
if (WiFi.status() == WL_CONNECTED)
Serial.println(WiFi.localIP());
else
Serial.println("Not connected");
Serial.println();
Serial.println("HTTP API:");
Serial.println("GET /key/1");
Serial.println("GET /key/2");
Serial.println("GET /key/3");
Serial.println("GET /key/4");
Serial.println("GET /status");
Serial.println();
Serial.println("CORS:");
Serial.println("Access-Control-Allow-Origin: *");
Serial.println();
Serial.println("Serial:");
Serial.println("1 -> KEY1 0x80FE98");
Serial.println("2 -> KEY2 0x80FE94");
Serial.println("3 -> KEY3 0x80FE92");
Serial.println("4 -> KEY4 0x80FE91");
Serial.println();
Serial.println("Ready.");
}
// =====================================================
// loop
// =====================================================
void loop()
{
// ---------------------------------------------------
// WiFi 自动重连
// ---------------------------------------------------
if (WiFi.status() != WL_CONNECTED)
{
if (
millis() - lastReconnectAttempt >=
RECONNECT_INTERVAL
)
{
lastReconnectAttempt = millis();
Serial.println(
"WiFi disconnected, reconnecting..."
);
WiFi.disconnect();
WiFi.begin(
WIFI_SSID,
WIFI_PASSWORD
);
}
}
// ---------------------------------------------------
// HTTP
// ---------------------------------------------------
server.handleClient();
// ---------------------------------------------------
// Serial
// ---------------------------------------------------
handleSerial();
// ---------------------------------------------------
// RF Queue
// ---------------------------------------------------
uint8_t key;
if (dequeueKey(key))
{
sendKeyRF(key);
}
yield();
}