设备管理系统

📖 设备接入指南

选择你的接入方式

场景推荐方式说明
批量烧录固件,设备自动注册批量注册所有设备用同一个鉴权码,无需逐一预注册
高安全要求,每设备独立密钥手动注册先在控制台创建设备拿 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_idsecret_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-TimestampUnix 秒级时间戳,误差±120秒
X-SignatureHMAC-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?

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 '{}'