BLE 开发者文档

OAKIOT 灯控设备 — BLE GATT 服务 & WiFi 安全配网协议参考

目录 1. 系统概览 2. BLE GATT 服务总览 2.1 LED 灯光服务 2.2 WiFi 配网服务 2.3 时间服务 2.4 标准 CTS 服务 2.5 OTA 更新服务 3. 安全配网模式 (WiFiProv) 3.1 BLE 连接与端点 3.2 PoP 验证码 3.3 Security1 握手实现 3.4 AES-CTR 会话加密 3.5 WiFi 凭据配置 3.6 WiFi 扫描 3.7 Protobuf 字段号完整参考 4. 常见问题 5. HTTP 局域网 API 6. 串口配置命令 7. 配置渠道对比

1. 系统概览

设备默认始终进入正常模式,安全配网模式需由 Web 端主动触发:

条件模式BLE 广播名可用服务
默认启动正常模式OAKIOT_XXXXXXLED / WiFi / Time / CTS GATT 服务
Web 端触发重置配网模式PROV_OAKIOT_XXXXXXWiFiProv protocomm(安全配网)

XXXXXX = eFuse MAC 后 3 字节十六进制

两种模式互斥运行(ESP32 BLE 栈限制)。设备默认进入正常模式,无 WiFi 时仍可通过 BLE 控制灯光。配网模式仅在 Web 端点击"WiFi 重置"后触发,配网成功后自动重启回到正常模式。

2. BLE GATT 服务总览

正常模式下注册 5 个 GATT 服务:

服务UUID特征值数
LED 灯光0000ff00-...-00805f9b34fb5
WiFi 配网0000ff10-...-00805f9b34fb2
自定义时间0000ff20-...-00805f9b34fb1
标准 CTS0x18052
OTA 更新0000ff30-...-00805f9b34fb3

2.1 LED 灯光服务 0000ff00

HSV 颜色 — 0000ff01 R/W/Notify 3字节

字节字段范围说明
0H0-255色相(映射 0°-360°)
1S0-255饱和度(0=白色)
2V0-255亮度
// 示例: 红色半亮
write([0x00, 0xFF, 0x80])

电源开关 — 0000ff02 R/W/Notify 1字节

说明
0x00关闭
0x01开启

灯效模式 — 0000ff03 R/W/Notify 1字节

名称说明
0x00静态 HSV固定颜色(默认)
0x01时间智能灯日出日落自动调光
0x64-0x6F动态灯效100-111,共 12 种

灯效 100-111 详表:柔和呼吸(100)、色相漫游(101)、调色板呼吸(102)、烛光摇曳(103)、潮汐渐变(104)、日落渐变(105)、极光流转(106)、心跳脉冲(107)、月光涟漪(108)、色彩交融(109)、萤火虫(110)、四季流转(111)

灯效模式≥1 时,HSV 写入不影响显示;切回 0 恢复。

灯效参数 — 0000ff04 R/W/Notify 3字节

字节字段默认说明
0Speed128灯效速度
1Param1128含义因灯效而异
2Param2128含义因灯效而异

时间灯效配置 — 0000ff05 R/W/Notify

写入 14 字节:

偏移字段类型默认说明
0hueuint8206色相
1saturationuint80饱和度
2maxBrightnessuint8255最大亮度
3nightBrightnessuint830夜灯亮度
4-5startTimeint16 LE-1渐亮开始(分钟,-1=日落)
6-7peakTimeint16 LE1260最亮时间(21:00=1260)
8-9nightTimeint16 LE1290夜灯时间(21:30=1290)
10-11offTimeint16 LE-1关闭时间(-1=日出前30分)
12fadeUpDurationuint80渐亮时长分钟(0=自动)
13fadeDownDurationuint830渐暗时长分钟

读取 18 字节: 前 14 字节配置 + 4 字节状态(阶段, 当前亮度, 日出时, 日落时)

// JavaScript 写入示例
const cfg = new Uint8Array(14);
cfg[0] = 206; cfg[1] = 0; cfg[2] = 255; cfg[3] = 30;
new DataView(cfg.buffer).setInt16(4, -1, true);     // startTime
new DataView(cfg.buffer).setInt16(6, 1260, true);   // peakTime
new DataView(cfg.buffer).setInt16(8, 1290, true);   // nightTime
new DataView(cfg.buffer).setInt16(10, -1, true);    // offTime
cfg[12] = 0; cfg[13] = 30;

