📖 设备接入指南
选择你的接入方式
| 场景 | 推荐方式 | 说明 |
|---|---|---|
| 批量烧录固件,设备自动注册 | 批量注册 | 所有设备用同一个鉴权码,无需逐一预注册 |
| 高安全要求,每设备独立密钥 | 手动注册 | 先在控制台创建设备拿 secret_key,再烧录 |
🔑 批量注册(鉴权码方式)
原理
设备发送自己的 device_id(可以是 MAC 地址自动生成)+ 固定的 鉴权码。服务器验证鉴权码有效后,自动注册设备并返回状态。
| 步骤 | 操作 |
|---|---|
| 1 | 控制台 → 鉴权码管理 → 生成鉴权码(记下这个字符串) |
| 2 | 把鉴权码写进设备固件 |
| 3 | 设备上电后调一次 POST /api/device/auth |
| 4 | 自动注册成功,控制台可见 |
请求格式:
POST /api/device/auth
Content-Type: application/json
{
"device_id": "ESP32-A4CF12D8E03B",
"identifier": "你的鉴权码"
}
成功响应:
{"code":0, "allowed":true, "status":"idle", "secret_key":"自动生成"}
ESP32 例程
#include <WiFi.h>
#include <HTTPClient.h>
// ========== 你的配置 ==========
const char* WIFI_SSID = "你的WiFi名";
const char* WIFI_PASS = "你的WiFi密码";
const char* IDENTIFIER = "你的鉴权码"; // 控制台生成
const char* SERVER_HOST = "devicemanager.2790cake.cn";
const int SERVER_PORT = 80;
// =============================
// 基于MAC地址自动生成设备ID
String getDeviceID() {
uint64_t mac = ESP.getEfuseMac();
char id[17];
sprintf(id, "%04X%08X", (uint16_t)(mac >> 32), (uint32_t)mac);
return "ESP32-" + String(id); // "ESP32-A4CF12D8E03B"
}
bool deviceGoOnline() {
HTTPClient http;
String url = String("http://") + SERVER_HOST + ":" + SERVER_PORT + "/api/device/auth";
http.begin(url);
http.addHeader("Content-Type", "application/json");
String body = "{\"device_id\":\"" + getDeviceID() +
"\",\"identifier\":\"" + IDENTIFIER + "\"}";
int code = http.POST(body);
String resp = http.getString();
http.end();
Serial.printf("[上线] HTTP %d → %s\n", code, resp.c_str());
return resp.indexOf("\"allowed\":true") > 0;
}
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println("\nWiFi OK");
if (deviceGoOnline()) {
Serial.println("✅ 设备已上线");
} else {
Serial.println("❌ 上线失败,检查鉴权码");
}
}
void loop() {
delay(60000);
deviceGoOnline(); // 每分钟上报一次,保持在线
}
💡 ESP32 可以用 Arduino IDE 直接编译。不需要额外安装任何库。
STM32 + ESP8266 例程
STM32 通过串口 AT 指令控制 ESP-01S 联网。核心步骤:连 WiFi → 发 HTTP POST。
// ========= 你的配置 =========
#define WIFI_SSID "你的WiFi名"
#define WIFI_PASS "你的WiFi密码"
#define IDENTIFIER "你的鉴权码" // 控制台生成
#define SERVER_HOST "devicemanager.2790cake.cn"
#define SERVER_PORT "80"
// ============================
// --- 通过USART发送AT指令 ---
// 假设 USART2 连接 ESP8266,已有 uart_send/uarts_recv 基础函数
// 获取设备ID(基于MCU唯一ID)
void get_device_id(char *buf) {
uint32_t uid[3];
uid[0] = HAL_GetUIDw0(); uid[1] = HAL_GetUIDw1(); uid[2] = HAL_GetUIDw2();
sprintf(buf, "STM32-%08lX%08lX", uid[0], uid[2]);
}
// ESP8266 AT 上线
int device_go_online(void) {
char did[40]; get_device_id(did);
// 1. 连接 WiFi
esp_send_cmd("AT+CWJAP=\"" WIFI_SSID "\",\"" WIFI_PASS "\"");
if (!esp_wait_ok(15000)) return -1;
// 2. 构建 JSON body(注意引号转义)
char body[256];
snprintf(body, sizeof(body),
"{\"device_id\":\"%s\",\"identifier\":\"%s\"}", did, IDENTIFIER);
// 3. 建立 TCP 连接
esp_send_cmd("AT+CIPSTART=\"TCP\",\"" SERVER_HOST "\"," SERVER_PORT);
if (!esp_wait_ok(5000)) return -2;
// 4. 构建 HTTP 请求
char req[512];
int len = snprintf(req, sizeof(req),
"POST /api/device/auth HTTP/1.1\r\n"
"Host: " SERVER_HOST ":" SERVER_PORT "\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %d\r\n"
"\r\n"
"%s",
(int)strlen(body), body);
// 5. 发送
char cmd[32]; snprintf(cmd, sizeof(cmd), "AT+CIPSEND=%d", len);
esp_send_cmd(cmd);
if (!esp_wait_for(">", 3000)) return -3;
esp_send_raw(req, len);
// 6. 收响应 (等待 +IPD,...)
char resp[512];
int n = esp_read_response(resp, sizeof(resp), 5000);
esp_send_cmd("AT+CIPCLOSE");
return (strstr(resp, "\"allowed\":true") != NULL) ? 0 : -4;
}
⚠️ ESP8266 的 AT 固件版本需要支持
AT+CIPSTART TCP 连接。推荐使用安信可官方 AT 固件 v2.2.0+。Arduino 例程
适用于 Arduino Uno/Mega + Ethernet Shield (W5100/W5500)。
#include <SPI.h>
#include <Ethernet.h>
// ========== 配置 ==========
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
const char* IDENTIFIER = "你的鉴权码";
const char* SERVER_HOST_B = "devicemanager.2790cake.cn";
// =========================
EthernetClient client;
String getDeviceID() {
char id[13];
// 使用 MAC 地址后6位作为设备ID
sprintf(id, "UNO-%02X%02X%02X", mac[3], mac[4], mac[5]);
return String(id);
}
bool deviceGoOnline() {
if (!client.connect("devicemanager.2790cake.cn", 80)) return false;
String did = getDeviceID();
String body = "{\"device_id\":\"" + did +
"\",\"identifier\":\"" + IDENTIFIER + "\"}";
client.println("POST /api/device/auth HTTP/1.1");
client.println("Host: devicemanager.2790cake.cn");
client.println("Content-Type: application/json");
client.print("Content-Length: "); client.println(body.length());
client.println();
client.print(body);
// 跳过HTTP头,读body
while (client.connected()) {
if (client.find("\r\n\r\n")) {
String resp = client.readString();
client.stop();
bool ok = resp.indexOf("\"allowed\":true") > 0;
Serial.print(ok ? "✅ 上线" : "❌ 失败");
Serial.println(" " + resp);
return ok;
}
}
client.stop(); return false;
}
void setup() {
Serial.begin(9600);
Ethernet.begin(mac);
delay(1000);
deviceGoOnline();
}
void loop() {
delay(60000);
deviceGoOnline();
}
Python 例程
import requests
import uuid
IDENTIFIER = "你的鉴权码"
SERVER = "http://devicemanager.2790cake.cn"
DEVICE_ID = "PY-" + uuid.getnode().to_bytes(6,"big").hex().upper()
resp = requests.post(f"{SERVER}/api/device/auth", json={
"device_id": DEVICE_ID,
"identifier": IDENTIFIER,
}).json()
if resp.get("allowed"):
print(f"✅ 上线成功, status={resp['status']}")
print(f" secret_key={resp.get('secret_key','')}")
else:
print(f"❌ 失败: {resp}")
🔐 手动注册(HMAC 签名方式)
原理
先在控制台逐台创建设备拿到 secret_key,固件中用 HMAC-SHA256 对请求签名。服务器验证签名通过后才响应。
| 步骤 | 操作 |
|---|---|
| 1 | 控制台 → 我的设备 → 添加设备,拿到 device_id 和 secret_key |
| 2 | 把 device_id 和 secret_key 写进设备固件 |
| 3 | 每次请求带三个 Header:X-Device-ID, X-Timestamp, X-Signature |
签名公式:
signature = hex( HMAC-SHA256(key=secret_key, msg=device_id + timestamp + body) )
三个必需的 Header:
| Header | 值 |
|---|---|
X-Device-ID | 设备ID(如 DEV-A1B2C3D4) |
X-Timestamp | Unix 秒级时间戳,误差±120秒 |
X-Signature | HMAC-SHA256 签名 hex |
ESP32 例程
ESP32 内置 mbedtls,直接用 Arduino 框架。
#include <WiFi.h>
#include <HTTPClient.h>
#include <mbedtls/md.h> // ESP32 内置,无需安装
// ========== 你的配置 ==========
const char* WIFI_SSID = "你的WiFi名";
const char* WIFI_PASS = "你的WiFi密码";
const char* DEVICE_ID = "DEV-A1B2C3D4"; // 控制台创建时填的ID
const char* SECRET_KEY = "你的32位hex密钥"; // 控制台返回的 secret_key
const char* SERVER_HOST = "devicemanager.2790cake.cn";
const int SERVER_PORT = 80;
// =============================
// HMAC-SHA256 签名(返回 hex 字符串)
String hmacSign(String deviceId, String timestamp, String body) {
String msg = deviceId + timestamp + body;
uint8_t hmac[32];
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1);
mbedtls_md_hmac_starts(&ctx, (const uint8_t*)SECRET_KEY, strlen(SECRET_KEY));
mbedtls_md_hmac_update(&ctx, (const uint8_t*)msg.c_str(), msg.length());
mbedtls_md_hmac_finish(&ctx, hmac);
mbedtls_md_free(&ctx);
char hex[65];
for (int i = 0; i < 32; i++) sprintf(hex + i * 2, "%02x", hmac[i]);
return String(hex);
}
int signedPost(String endpoint, String body) {
HTTPClient http;
String url = String("http://") + SERVER_HOST + ":" + SERVER_PORT + endpoint;
http.begin(url);
String ts = String(time(nullptr));
String sig = hmacSign(DEVICE_ID, ts, body);
http.addHeader("Content-Type", "application/json");
http.addHeader("X-Device-ID", DEVICE_ID);
http.addHeader("X-Timestamp", ts);
http.addHeader("X-Signature", sig);
int code = http.POST(body);
String resp = http.getString();
http.end();
Serial.printf("[%s] HTTP %d → %s\n", endpoint.c_str(), code, resp.c_str());
return code;
}
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println("\nWiFi OK");
// 同步NTP时间(关键!签名需要准确时间戳)
configTime(8 * 3600, 0, "ntp.aliyun.com", "pool.ntp.org");
struct tm ti;
if (getLocalTime(&ti, 5000)) {
Serial.printf("NTP时间: %04d-%02d-%02d %02d:%02d:%02d\n",
ti.tm_year+1900, ti.tm_mon+1, ti.tm_mday, ti.tm_hour, ti.tm_min, ti.tm_sec);
}
// 上线鉴权
if (signedPost("/api/device/auth", "{}") == 200) {
Serial.println("✅ 鉴权通过");
}
}
unsigned long lastBeat = 0;
void loop() {
if (millis() - lastBeat > 60000) {
lastBeat = millis();
signedPost("/api/v1/heartbeat", "{}");
}
delay(1000);
}
STM32 + ESP8266 例程
STM32 使用 HAL 库自带的 mbedtls。
#include "mbedtls/md.h"
// ========== 你的配置 ==========
#define DEVICE_ID "DEV-A1B2C3D4"
#define SECRET_KEY "你的32位hex密钥"
#define SERVER_HOST "devicemanager.2790cake.cn"
#define SERVER_PORT 80
// =============================
// HMAC-SHA256, 输出 64 字符 hex
void hmac_sha256_hex(const char *key, const char *msg, char *hex_out) {
uint8_t hmac[32];
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1);
mbedtls_md_hmac_starts(&ctx, (const uint8_t*)key, strlen(key));
mbedtls_md_hmac_update(&ctx, (const uint8_t*)msg, strlen(msg));
mbedtls_md_hmac_finish(&ctx, hmac);
mbedtls_md_free(&ctx);
for (int i = 0; i < 32; i++)
sprintf(hex_out + i*2, "%02x", hmac[i]);
hex_out[64] = '\0';
}
// 获取 Unix 时间戳(需先从 NTP 同步)
// 简易实现:启动时手动设置基准时间 + HAL_GetTick 推算
extern uint32_t ntp_base_time; // NTP 获取的基准时间戳
uint32_t get_unix_time(void) {
return ntp_base_time + HAL_GetTick() / 1000;
}
// 带签名的 POST 请求(通过 ESP8266 AT 指令)
int signed_post(const char *endpoint) {
char ts_str[20]; sprintf(ts_str, "%lu", get_unix_time());
// 签名
char msg[128]; snprintf(msg, sizeof(msg), "%s%s{}", DEVICE_ID, ts_str);
char sig[65]; hmac_sha256_hex(SECRET_KEY, msg, sig);
// 构建 HTTP 请求
char req[1024];
int len = snprintf(req, sizeof(req),
"POST %s HTTP/1.1\r\n"
"Host: " SERVER_HOST ":%d\r\n"
"Content-Type: application/json\r\n"
"X-Device-ID: " DEVICE_ID "\r\n"
"X-Timestamp: %s\r\n"
"X-Signature: %s\r\n"
"Content-Length: 2\r\n"
"\r\n"
"{}",
endpoint, SERVER_PORT, ts_str, sig);
// ESP8266 AT 发送
char cmd[40];
snprintf(cmd, sizeof(cmd), "AT+CIPSTART=\"TCP\",\"" SERVER_HOST "\",%d", SERVER_PORT);
esp_send_cmd(cmd); esp_wait_ok(5000);
snprintf(cmd, sizeof(cmd), "AT+CIPSEND=%d", len);
esp_send_cmd(cmd); esp_wait_for(">", 3000);
esp_send_raw(req, len);
char resp[512]; esp_read_response(resp, sizeof(resp), 5000);
esp_send_cmd("AT+CIPCLOSE");
return (strstr(resp, "\"code\":0") != NULL) ? 0 : -1;
}
// 主流程
void device_main(void) {
wifi_connect(); // AT+CWJAP
ntp_sync(); // 获取 ntp_base_time
signed_post("/api/device/auth"); // 上线
while (1) {
HAL_Delay(60000);
signed_post("/api/v1/heartbeat"); // 心跳
}
}
Arduino 例程
Arduino Uno + Ethernet Shield,需安装 SHA256 库。
#include <SPI.h>
#include <Ethernet.h>
#include <SHA256.h> // 库: Arduino Cryptosuite by Cathedrow
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
EthernetClient client;
const char* DEVICE_ID = "DEV-A1B2C3D4";
const char* SECRET_KEY="你的32位...tring hmacHex(String key, String msg) {
uint8_t k[64]={0}, ipad[80], opad[80];
int kl = min((int)key.length(), 64);
memcpy(k, key.c_str(), kl);
for (int i=0; i<64; i++) { ipad[i]=k[i]^0x36; opad[i]=k[i]^0x5c; }
memcpy(ipad+64, msg.c_str(), msg.length());
SHA256 sha;
sha.update(ipad, 64+msg.length());
uint8_t *ih = sha.digest();
memcpy(opad+64, ih, 32);
sha.reset(); sha.update(opad, 96);
uint8_t *hmac = sha.digest();
char hx[65]; for(int i=0;i<32;i++) sprintf(hx+i*2,"%02x",hmac[i]);
return String(hx);
}
int signedPost(const char* ep) {
if (!client.connect("devicemanager.2790cake.cn", 80)) return -1;
String ts = String(millis()/1000);
String sig = hmacHex(SECRET_KEY, String(DEVICE_ID)+ts+"{}");
client.print("POST "); client.print(ep); client.println(" HTTP/1.1");
client.println("Host: devicemanager.2790cake.cn");
client.println("Content-Type: application/json");
client.print("X-Device-ID: "); client.println(DEVICE_ID);
client.print("X-Timestamp: "); client.println(ts);
client.print("X-Signature: "); client.println(sig);
client.println("Content-Length: 2\r\n{}");
while(client.connected()&&!client.available());
String r; while(client.available()) r+=(char)client.read();
client.stop();
return r.indexOf("\"code\":0")>0 ? 200 : 403;
}
void setup(){Serial.begin(9600);Ethernet.begin(mac);delay(1000);signedPost("/api/device/auth");}
void loop(){delay(60000);signedPost("/api/v1/heartbeat");}
Python 例程
import hmac, hashlib, time, requests
DEVICE_ID = "DEV-A1B2C3D4"
SECRET_KEY="你的32位...RVER = "http://devicemanager.2790cake.cn"
def signed_post(endpoint, body="{}"):
ts = str(int(time.time()))
msg = f"{DEVICE_ID}{ts}{body}"
sig = hmac.new(SECRET_KEY.encode(), msg.encode(), hashlib.sha256).hexdigest()
return requests.post(f"{SERVER}{endpoint}", data=body, headers={
"Content-Type": "application/json",
"X-Device-ID": DEVICE_ID,
"X-Timestamp": ts,
"X-Signature": sig,
}).json()
# 上线
r = signed_post("/api/device/auth")
print(f"状态: {r['status']}") # trial / idle / permanent
# 心跳循环
while True:
r = signed_post("/api/v1/heartbeat")
if r["status"] in ("expired", "locked"):
print("设备已锁定/过期"); break
print(f"♥ {r['status']}")
time.sleep(60)
API 参考
POST /api/device/auth
设备鉴权(两种方式都走这个端点。body 里有 identifier 用批量模式,否则用 HMAC 模式)。
批量模式(identifier)
→ {"device_id":"ESP32-xxx", "identifier":"鉴权码"}
← {"code":0, "allowed":true, "status":"idle", "secret_key":"..."}
HMAC 模式
→ Headers: X-Device-ID, X-Timestamp, X-Signature Body: {}
← {"code":0, "status":"trial", "trial_remaining_seconds":259200}
POST /api/v1/heartbeat
心跳上报(仅 HMAC 模式)。建议 30-60 秒一次。
→ 同 HMAC Headers + Body: {}
← {"code":0, "status":"trial", "trial_remaining_seconds":...}
POST /api/v1/activate
激活状态查询(仅 HMAC 模式)。
→ 同 HMAC Headers + Body: {}
← {"code":0, "status":"trial", "trial_remaining_seconds":...}
常见问题
Q: 返回 403?
- 批量模式:鉴权码是否正确 / 是否已过期 / 设备数是否已满
- HMAC 模式:时间戳误差超过 120 秒 → 检查 NTP 同步 / secret_key 是否正确 / 签名消息格式:
device_id + timestamp + body
Q: 设备列表里看不到新设备?
用鉴权码所属账号登录。鉴权码是谁生成的,设备就注册到谁的账号下。
Q: ESP32 时间不对?
// 东八区 NTP
configTime(8 * 3600, 0, "ntp.aliyun.com", "pool.ntp.org");
struct tm ti;
if (getLocalTime(&ti, 5000)) {
Serial.printf("时间: %04d-%02d-%02d %02d:%02d:%02d\n",
ti.tm_year+1900, ti.tm_mon+1, ti.tm_mday,
ti.tm_hour, ti.tm_min, ti.tm_sec);
}
Q: 命令行测试签名?
DEV="DEV-A1B2C3D4"; KEY="你的secret_key"; TS=$(date +%s)
SIG=$(echo -n "${DEV}${TS}{}"|openssl dgst -sha256 -hmac "$KEY"|awk '{print $NF}')
curl -X POST http://devicemanager.2790cake.cn/api/device/auth \
-H "X-Device-ID: $DEV" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" -d '{}'