关键字: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 天线,典型低成本遥控:

微信图片_2026-08-21_095332_487.jpg

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

image-20260821095246459.png

这个数有意思:

$$ 26.250 \times 12 = 315.000\text{ MHz} $$

猜测载波就是 315 MHz,下面做空口分析。

二、USRP 定频 + 抓包

连接USRP,启动gnuradio-companion:

image-20260821083133229.png

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

image-20260821082840466.png

频谱瀑布图:

image-20260821082850410.png

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

image-20260821100127177.png

每个按钮多按几次:

image-20260821090457391.png

三、URH 解调

ASK 解调之后,两电平包络很干净:

高电平:315 MHz 载波存在
低电平:载波关闭

这就是 OOK(On-Off Keying),ASK 的特例。

image-20260821152640937.png

image-20260821094352514.png

量关键时序(采样率 1 MSps,1 sample = 1 µs):

内容HIGHLOW
逻辑 01T ≈ 370 µs3T ≈ 1110 µs
逻辑 13T ≈ 1110 µs1T ≈ 370 µs
Sync1T ≈ 370 µs31T ≈ 11.47 ms

写成编码规则:

0    = 1T HIGH + 3T LOW
1    = 3T HIGH + 1T LOW
SYNC = 1T HIGH + 31T LOW

T≈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
KEY10x80FE98
KEY20x80FE94
KEY30x80FE92
KEY40x80FE91

五、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复刻原遥控器波形:
image-20260826164801082.png

六、为什么波形不一样也能控?

先看原遥控器一次按键的真实结构:

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,所以末尾总会多一个。

发送方式完整有效帧开头结尾能否控制
原遥控器5SyncDATA 后直接 LOW
手写旧版(DATA 开头)4直接 DATA多一个 Sync
rc-switch + 初始 Sync5Sync多一个 Sync
最终 1:1 手写版5Sync与原机一致

接收端真正在乎的是:载波、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 315M
model     : 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 Code24-bit 位反转后URH / ESP KEY
10x197F010x80FE980x80FE98
20x297F010x80FE940x80FE94
30x497F010x80FE920x80FE92
40x897F010x80FE910x80FE91
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  XTAL

PCB 布局:

              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 crystal

USRP 实测原遥控器 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     = 0
0x80000000 → 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
01T HIGH + 3T LOW
13T HIGH + 1T LOW
Sync1T HIGH + 31T LOW
数据长度24 bit
地址长度20 bit
按键长度4 bit
地址0x80FE9
KEY10x80FE98
KEY20x80FE94
KEY30x80FE92
KEY40x80FE91
数据位序MSB First(rtl_433 Raw Code 为位反转显示)
每次发送5 个 DATA
起始独立 Sync
帧间4 个 Sync
最后一帧无尾部 Sync
硬件疑似CMT2157B

写在最后

前后大概一周。大头耗在前期理论学习和硬件折腾上;真正配好设备之后,软件调试一天就把控制跑通了,串口、Wi-Fi 都是后来添砖加瓦。

固定码(不是滚动码)这条路其实很清楚:采集 → 分析 → 重放。闭环走下来就是:

定频抓包 → 时序认协议 → 按规则解码 → 重放回采

OOK 遥控逆向里,抓住这几项就够:

载波频率  脉冲宽度  逻辑编码  bit顺序  帧结构  重复结构

芯片一个字都没印,也能从空口把遥控做出来。

另外:第一次焊天线。手抖慎入。技能 +1,硬件工程师?

参考

附录:完整源码

#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();
}

标签: none

添加新评论

行为验证™ 安全组件加载中...