2.2 WiFi 配网服务 0000ff10 非安全

此服务以明文传输 WiFi 密码,存在被 BLE 嗅探窃取的风险,仅适用于开发调试和简单测试。生产环境请使用 Security1 安全配网

WiFi 配置 — 0000ff11 R/W

写入: UTF-8 字符串 SSID\nPASSWORD(换行分隔)

读取: 返回当前存储的 SSID(不含密码)

特殊指令: 写入单字节 0x00 → 清除凭据并重启进入安全配网模式

// 设置 WiFi
write(encode("MyWiFi\n12345678"))
// 重置 WiFi → 进入配网模式
write([0x00])

WiFi 状态 — 0000ff12 R/Notify 1字节

说明
0x00未连接
0x01正在连接
0x02已连接
0x03连接失败

2.3 自定义时间服务 0000ff20

时间同步 — 0000ff21 R/W/Notify 4字节

uint32 小端序,UTC epoch 秒数。

// 写入当前时间
const epoch = Math.floor(Date.now() / 1000);
const buf = new ArrayBuffer(4);
new DataView(buf).setUint32(0, epoch, true);
await char.writeValue(buf);

2.4 BLE 标准 CTS 0x1805

Current Time — 0x2A2B R/W/Notify 10字节

偏移字段类型说明
0-1Yearuint16 LE年份
2Monthuint81-12
3Dayuint81-31
4Hoursuint80-23
5Minutesuint80-59
6Secondsuint80-59
7DayOfWeekuint81=周一...7=周日
8Fractions256uint81/256 秒
9AdjustReasonuint8调整原因

写入至少 7 字节可设时间,字节 7-9 可选。

Local Time Info — 0x2A0F Read 2字节

偏移字段说明
0TimeZoneint8, 单位15分钟(UTC+8=32)
1DSTOffset0=标准, 4=+1h, 255=未知
CTS 和自定义时间服务双向同步:写入一方后另一方自动更新。标准客户端用 CTS,自定义 App 用 epoch 更简洁。

2.5 OTA 更新服务 0000ff30

BLE OTA 服务用于远程固件更新的交互控制。设备定期自动检查更新,发现新版本后通知客户端,由用户确认后下载升级。

OTA Control — 0000ff31 Write/Notify 1字节

写入命令:

命令说明
0x01检查更新手动触发服务器检查
0x02确认升级开始下载固件
0x03取消取消当前操作

Notify:状态变化时推送 1 字节状态码。

OTA 状态码

名称说明
0x00IDLE空闲
0x01CHECKING正在检查更新
0x02AVAILABLE有新版本可用
0x03NO_UPDATE已是最新版本
0x04DOWNLOADING正在下载固件
0x05VERIFYING正在校验
0x06READY准备重启
0x07REBOOTING即将重启
0xE0-0xE5ERROR各类错误

OTA Info — 0000ff32 Read/Notify JSON

返回 UTF-8 JSON 字符串,包含版本信息和更新详情:

// 有可用更新
{"cur":"1.0.0","new":"1.2.0","size":483200,"log":"修复BLE断连","force":false,"state":2}

// 无更新
{"cur":"1.0.0","new":"","state":0}

OTA Progress — 0000ff33 Read/Notify 2字节

字节说明范围
[0]下载进度百分比0-100
[1]当前状态码见状态码表

完整交互序列

客户端 ESP32 设备 │ │ │── Write Ctrl: 0x01 ───────────────────>│ 检查更新 │<── Notify Ctrl: 0x01 ────────────────│ (CHECKING) │<── Notify Ctrl: 0x02 ────────────────│ (AVAILABLE) │── Read Info ──────────────────────────>│ │<── {cur, new, size, log} ────────────│ │── Write Ctrl: 0x02 ───────────────────>│ 确认升级 │<── Notify Progress: [1%, 0x04] ──────│ │<── Notify Progress: [50%, 0x04] ─────│ │<── Notify Progress: [100%, 0x04] ────│ │<── Notify Ctrl: 0x05 ────────────────│ (VERIFYING) │<── Notify Ctrl: 0x07 ────────────────│ (REBOOTING) │ ... BLE 断开,设备重启 ... │
// JavaScript OTA 交互示例
const service = await server.getPrimaryService('0000ff30-0000-1000-8000-00805f9b34fb');
const charCtrl = await service.getCharacteristic('0000ff31-0000-1000-8000-00805f9b34fb');
const charInfo = await service.getCharacteristic('0000ff32-0000-1000-8000-00805f9b34fb');
const charProg = await service.getCharacteristic('0000ff33-0000-1000-8000-00805f9b34fb');

// 订阅通知
await charCtrl.startNotifications();
charCtrl.addEventListener('characteristicvaluechanged', e => {
  const state = new Uint8Array(e.target.value.buffer)[0];
  console.log('OTA state:', state);
});

await charProg.startNotifications();
charProg.addEventListener('characteristicvaluechanged', e => {
  const data = new Uint8Array(e.target.value.buffer);
  console.log(`Progress: ${data[0]}%, state: 0x${data[1].toString(16)}`);
});

// 检查更新
await charCtrl.writeValueWithResponse(new Uint8Array([0x01]));

// 读取更新信息
const infoData = await charInfo.readValue();
const info = JSON.parse(new TextDecoder().decode(infoData.buffer));

// 确认升级
if (info.new) {
  await charCtrl.writeValueWithResponse(new Uint8Array([0x02]));
}
自动检查:设备启动 60 秒后首次检查,之后每 24 小时自动检查一次。检查需要 WiFi 连接。
回滚保护:新固件启动后会确认分区有效,若崩溃则自动回滚到旧版本。
断电安全:更新信息持久化到 NVS,设备重启后不丢失。

3. 安全配网模式 (WiFiProv Security1)

3.1 BLE 连接与端点

配网模式使用 ESP-IDF protocomm 协议,与正常模式的 GATT 服务完全不同。

参数
Service UUID (128-bit)021a9004-0382-4aea-bff4-6b3f1c5adfb4
安全等级Security1(X25519 + AES-256-CTR)

端点特征值(16-bit UUID)

端点UUID用途
prov-ctrl0xFF4F控制端点
prov-scan0xFF50WiFi 扫描
prov-session0xFF51Security1 安全握手
prov-config0xFF52WiFi 凭据配置
proto-ver0xFF53协议版本(JSON)
Web Bluetooth 连接时必须在 optionalServices 中声明 128-bit Service UUID,特征值通过 16-bit UUID 访问。
// Web Bluetooth 连接示例
const device = await navigator.bluetooth.requestDevice({
  filters: [{ namePrefix: 'PROV_' }],
  optionalServices: ['021a9004-0382-4aea-bff4-6b3f1c5adfb4']
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService('021a9004-0382-4aea-bff4-6b3f1c5adfb4');
const charSession = await service.getCharacteristic(0xFF51);
const charConfig  = await service.getCharacteristic(0xFF52);
const charScan    = await service.getCharacteristic(0xFF50);

通信模式

每次请求为 write → 等待 → read:

await characteristic.writeValueWithResponse(requestData);
await sleep(200);    // 等待设备处理
const response = await characteristic.readValue();

握手阶段明文通信;会话建立后 prov-config / prov-scan 数据自动 AES-256-CTR 加解密。

3.2 PoP 验证码

每台设备 PoP 基于 eFuse MAC 地址派生,保证唯一:

pop = SHA-256("OAKIO_POP_" + eFuse_MAC[6字节]).hex()[0:8]
// 例如 MAC = AA:BB:CC:DD:EE:FF → pop = "a3f1b20c"

即取 SHA-256 前 4 字节转为 8 位十六进制字符串。印刷在设备标签或串口输出。

自定义 PoP(固件端)

// ESP32_LED_TEST.ino 中修改 makeDevicePoP()
static String makeDevicePoP() {
  return "12345678";  // 固定 8 位
}

客户端计算 PoP(JavaScript)

async function computePoP(macBytes) {
  // macBytes: Uint8Array(6) — 设备 eFuse MAC
  const input = new Uint8Array(16);
  const prefix = new TextEncoder().encode("OAKIO_POP_");
  input.set(prefix, 0);
  input.set(macBytes, 10);
  const hash = await crypto.subtle.digest('SHA-256', input);
  return Array.from(new Uint8Array(hash).slice(0, 4))
    .map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
}

3.3 Security1 握手实现

握手通过 prov-session (0xFF51) 端点完成,共 2 轮请求:

客户端 ESP32 设备 │ │ │── Command0 (client_pubkey 32B) ────────>│ │<── Response0 ──────────────────────────│ │ device_pubkey (32B) + device_random (16B, 用作 AES IV) │ │ │ [双方各自计算] │ │ shared = X25519(my_private, peer_public)│ │ popHash = SHA-256(pop_string) │ │ sessionKey = shared XOR popHash │ │ │ │── Command1 (client_verify) ────────────>│ │<── Response1 (device_verify) ──────────│ │ │ │ ══ 安全会话建立 ══ │

Step 1: 发送 Command0

// 生成 X25519 密钥对
const keyPair = await crypto.subtle.generateKey(
  { name: 'X25519' }, true, ['deriveBits']
);
// 导出公钥 (32 字节 raw)
const clientPubKey = new Uint8Array(
  await crypto.subtle.exportKey('raw', keyPair.publicKey)
);
// 构建 protobuf: SessionData { sec_ver=1, sec1: Sec1Payload { msg=0, sc0: { client_pubkey } } }
const request = buildSessionCmd0(clientPubKey);
// 发送
await charSession.writeValueWithResponse(request);
await sleep(200);
const resp = await charSession.readValue();

Step 2: 解析 Response0

// 解析 protobuf → SessionResp0 { status, device_pubkey(32B), device_random(16B) }
const { devicePubKey, deviceRandom } = parseSessionResp0(resp);
// deviceRandom 即为 AES-CTR 的 IV

Step 3: 计算会话密钥

// X25519 密钥协商
const sharedBits = await crypto.subtle.deriveBits(
  { name: 'X25519', public: await importDevicePubKey(devicePubKey) },
  keyPair.privateKey, 256
);
const shared = new Uint8Array(sharedBits);      // 32 字节

// SHA-256(PoP 字符串)
const popHash = new Uint8Array(
  await crypto.subtle.digest('SHA-256', new TextEncoder().encode(popString))
);

// 会话密钥 = shared XOR popHash
const sessionKey = new Uint8Array(32);
for (let i = 0; i < 32; i++) sessionKey[i] = shared[i] ^ popHash[i];
如果 PoP 错误,sessionKey 计算结果不同,Step 4 验证必然失败。

Step 4: 双向验证 (Command1 / Response1)

// 客户端加密 device_pubkey 作为验证数据
// 使用 AES-256-CTR(sessionKey, IV=deviceRandom)
const clientVerify = await aesCTR_encrypt(sessionKey, deviceRandom, devicePubKey);
// ↑ 消耗计数器块 0-1 (32 字节)

// 构建 Command1 并发送
await charSession.writeValueWithResponse(buildSessionCmd1(clientVerify));
await sleep(200);
const resp1 = await charSession.readValue();
const { deviceVerify } = parseSessionResp1(resp1);

// 验证: 设备加密了 client_pubkey,需要解密比对
// 关键: ESP-IDF 在同一个 AES-CTR 上下文中:
//   先解密 client_verify (32B, 块 0-1)
//   再加密 device_verify (32B, 块 2-3)
// 客户端必须将 devicePubKey || deviceVerify 拼为 64B 一次性解密
const combined = concat(devicePubKey, new Uint8Array(deviceVerify.buffer));
const decrypted = await aesCTR_decrypt(sessionKey, deviceRandom, combined);
const decryptedVerify = decrypted.slice(32);  // 后 32B = 解密后的 device_verify
// 比对: decryptedVerify === clientPubKey
if (!arraysEqual(decryptedVerify, clientPubKey)) throw new Error('Verify failed');
握手共消耗 64 字节(4 个 AES 块),后续会话通信的计数器从字节偏移 64 开始。

3.4 AES-CTR 会话加密

握手完成后,所有 prov-config / prov-scan 数据需加解密。ESP-IDF 使用 mbedtls_aes_crypt_ctr 维护字节级计数器,Web Crypto API 每次从整块起始,需手动对齐。

核心实现

let ctrByteOffset = 64; // 握手消耗 64 字节

async function sessionCrypt(data) {
  const byteOff  = ctrByteOffset % 16;     // 块内已消耗字节
  const blockNum = Math.floor(ctrByteOffset / 16);
  const counter  = incrementIV(sessionIV, blockNum);

  let result;
  if (byteOff > 0) {
    // 非 16 字节对齐: 前面填零占位
    const padded = new Uint8Array(byteOff + data.length);
    padded.set(data, byteOff);
    const encrypted = await aesCTR(sessionKey, counter, padded);
    result = encrypted.slice(byteOff);
  } else {
    result = await aesCTR(sessionKey, counter, data);
  }
  ctrByteOffset += data.length;
  return result;
}

IV 递增函数

function incrementIV(iv, blocks) {
  const counter = new Uint8Array(iv);  // 复制 16 字节 IV
  // 大端序递增最后 4 字节
  let carry = blocks;
  for (let i = 15; i >= 12 && carry > 0; i--) {
    const sum = counter[i] + carry;
    counter[i] = sum & 0xFF;
    carry = sum >> 8;
  }
  return counter;
}

加密/解密通信示例

// 发送加密请求
async function sendEncrypted(characteristic, plainData) {
  const encrypted = await sessionCrypt(new Uint8Array(plainData));
  await characteristic.writeValueWithResponse(encrypted);
  await sleep(200);
  const respView = await characteristic.readValue();
  const respEncrypted = new Uint8Array(respView.buffer);
  const respPlain = await sessionCrypt(respEncrypted);
  return respPlain;
}

3.5 WiFi 凭据配置

安全会话建立后,通过 prov-config (0xFF52) 端点发送 WiFi 凭据:

1. CmdSetConfig (SSID + Password) → 加密发送 2. CmdApplyConfig → 设备尝试连接 WiFi 3. CmdGetStatus (轮询) → 检查连接结果 4. 连接成功 → 设备自动重启进入正常模式

发送凭据

// 构建 CmdSetConfig protobuf
// NetworkConfigPayload { msg_type=2, cmd_set_config: { ssid, passphrase } }
const cmd = buildCmdSetConfig(ssidString, passwordString);
await sendEncrypted(charConfig, cmd);

// 应用配置
// NetworkConfigPayload { msg_type=4, cmd_apply_config: {} }
const apply = buildCmdApplyConfig();
await sendEncrypted(charConfig, apply);

轮询状态

// NetworkConfigPayload { msg_type=0, cmd_get_status: {} }
async function pollStatus() {
  for (let i = 0; i < 15; i++) {
    await sleep(2000);
    const resp = await sendEncrypted(charConfig, buildCmdGetStatus());
    const status = parseRespGetStatus(resp);
    // staState: 0=Connected, 1=Connecting, 2=Disconnected
    if (status.staState === 0) return true;   // 成功
    if (status.staState === 2) return false;  // 失败
  }
  return false; // 超时
}

3.6 WiFi 扫描

安全会话建立后,可通过 prov-scan (0xFF50) 端点扫描附近 WiFi:

扫描流程

// 1. 触发扫描
const scanStart = buildCmdScanStart();  // blocking=true
await sendEncrypted(charScan, scanStart);

// 2. 等待完成
let count = 0;
while (true) {
  await sleep(1000);
  const resp = await sendEncrypted(charScan, buildCmdScanStatus());
  const status = parseRespScanStatus(resp);
  if (status.finished) { count = status.count; break; }
}

// 3. 分批获取结果 (每次 ≤4 条,避免 BLE MTU 限制)
const results = [];
for (let i = 0; i < count; i += 4) {
  const batch = Math.min(4, count - i);
  const resp = await sendEncrypted(charScan, buildCmdScanResult(i, batch));
  results.push(...parseRespScanResult(resp));
}

WiFi 扫描结果字段

字段Protobuf #类型说明
ssid1bytes网络名称
channel2varint信道号
rssi3int32信号强度 (dBm)
bssid4bytesMAC 地址
auth5varint认证模式

WifiAuthMode: 0=Open, 1=WEP, 2=WPA_PSK, 3=WPA2_PSK, 4=WPA_WPA2_PSK, 5=WPA2_Enterprise, 6=WPA3_PSK, 7=WPA2_WPA3_PSK

3.7 Protobuf 字段号完整参考

本项目使用手工 Protobuf 编解码(无依赖库),以下为所有消息的字段号。

通用编解码

// Protobuf varint 编码
function pbVarint(val) {
  const bytes = [];
  while (val > 0x7f) { bytes.push((val & 0x7f) | 0x80); val >>>= 7; }
  bytes.push(val & 0x7f);
  return new Uint8Array(bytes);
}

// 编码字段: tag = (fieldNum << 3) | wireType
// wireType 0 = varint, 2 = length-delimited (bytes/string/子消息)
function pbField(fieldNum, wireType, data) {
  const tag = pbVarint((fieldNum << 3) | wireType);
  if (wireType === 0) return concat(tag, pbVarint(data));
  if (wireType === 2) {
    const d = data instanceof Uint8Array ? data : new TextEncoder().encode(data);
    return concat(tag, pbVarint(d.length), d);
  }
  return tag;
}

SessionData / Sec1Payload (prov-session)

消息字段#Wire说明
SessionDatasec_ver2varint固定 = 1
SessionDatasec111bytesSec1Payload 子消息
Sec1Payloadmsg1varintSec1MsgType
Sec1Payloadsc020bytesSessionCmd0
Sec1Payloadsr021bytesSessionResp0
Sec1Payloadsc122bytesSessionCmd1
Sec1Payloadsr123bytesSessionResp1
Sec1MsgType
Session_Command00
Session_Response01
Session_Command12
Session_Response13

SessionCmd/Resp 内部字段

消息字段#类型
SessionCmd0client_pubkey1bytes
SessionResp0status1varint
SessionResp0device_pubkey2bytes
SessionResp0device_random3bytes
SessionCmd1client_verify_data2bytes
SessionResp1status1varint
SessionResp1device_verify_data3bytes

NetworkConfigPayload (prov-config)

字段#类型说明
msg_type1varintNetworkConfigMsgType
cmd_get_status10bytes空消息
resp_get_status11bytesRespGetStatus
cmd_set_config12bytesCmdSetConfig
resp_set_config13bytesRespSetConfig
cmd_apply_config14bytes空消息
resp_apply_config15bytesRespApplyConfig
NetworkConfigMsgType
TypeCmdGetStatus0
TypeRespGetStatus1
TypeCmdSetConfig2
TypeRespSetConfig3
TypeCmdApplyConfig4
TypeRespApplyConfig5

CmdSetConfig 内部: ssid(1, bytes), passphrase(2, bytes)

RespGetStatus 内部: status(1, varint), sta_state(2, varint: 0=Connected 1=Connecting 2=Disconnected), fail_reason(10, varint)

WiFiScanPayload (prov-scan)

字段#类型说明
msg1varintWiFiScanMsgType
status2varint状态码
cmd_scan_start10bytesCmdScanStart
resp_scan_start11bytesRespScanStart
cmd_scan_status12bytes空消息
resp_scan_status13bytesRespScanStatus
cmd_scan_result14bytesCmdScanResult
resp_scan_result15bytesRespScanResult

CmdScanStart: blocking(1, bool) | CmdScanResult: start_index(1), count(2)

RespScanStatus: scan_finished(1, bool), result_count(2, varint)

4. 常见问题

PoP 验证失败

握手 Step 4 解密比对不通过。检查 PoP 输入是否与设备标签一致(大小写敏感)。

WiFi 连接失败

GetStatus 返回 staState=2 (Disconnected)。fail_reason 对应 ESP-IDF wifi_err_reason_t:201=AUTH_FAIL(密码错误),15=4WAY_HANDSHAKE_TIMEOUT。

BLE 通信超时

write 后 read 返回空数据:增加 sleep 到 300-500ms。部分设备 ATT MTU 协商慢,可重连解决。

AES-CTR 计数器失步

加解密结果乱码:确保 ctrByteOffset 严格按 data.length 递增,每次加密和解密都推进。握手后初始值为 64。

WiFi 扫描结果截断

BLE MTU 限制:每次 CmdScanResult 的 count 建议 ≤4。

配网完成后设备无响应

设备 WiFi 连接成功后会自动重启,BLE 断开是预期行为。重启后进入正常模式,广播名称变为 OAKIOT_XXXXXX

OAKIOT ESP32 BLE 开发文档 · 基于 ESP-IDF WiFiProv / protocomm 协议

5. HTTP 局域网 API

设备支持通过 HTTP RESTful API 在局域网内进行配置和控制,默认关闭,需通过串口或配置命令主动开启。

HTTP 服务用于与 Home Assistant 等智能家居系统集成,仅限局域网内使用。

启用方式

// 串口命令
{"cmd":"set_http","enabled":true,"port":80}

// 或通过 set_config
{"cmd":"set_config","http_enabled":true}

端点列表

方法路径说明
GET/api/status设备综合状态
GET/api/config运行时配置
PUT/api/config修改配置
GET/api/ledLED 灯光状态
PUT/api/led设置灯光
GET/api/effect灯效模式与参数
PUT/api/effect设置灯效
GET/api/timefx时间灯效配置
PUT/api/timefx设置时间灯效
GET/api/wifiWiFi 连接状态
GET/api/otaOTA 状态
POST/api/ota/check检查更新
POST/api/ota/update确认升级
POST/api/ota/cancel取消更新

集成示例

// 查询设备状态
curl http://192.168.1.100/api/status

// 开灯设为暖白
curl -X PUT http://192.168.1.100/api/led \
  -H "Content-Type: application/json" \
  -d '{"power":true,"h":30,"s":180,"v":255}'

// 设置呼吸灯效
curl -X PUT http://192.168.1.100/api/effect \
  -H "Content-Type: application/json" \
  -d '{"effect_mode":100,"effect_speed":80}'

// 修改NTP配置
curl -X PUT http://192.168.1.100/api/config \
  -H "Content-Type: application/json" \
  -d '{"ntp_server1":"pool.ntp.org","ntp_gmt_offset":32400}'

6. 串口配置命令扩展

通过 USB 串口(115200, 8N1)发送 JSON 命令,可配置所有运行时参数。

运行时配置命令

命令说明
get_config获取全部运行时配置(NTP、OTA URL/间隔、HTTP开关等)
set_config修改运行时配置(仅传要改的字段)
get_status设备综合状态(MAC、版本、WiFi、时间、堆内存等)
get_ledLED 灯光状态
set_effect设置灯效模式/参数
get_wifiWiFi 连接状态
set_wifi设置 WiFi 凭据
get_timefx时间灯效配置
set_timefx修改时间灯效参数
set_http开启/关闭 HTTP 服务

可配置参数

字段类型默认值说明
ntp_server1stringntp.aliyun.comNTP 服务器 1
ntp_server2stringtime.nist.govNTP 服务器 2
ntp_gmt_offsetnumber28800UTC 偏移 (秒)
ntp_daylight_offsetnumber0夏令时偏移 (秒)
device_prefixstringOAKIOTBLE 广播名前缀
save_check_intervalnumber300000NVS 保存间隔 (ms)
ota_base_urlstringhttps://ota.iot.oakiot.ccOTA 服务器地址
ota_check_intervalnumber86400000OTA 检查间隔 (ms)
ota_first_delaynumber60000OTA 首次延迟 (ms)
http_enabledbooleanfalseHTTP 服务开关
http_portnumber80HTTP 服务端口
// 获取运行时配置
{"cmd":"get_config"}

// 修改NTP和OTA配置
{"cmd":"set_config","ntp_server1":"pool.ntp.org","ota_check_interval":43200000}

// 开启HTTP服务
{"cmd":"set_http","enabled":true,"port":80}

// 查看综合状态
{"cmd":"get_status"}

7. 配置渠道对比

配置项BLE串口HTTP
LED HSV/开关0xff01/02set_ledPUT /api/led
灯效模式/参数0xff03/04set_effectPUT /api/effect
时间灯效0xff05set_timefxPUT /api/timefx
WiFi 凭据0xff11set_wifi
WiFi 状态0xff12get_wifiGET /api/wifi
时间同步0xff21 / 0x2A2B
NTP/时区配置set_configPUT /api/config
设备前缀set_configPUT /api/config
OTA URL/间隔set_configPUT /api/config
OTA 检查/升级0xff31ota_check等POST /api/ota/*
HTTP 服务开关set_httpPUT /api/config
设备信息OTA Infoget_device_infoGET /api/status