Skip to content

Latest commit

 

History

History
1511 lines (1227 loc) · 39.3 KB

File metadata and controls

1511 lines (1227 loc) · 39.3 KB

小智语音助手 Linux 简化验证版本 - 技术文档 V1.2

项目概述

项目名称: 小智 (XiaoZhi) 语音助手 - 简化验证版本 项目类型: 网络通信验证 目标平台: Linux (桌面/嵌入式) 主要用途: 验证 OTA HTTP 请求和 WebSocket 连接流程


设计目标

本版本专注于验证核心网络通信流程,移除音频、GUI 等复杂模块:

功能模块 V1.1 V1.2 说明
OTA HTTP 请求 保留 - 核心验证功能
WebSocket 连接 保留 - 核心验证功能
TLS/SSL Mbed TLS OpenSSL 改用 OpenSSL
WebSocket 协议 自实现 自实现 保留
音频处理 移除
GUI (LVGL) 移除
MCP 工具 移除
状态机 简化 简化状态

简化架构

系统架构

┌─────────────────────────────────────────────────────────────┐
│                        应用层                                │
├─────────────────────────────────────────────────────────────┤
│           主程序  │  日志输出  │  简化状态机                 │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│                      协议层 (自实现)                         │
├─────────────────────────────────────────────────────────────┤
│            WebSocket 协议  │  握手  │  帧处理  │  消息编解码 │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│                      传输层                                  │
├─────────────────────────────────────────────────────────────┤
│               OpenSSL (TLS)         │    TCP Socket         │
└─────────────────────────────────────────────────────────────┘

### 核心依赖

| 库 | 版本 | 许可证 | 用途 |
|----|------|--------|------|
| OpenSSL | 1.1.1+ | Apache-2.0 | TLS 加密 |
| libcurl | 7.68+ | MIT | HTTP 请求 (OTA) |
| cJSON | 1.7.x+ | MIT | JSON 解析 |

### 编译工具链
  • GCC: 7.0+ (支持 C11)
  • CMake: 3.10+
  • pkg-config: 0.29+
---

## 目录结构 (极简版)

xiaozhi-linux/ ├── src/ │ ├── main.c # 主程序入口 │ ├── websocket.c # WebSocket 协议实现 │ ├── tls.c # OpenSSL TLS 封装 │ ├── http.c # HTTP 客户端 (用于 OTA) │ ├── ota.c # OTA 激活 │ ├── device.c # 设备信息获取 │ └── message.c # 消息处理 ├── include/ │ ├── websocket.h │ ├── tls.h │ ├── http.h │ ├── ota.h │ ├── device.h │ └── message.h ├── third_party/ │ └── cjson/ # cJSON 源码 ├── CMakeLists.txt └── STORY-V1.2.md # 本文档

---

## 依赖库安装

### Ubuntu/Debian

```bash
# OpenSSL 开发库
sudo apt-get install libssl-dev

# libcurl 开发库
sudo apt-get install libcurl4-openssl-dev

# 编译工具
sudo apt-get install build-essential cmake pkg-config

Fedora/RHEL

sudo dnf install openssl-devel libcurl-devel gcc cmake pkg-config

Arch Linux

sudo pacman -S openssl curl gcc cmake pkg-config

OpenSSL TLS 封装

TLS 连接结构

/* TLS 配置 */
typedef struct {
    const char *ca_file;           /* CA 证书文件 */
    const char *cert_file;         /* 客户端证书文件 (可选) */
    const char *key_file;          /* 客户端私钥文件 (可选) */
    bool verify_cert;             /* 是否验证证书 */
    int timeout_sec;              /* 连接超时 (秒) */
} tls_config_t;

/* TLS 连接 */
typedef struct {
    SSL *ssl;                     /* OpenSSL SSL 对象 */
    SSL_CTX *ctx;                 /* OpenSSL SSL 上下文 */
    int sock_fd;                  /* 底层 socket 文件描述符 */
    bool connected;               /* 连接状态 */
} tls_connection_t;

TLS 操作函数

/* 初始化 TLS 连接 */
int tls_init(tls_connection_t *conn, const tls_config_t *config);

/* TLS 连接到服务器 */
int tls_connect(tls_connection_t *conn, const char *host, uint16_t port);

/* 发送数据 */
ssize_t tls_send(tls_connection_t *conn, const void *data, size_t len);

/* 接收数据 */
ssize_t tls_recv(tls_connection_t *conn, void *buf, size_t len);

/* 关闭连接 */
void tls_close(tls_connection_t *conn);

/* 释放资源 */
void tls_cleanup(tls_connection_t *conn);

TLS 实现

#include <openssl/ssl.h>
#include <openssl/err.h>

int tls_init(tls_connection_t *conn, const tls_config_t *config) {
    /* 初始化 OpenSSL */
    SSL_library_init();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();

    /* 创建 SSL 上下文 */
    const SSL_METHOD *method = TLS_client_method();
    conn->ctx = SSL_CTX_new(method);
    if (!conn->ctx) {
        ERR_print_errors_fp(stderr);
        return -1;
    }

    /* 加载 CA 证书 */
    if (config->ca_file && config->verify_cert) {
        if (SSL_CTX_load_verify_locations(conn->ctx, config->ca_file, NULL) != 1) {
            ERR_print_errors_fp(stderr);
            SSL_CTX_free(conn->ctx);
            return -1;
        }
        SSL_CTX_set_verify(conn->ctx, SSL_VERIFY_PEER, NULL);
    } else {
        SSL_CTX_set_verify(conn->ctx, SSL_VERIFY_NONE, NULL);
    }

    /* 设置超时 */
    if (config->timeout_sec > 0) {
        SSL_CTX_set_timeout(conn->ctx, config->timeout_sec);
    }

    conn->ssl = NULL;
    conn->sock_fd = -1;
    conn->connected = false;

    return 0;
}

int tls_connect(tls_connection_t *conn, const char *host, uint16_t port) {
    struct sockaddr_in addr;
    char port_str[6];

    /* 创建 socket */
    conn->sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (conn->sock_fd < 0) {
        perror("socket");
        return -1;
    }

    /* 解析地址 */
    snprintf(port_str, sizeof(port_str), "%u", port);
    struct addrinfo hints = {
        .ai_family = AF_INET,
        .ai_socktype = SOCK_STREAM,
    };
    struct addrinfo *result;

    if (getaddrinfo(host, port_str, &hints, &result) != 0) {
        perror("getaddrinfo");
        close(conn->sock_fd);
        return -1;
    }

    /* 连接服务器 */
    if (connect(conn->sock_fd, result->ai_addr, result->ai_addrlen) != 0) {
        perror("connect");
        freeaddrinfo(result);
        close(conn->sock_fd);
        return -1;
    }
    freeaddrinfo(result);

    /* 创建 SSL 连接 */
    conn->ssl = SSL_new(conn->ctx);
    if (!conn->ssl) {
        ERR_print_errors_fp(stderr);
        close(conn->sock_fd);
        return -1;
    }

    SSL_set_fd(conn->ssl, conn->sock_fd);

    /* SSL 握手 */
    if (SSL_connect(conn->ssl) != 1) {
        ERR_print_errors_fp(stderr);
        SSL_free(conn->ssl);
        close(conn->sock_fd);
        return -1;
    }

    /* 验证证书 */
    if (SSL_get_verify_result(conn->ssl) != X509_V_OK) {
        fprintf(stderr, "Certificate verification failed\n");
        SSL_shutdown(conn->ssl);
        SSL_free(conn->ssl);
        close(conn->sock_fd);
        return -1;
    }

    conn->connected = true;
    return 0;
}

ssize_t tls_send(tls_connection_t *conn, const void *data, size_t len) {
    if (!conn->connected || !conn->ssl) {
        return -1;
    }
    return SSL_write(conn->ssl, data, len);
}

ssize_t tls_recv(tls_connection_t *conn, void *buf, size_t len) {
    if (!conn->connected || !conn->ssl) {
        return -1;
    }
    return SSL_read(conn->ssl, buf, len);
}

void tls_close(tls_connection_t *conn) {
    if (conn->ssl) {
        SSL_shutdown(conn->ssl);
    }
    if (conn->sock_fd >= 0) {
        close(conn->sock_fd);
    }
    conn->connected = false;
}

void tls_cleanup(tls_connection_t *conn) {
    if (conn->ssl) {
        SSL_free(conn->ssl);
        conn->ssl = NULL;
    }
    if (conn->ctx) {
        SSL_CTX_free(conn->ctx);
        conn->ctx = NULL;
    }
    conn->sock_fd = -1;
    conn->connected = false;
}

WebSocket 协议实现

WebSocket 帧结构

/* WebSocket 帧头 (2-14 bytes) */
typedef struct __attribute__((packed)) {
    unsigned int opcode: 4;      /* 4 bits: 帧类型 */
    unsigned int rsv3: 1;        /* 1 bit: 保留 */
    unsigned int rsv2: 1;        /* 1 bit: 保留 */
    unsigned int rsv1: 1;        /* 1 bit: 保留 */
    unsigned int fin: 1;         /* 1 bit: 是否最后一帧 */

    unsigned int payload_len: 7; /* 7 bits: 负载长度 */
    unsigned int mask: 1;        /* 1 bit: 是否使用掩码 */
} ws_frame_header_t;

/* 操作码定义 */
#define WS_OPCODE_CONTINUATION  0x0
#define WS_OPCODE_TEXT         0x1
#define WS_OPCODE_BINARY        0x2
#define WS_OPCODE_CLOSE         0x8
#define WS_OPCODE_PING          0x9
#define WS_OPCODE_PONG          0xA

/* WebSocket 帧结构 */
typedef struct {
    ws_frame_header_t header;
    uint8_t mask_key[4];
    uint8_t *payload;
    size_t payload_len;
} ws_frame_t;

/* WebSocket 状态 */
typedef enum {
    WS_STATE_CONNECTING = 0,
    WS_STATE_OPEN       = 1,
    WS_STATE_CLOSING    = 2,
    WS_STATE_CLOSED     = 3
} ws_state_t;

/* WebSocket 连接 */
typedef struct {
    tls_connection_t tls;
    ws_state_t state;
    char rx_buffer[8192];
    size_t rx_len;
} websocket_connection_t;

WebSocket 握手

/* 生成 Sec-WebSocket-Key */
int ws_generate_handshake_key(char *key_out, size_t key_len) {
    unsigned char random_bytes[16];
    FILE *urandom = fopen("/dev/urandom", "rb");
    if (!urandom) {
        return -1;
    }
    fread(random_bytes, 1, 16, urandom);
    fclose(urandom);

    /* Base64 编码 */
    EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
    int out_len = 0;
    EVP_EncodeInit(ctx);
    EVP_EncodeUpdate(ctx, (unsigned char*)key_out, &out_len, random_bytes, 16);
    int final_len = 0;
    EVP_EncodeFinal(ctx, (unsigned char*)key_out + out_len, &final_len);
    EVP_ENCODE_CTX_free(ctx);

    /* 移除换行符 */
    char *newline = strchr(key_out, '\n');
    if (newline) *newline = '\0';

    return 0;
}

/* 构建 HTTP Upgrade 请求 */
int ws_build_handshake_request(
    const char *host,
    uint16_t port,
    const char *path,
    const char *token,
    const char *device_id,
    const char *client_id,
    char *request_out,
    size_t request_len
) {
    char key[32];
    if (ws_generate_handshake_key(key, sizeof(key)) != 0) {
        return -1;
    }

    int ret = snprintf(request_out, request_len,
        "GET %s HTTP/1.1\r\n"
        "Host: %s\r\n"
        "Upgrade: websocket\r\n"
        "Connection: Upgrade\r\n"
        "Sec-WebSocket-Key: %s\r\n"
        "Sec-WebSocket-Version: 13\r\n"
        "Authorization: Bearer %s\r\n"
        "Protocol-Version: 3\r\n"
        "Device-Id: %s\r\n"
        "Client-Id: %s\r\n"
        "\r\n",
        path, host, key, token, device_id, client_id
    );

    return (ret < 0 || ret >= request_len) ? -1 : 0;
}

/* 解析服务器响应 */
int ws_parse_handshake_response(const char *response, size_t response_len) {
    /* 检查 HTTP 状态码 */
    if (strncmp(response, "HTTP/1.1 101", 12) != 0 &&
        strncmp(response, "HTTP/1.0 101", 12) != 0) {
        fprintf(stderr, "Invalid HTTP status: %.12s\n", response);
        return -1;
    }

    /* 检查 Upgrade 头 */
    if (strstr(response, "Upgrade: websocket") == NULL &&
        strstr(response, "upgrade: websocket") == NULL) {
        fprintf(stderr, "Missing Upgrade header\n");
        return -1;
    }

    /* 检查 Connection 头 */
    if (strstr(response, "Connection: Upgrade") == NULL &&
        strstr(response, "connection: Upgrade") == NULL) {
        fprintf(stderr, "Missing Connection header\n");
        return -1;
    }

    return 0;
}

/* WebSocket 连接 */
int ws_connect(
    websocket_connection_t *ws,
    const char *url,
    const char *token,
    const char *device_id,
    const char *client_id
) {
    /* 解析 URL: wss://host:port/path */
    char host[256] = {0};
    uint16_t port = 443;
    char path[256] = "/";

    if (strncmp(url, "wss://", 6) == 0) {
        const char *p = url + 6;
        const char *slash = strchr(p, '/');
        const char *colon = strchr(p, ':');

        if (colon && (!slash || colon < slash)) {
            size_t host_len = colon - p;
            if (host_len >= sizeof(host)) host_len = sizeof(host) - 1;
            strncpy(host, p, host_len);
            port = atoi(colon + 1);

            if (slash) {
                strncpy(path, slash, sizeof(path) - 1);
            }
        } else if (slash) {
            size_t host_len = slash - p;
            if (host_len >= sizeof(host)) host_len = sizeof(host) - 1;
            strncpy(host, p, host_len);
            strncpy(path, slash, sizeof(path) - 1);
        } else {
            strncpy(host, p, sizeof(host) - 1);
        }
    } else {
        fprintf(stderr, "Only wss:// scheme is supported\n");
        return -1;
    }

    /* 初始化 TLS */
    tls_config_t tls_conf = {
        .verify_cert = true,
        .timeout_sec = 10
    };

    if (tls_init(&ws->tls, &tls_conf) != 0) {
        return -1;
    }

    /* 连接服务器 */
    if (tls_connect(&ws->tls, host, port) != 0) {
        tls_cleanup(&ws->tls);
        return -1;
    }

    /* 构建握手请求 */
    char request[1024];
    if (ws_build_handshake_request(host, port, path, token, device_id, client_id,
                                   request, sizeof(request)) != 0) {
        tls_close(&ws->tls);
        tls_cleanup(&ws->tls);
        return -1;
    }

    /* 发送握手请求 */
    if (tls_send(&ws->tls, request, strlen(request)) != (ssize_t)strlen(request)) {
        tls_close(&ws->tls);
        tls_cleanup(&ws->tls);
        return -1;
    }

    /* 接收响应 */
    char response[1024];
    ssize_t n = tls_recv(&ws->tls, response, sizeof(response) - 1);
    if (n <= 0) {
        fprintf(stderr, "Failed to receive handshake response\n");
        tls_close(&ws->tls);
        tls_cleanup(&ws->tls);
        return -1;
    }
    response[n] = '\0';

    /* 解析响应 */
    if (ws_parse_handshake_response(response, n) != 0) {
        fprintf(stderr, "Invalid handshake response:\n%s\n", response);
        tls_close(&ws->tls);
        tls_cleanup(&ws->tls);
        return -1;
    }

    ws->state = WS_STATE_OPEN;
    ws->rx_len = 0;

    printf("WebSocket connected to %s\n", url);
    return 0;
}

WebSocket 帧处理

/* 掩码/反掩码 */
void ws_mask_payload(uint8_t *data, size_t len, const uint8_t *mask) {
    for (size_t i = 0; i < len; i++) {
        data[i] ^= mask[i % 4];
    }
}

/* 发送文本帧 */
int ws_send_text(websocket_connection_t *ws, const char *text) {
    if (ws->state != WS_STATE_OPEN) {
        return -1;
    }

    size_t text_len = strlen(text);
    uint8_t frame_header[14];
    size_t header_len = 0;

    /* 构建帧头 */
    frame_header[0] = 0x81;  /* FIN + TEXT opcode */

    if (text_len < 126) {
        frame_header[1] = text_len;
        header_len = 2;
    } else if (text_len < 65536) {
        frame_header[1] = 126;
        frame_header[2] = (text_len >> 8) & 0xFF;
        frame_header[3] = text_len & 0xFF;
        header_len = 4;
    } else {
        /* 不支持超大帧 */
        return -1;
    }

    /* 发送帧头 */
    if (tls_send(&ws->tls, frame_header, header_len) != (ssize_t)header_len) {
        return -1;
    }

    /* 发送负载数据 */
    if (tls_send(&ws->tls, text, text_len) != (ssize_t)text_len) {
        return -1;
    }

    return 0;
}

/* 接收帧 */
int ws_recv_frame(websocket_connection_t *ws, char *data, size_t data_len, int *opcode) {
    if (ws->state != WS_STATE_OPEN) {
        return -1;
    }

    /* 接收帧头前 2 字节 */
    uint8_t header[2];
    ssize_t n = tls_recv(&ws->tls, header, 2);
    if (n <= 0) {
        return -1;
    }

    bool fin = (header[0] & 0x80) != 0;
    *opcode = header[0] & 0x0F;
    bool masked = (header[1] & 0x80) != 0;
    uint64_t payload_len = header[1] & 0x7F;

    /* 读取扩展长度 */
    if (payload_len == 126) {
        uint8_t ext_len[2];
        n = tls_recv(&ws->tls, ext_len, 2);
        if (n != 2) return -1;
        payload_len = (ext_len[0] << 8) | ext_len[1];
    } else if (payload_len == 127) {
        uint8_t ext_len[8];
        n = tls_recv(&ws->tls, ext_len, 8);
        if (n != 8) return -1;
        /* 简化处理:不支持超大帧 */
        return -1;
    }

    /* 读取掩码键 (客户端接收的帧应该没有掩码) */
    uint8_t mask_key[4] = {0};
    if (masked) {
        n = tls_recv(&ws->tls, mask_key, 4);
        if (n != 4) return -1;
    }

    /* 读取负载数据 */
    if (payload_len > data_len - 1) {
        return -1;
    }

    size_t total_recv = 0;
    while (total_recv < payload_len) {
        n = tls_recv(&ws->tls, data + total_recv, payload_len - total_recv);
        if (n <= 0) return -1;
        total_recv += n;
    }

    /* 反掩码 */
    if (masked) {
        ws_mask_payload((uint8_t*)data, payload_len, mask_key);
    }

    data[payload_len] = '\0';

    /* 处理关闭帧 */
    if (*opcode == WS_OPCODE_CLOSE) {
        ws->state = WS_STATE_CLOSING;
        /* 发送关闭响应 */
        uint8_t close_frame[2] = {0x88, 0x00};
        tls_send(&ws->tls, close_frame, 2);
        return 0;
    }

    /* 处理 Ping 帧 */
    if (*opcode == WS_OPCODE_PING) {
        /* 自动回复 Pong */
        uint8_t pong_header = 0x8A;
        tls_send(&ws->tls, &pong_header, 1);
        tls_send(&ws->tls, data, payload_len);
        return ws_recv_frame(ws, data, data_len, opcode);  /* 继续接收 */
    }

    return payload_len;
}

/* 关闭 WebSocket 连接 */
void ws_close(websocket_connection_t *ws) {
    if (ws->state == WS_STATE_OPEN) {
        /* 发送关闭帧 */
        uint8_t close_frame[2] = {0x88, 0x00};
        tls_send(&ws->tls, close_frame, 2);
        ws->state = WS_STATE_CLOSING;
    }

    tls_close(&ws->tls);
    tls_cleanup(&ws->tls);
    ws->state = WS_STATE_CLOSED;
}

OTA HTTP 请求

使用 libcurl 的 HTTP 客户端

#include <curl/curl.h>

/* HTTP 响应结构 */
typedef struct {
    char *data;
    size_t size;
} http_response_t;

/* 写回调函数 */
size_t http_write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
    size_t total_size = size * nmemb;
    http_response_t *resp = (http_response_t *)userp;

    char *ptr = realloc(resp->data, resp->size + total_size + 1);
    if (!ptr) {
        return 0;
    }

    resp->data = ptr;
    memcpy(resp->data + resp->size, contents, total_size);
    resp->size += total_size;
    resp->data[resp->size] = '\0';

    return total_size;
}

/* 发送 HTTP GET 请求 */
int http_get(
    const char *url,
    const char *token,
    http_response_t *response
) {
    CURL *curl;
    CURLcode res;

    curl = curl_easy_init();
    if (!curl) {
        return -1;
    }

    response->data = malloc(1);
    response->data[0] = '\0';
    response->size = 0;

    /* 设置 URL */
    curl_easy_setopt(curl, CURLOPT_URL, url);

    /* 设置 CA 证书路径 */
    curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");

    /* 设置超时 */
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);

    /* 设置 Authorization 头 */
    char auth_header[256];
    snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", token);
    struct curl_slist *headers = NULL;
    headers = curl_slist_append(headers, auth_header);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);

    /* 设置写回调 */
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, response);

    /* 执行请求 */
    res = curl_easy_perform(curl);

    curl_slist_free_all(headers);

    if (res != CURLE_OK) {
        fprintf(stderr, "HTTP request failed: %s\n", curl_easy_strerror(res));
        free(response->data);
        response->data = NULL;
        response->size = 0;
        curl_easy_cleanup(curl);
        return -1;
    }

    /* 获取 HTTP 状态码 */
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
    curl_easy_cleanup(curl);

    if (http_code != 200) {
        fprintf(stderr, "HTTP error: %ld\n", http_code);
        free(response->data);
        response->data = NULL;
        response->size = 0;
        return -1;
    }

    return 0;
}

/* 发送 HTTP POST 请求 (JSON) */
int http_post_json(
    const char *url,
    const char *token,
    const char *json_data,
    http_response_t *response
) {
    CURL *curl;
    CURLcode res;

    curl = curl_easy_init();
    if (!curl) {
        return -1;
    }

    response->data = malloc(1);
    response->data[0] = '\0';
    response->size = 0;

    /* 设置 URL */
    curl_easy_setopt(curl, CURLOPT_URL, url);

    /* 设置 CA 证书路径 */
    curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");

    /* 设置超时 */
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);

    /* 设置 POST 数据 */
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_data);

    /* 设置请求头 */
    struct curl_slist *headers = NULL;
    char auth_header[256];
    snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", token);
    headers = curl_slist_append(headers, auth_header);
    headers = curl_slist_append(headers, "Content-Type: application/json");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);

    /* 设置写回调 */
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, response);

    /* 执行请求 */
    res = curl_easy_perform(curl);

    curl_slist_free_all(headers);

    if (res != CURLE_OK) {
        fprintf(stderr, "HTTP POST failed: %s\n", curl_easy_strerror(res));
        free(response->data);
        response->data = NULL;
        response->size = 0;
        curl_easy_cleanup(curl);
        return -1;
    }

    /* 获取 HTTP 状态码 */
    long http_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
    curl_easy_cleanup(curl);

    if (http_code != 200 && http_code != 201) {
        fprintf(stderr, "HTTP error: %ld\n", http_code);
        free(response->data);
        response->data = NULL;
        response->size = 0;
        return -1;
    }

    return 0;
}

OTA 激活流程

/* 设备信息 */
typedef struct {
    char mac_address[18];        /* MAC 地址 */
    char client_id[37];          /* UUID */
    char board_type[32];         /* 板型号 */
    char app_version[16];        /* 版本号 */
    char chip_model[32];         /* 芯片型号 */
} device_info_t;

/* OTA 激活响应 */
typedef struct {
    char server_url[256];        /* WebSocket 服务器地址 */
    char mqtt_endpoint[256];     /* MQTT 端点 (可选) */
    char activation_code[64];    /* 激活码 */
    char firmware_version[32];   /* 固件版本 */
} ota_activation_response_t;

/* 获取 MAC 地址 */
int device_get_mac_address(char *mac_out, size_t mac_len) {
    struct ifreq ifr;
    int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);

    if (sock < 0) {
        return -1;
    }

    /* 使用 eth0 作为默认接口 */
    strncpy(ifr.ifr_name, "eth0", IFNAMSIZ - 1);

    if (ioctl(sock, SIOCGIFHWADDR, &ifr) < 0) {
        close(sock);
        return -1;
    }

    close(sock);

    unsigned char *hwaddr = (unsigned char *)ifr.ifr_hwaddr.sa_data;
    snprintf(mac_out, mac_len, "%02x:%02x:%02x:%02x:%02x:%02x",
             hwaddr[0], hwaddr[1], hwaddr[2], hwaddr[3], hwaddr[4], hwaddr[5]);

    return 0;
}

/* 生成 UUID */
int device_generate_uuid(char *uuid_out, size_t uuid_len) {
    unsigned char uuid_bytes[16];
    FILE *urandom = fopen("/dev/urandom", "rb");
    if (!urandom) {
        return -1;
    }
    fread(uuid_bytes, 1, 16, urandom);
    fclose(urandom);

    /* 设置版本和变体 */
    uuid_bytes[6] = (uuid_bytes[6] & 0x0F) | 0x40;  /* 版本 4 */
    uuid_bytes[8] = (uuid_bytes[8] & 0x3F) | 0x80;  /* 变体 */

    snprintf(uuid_out, uuid_len,
             "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
             uuid_bytes[0], uuid_bytes[1], uuid_bytes[2], uuid_bytes[3],
             uuid_bytes[4], uuid_bytes[5], uuid_bytes[6], uuid_bytes[7],
             uuid_bytes[8], uuid_bytes[9], uuid_bytes[10], uuid_bytes[11],
             uuid_bytes[12], uuid_bytes[13], uuid_bytes[14], uuid_bytes[15]);

    return 0;
}

/* 发送 OTA 激活请求 */
int ota_send_activation_request(
    const char *server_url,
    const char *token,
    const device_info_t *device,
    ota_activation_response_t *response
) {
    /* 构建请求数据 */
    char request_data[512];
    snprintf(request_data, sizeof(request_data),
        "{"
        "\"mac_address\": \"%s\","
        "\"client_id\": \"%s\","
        "\"board_type\": \"%s\","
        "\"app_version\": \"%s\","
        "\"chip_model\": \"%s\""
        "}",
        device->mac_address,
        device->client_id,
        device->board_type,
        device->app_version,
        device->chip_model
    );

    /* 发送 POST 请求 */
    http_response_t http_resp;
    if (http_post_json(server_url, token, request_data, &http_resp) != 0) {
        return -1;
    }

    /* 解析响应 */
    cJSON *json = cJSON_Parse(http_resp.data);
    free(http_resp.data);

    if (!json) {
        fprintf(stderr, "Failed to parse activation response\n");
        return -1;
    }

    cJSON *server_url_item = cJSON_GetObjectItem(json, "server_url");
    cJSON *activation_code_item = cJSON_GetObjectItem(json, "activation_code");
    cJSON *firmware_version_item = cJSON_GetObjectItem(json, "firmware_version");

    if (server_url_item && cJSON_IsString(server_url_item)) {
        strncpy(response->server_url, server_url_item->valuestring, sizeof(response->server_url) - 1);
    }
    if (activation_code_item && cJSON_IsString(activation_code_item)) {
        strncpy(response->activation_code, activation_code_item->valuestring, sizeof(response->activation_code) - 1);
    }
    if (firmware_version_item && cJSON_IsString(firmware_version_item)) {
        strncpy(response->firmware_version, firmware_version_item->valuestring, sizeof(response->firmware_version) - 1);
    }

    cJSON_Delete(json);

    printf("Activation successful!\n");
    printf("  Server URL: %s\n", response->server_url);
    printf("  Activation Code: %s\n", response->activation_code);
    printf("  Firmware Version: %s\n", response->firmware_version);

    return 0;
}

消息处理 (简化版)

消息类型定义

/* 消息类型 */
typedef enum {
    MSG_TYPE_HELLO  = 0,
    MSG_TYPE_PING   = 4,
    MSG_TYPE_STT    = 10,
    MSG_TYPE_TTS    = 11,
    MSG_TYPE_LLM    = 12,
} msg_type_t;

/* 消息结构 */
typedef struct {
    msg_type_t type;
    cJSON *json;
} message_t;

Hello 消息

/* 发送 Hello 消息 */
int send_hello_message(websocket_connection_t *ws) {
    const char *hello_msg =
        "{"
        "\"type\": \"hello\","
        "\"version\": 1,"
        "\"transport\": \"websocket\","
        "\"audio_params\": {"
        "\"format\": \"opus\","
        "\"sample_rate\": 16000,"
        "\"channels\": 1,"
        "\"frame_duration\": 60"
        "}"
        "}";

    return ws_send_text(ws, hello_msg);
}

/* 处理服务器消息 */
int handle_server_message(websocket_connection_t *ws, const char *message) {
    cJSON *json = cJSON_Parse(message);
    if (!json) {
        fprintf(stderr, "Failed to parse message: %s\n", message);
        return -1;
    }

    cJSON *type_item = cJSON_GetObjectItem(json, "type");
    if (!type_item || !cJSON_IsString(type_item)) {
        fprintf(stderr, "Missing 'type' field\n");
        cJSON_Delete(json);
        return -1;
    }

    const char *type = type_item->valuestring;

    if (strcmp(type, "hello") == 0) {
        printf("Received: Hello message (handshake confirmed)\n");
    }
    else if (strcmp(type, "pong") == 0) {
        printf("Received: Pong message\n");
    }
    else if (strcmp(type, "stt") == 0) {
        cJSON *text_item = cJSON_GetObjectItem(json, "text");
        if (text_item && cJSON_IsString(text_item)) {
            printf("STT: %s\n", text_item->valuestring);
        }
    }
    else if (strcmp(type, "tts") == 0) {
        cJSON *state_item = cJSON_GetObjectItem(json, "state");
        if (state_item && cJSON_IsString(state_item)) {
            printf("TTS State: %s\n", state_item->valuestring);
        }
    }
    else if (strcmp(type, "llm") == 0) {
        cJSON *content_item = cJSON_GetObjectItem(json, "content");
        if (content_item && cJSON_IsString(content_item)) {
            printf("LLM: %s\n", content_item->valuestring);
        }
    }
    else {
        printf("Received: %s message\n", type);
    }

    cJSON_Delete(json);
    return 0;
}

主程序

简化状态机

/* 客户端状态 (简化版) */
typedef enum {
    STATE_STARTING     = 0,
    STATE_IDLE         = 1,
    STATE_CONNECTING    = 2,
    STATE_CONNECTED     = 3,
    STATE_ERROR        = 99
} client_state_t;

/* 主应用结构 */
typedef struct {
    client_state_t state;
    websocket_connection_t ws;
    device_info_t device;
    ota_activation_response_t activation;
    bool running;
} application_t;

主程序实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <openssl/ssl.h>
#include <curl/curl.h>
#include <cjson/cJSON.h>

/* 全局应用 */
static application_t g_app;

/* 信号处理 */
void signal_handler(int sig) {
    if (sig == SIGINT || sig == SIGTERM) {
        printf("\nShutting down...\n");
        g_app.running = false;
    }
}

/* 打印使用说明 */
void print_usage(const char *prog_name) {
    printf("Usage: %s [OPTIONS]\n\n", prog_name);
    printf("Options:\n");
    printf("  -ota <url>          OTA activation server URL\n");
    printf("                      (default: https://api.tenclass.net/ota/activate)\n");
    printf("  -ws <url>           WebSocket server URL\n");
    printf("                      (default: from activation response or\n");
    printf("                       wss://api.tenclass.net/xiaozhi/v1/)\n");
    printf("  -token <string>     Authentication token\n");
    printf("                      (default: test-token)\n");
    printf("  -activate-only      Only perform activation, then exit\n");
    printf("  -skip-tls-verify    Skip TLS certificate verification\n");
    printf("  -help, -h           Show this help\n");
    printf("\nExamples:\n");
    printf("  %s                              # Use defaults\n", prog_name);
    printf("  %s -activate-only               # Activate only\n", prog_name);
    printf("  %s -ws wss://server.com -token KEY\n", prog_name);
}

int main(int argc, char *argv[]) {
    /* 默认配置 */
    const char *ota_url = "https://api.tenclass.net/ota/activate";
    const char *ws_url = NULL;
    const char *token = "test-token";
    bool activate_only = false;
    bool skip_tls_verify = false;

    /* 解析命令行参数 */
    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-ota") == 0 && i + 1 < argc) {
            ota_url = argv[++i];
        } else if (strcmp(argv[i], "-ws") == 0 && i + 1 < argc) {
            ws_url = argv[++i];
        } else if (strcmp(argv[i], "-token") == 0 && i + 1 < argc) {
            token = argv[++i];
        } else if (strcmp(argv[i], "-activate-only") == 0) {
            activate_only = true;
        } else if (strcmp(argv[i], "-skip-tls-verify") == 0) {
            skip_tls_verify = true;
        } else if (strcmp(argv[i], "-help") == 0 || strcmp(argv[i], "-h") == 0) {
            print_usage(argv[0]);
            return 0;
        } else {
            fprintf(stderr, "Unknown option: %s\n", argv[i]);
            print_usage(argv[0]);
            return 1;
        }
    }

    /* 初始化 */
    memset(&g_app, 0, sizeof(g_app));
    g_app.state = STATE_STARTING;
    g_app.running = true;

    /* 设置信号处理 */
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);

    /* 初始化 libcurl */
    curl_global_init(CURL_GLOBAL_DEFAULT);

    /* 获取设备信息 */
    if (device_get_mac_address(g_app.device.mac_address, sizeof(g_app.device.mac_address)) != 0) {
        fprintf(stderr, "Failed to get MAC address\n");
        return 1;
    }

    if (device_generate_uuid(g_app.device.client_id, sizeof(g_app.device.client_id)) != 0) {
        fprintf(stderr, "Failed to generate UUID\n");
        return 1;
    }

    strncpy(g_app.device.board_type, "linux-generic", sizeof(g_app.device.board_type) - 1);
    strncpy(g_app.device.app_version, "1.0.0", sizeof(g_app.device.app_version) - 1);
    strncpy(g_app.device.chip_model, "x86_64", sizeof(g_app.device.chip_model) - 1);

    printf("Device Info:\n");
    printf("  MAC Address: %s\n", g_app.device.mac_address);
    printf("  Client ID: %s\n", g_app.device.client_id);
    printf("  Board: %s\n", g_app.device.board_type);
    printf("  Version: %s\n\n", g_app.device.app_version);

    /* OTA 激活 */
    printf("Activating device...\n");
    if (ota_send_activation_request(ota_url, token, &g_app.device, &g_app.activation) != 0) {
        fprintf(stderr, "Activation failed\n");
        curl_global_cleanup();
        return 1;
    }

    if (activate_only) {
        printf("Activation complete. Exiting...\n");
        curl_global_cleanup();
        return 0;
    }

    /* 如果没有指定 WebSocket URL,使用激活响应中的 URL */
    if (!ws_url) {
        if (strlen(g_app.activation.server_url) > 0) {
            ws_url = g_app.activation.server_url;
        } else {
            ws_url = "wss://api.tenclass.net/xiaozhi/v1/";
        }
    }

    /* 连接 WebSocket */
    printf("\nConnecting to WebSocket...\n");
    g_app.state = STATE_CONNECTING;

    if (ws_connect(&g_app.ws, ws_url, token, g_app.device.mac_address, g_app.device.client_id) != 0) {
        fprintf(stderr, "WebSocket connection failed\n");
        curl_global_cleanup();
        return 1;
    }

    g_app.state = STATE_CONNECTED;

    /* 发送 Hello 消息 */
    printf("Sending Hello message...\n");
    if (send_hello_message(&g_app.ws) != 0) {
        fprintf(stderr, "Failed to send Hello message\n");
    }

    /* 主循环 - 接收消息 */
    printf("\nWaiting for messages (Ctrl+C to exit)...\n\n");

    while (g_app.running) {
        char message[4096];
        int opcode;

        int len = ws_recv_frame(&g_app.ws, message, sizeof(message) - 1, &opcode);

        if (len < 0) {
            printf("Connection closed\n");
            break;
        }

        if (len == 0) {
            continue;
        }

        /* 处理消息 */
        handle_server_message(&g_app.ws, message);

        /* 定期发送 Ping */
        static int ping_counter = 0;
        ping_counter++;
        if (ping_counter > 30) {  /* 每 30 条消息发送一次 Ping */
            const char *ping_msg = "{\"type\": \"ping\", \"id\": 12345}";
            ws_send_text(&g_app.ws, ping_msg);
            ping_counter = 0;
        }
    }

    /* 清理 */
    printf("\nClosing connection...\n");
    ws_close(&g_app.ws);
    curl_global_cleanup();

    printf("Done.\n");
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(xiaozhi-simple C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")

# 第三方库
set(CJSON_DIR ${CMAKE_SOURCE_DIR}/third_party/cjson)

# 包含目录
include_directories(
    ${CMAKE_SOURCE_DIR}/include
    ${CJSON_DIR}
)

# 源文件
set(SOURCES
    src/main.c
    src/websocket.c
    src/tls.c
    src/http.c
    src/ota.c
    src/device.c
    src/message.c
    ${CJSON_DIR}/cJSON.c
)

# 可执行文件
add_executable(xiaozhi-simple ${SOURCES})

# 链接库
find_package(OpenSSL REQUIRED)
find_package(CURL REQUIRED)

target_link_libraries(xiaozhi-simple
    OpenSSL::SSL
    OpenSSL::Crypto
    CURL::libcurl
    pthread
)

# 安装 (可选)
install(TARGETS xiaozhi-simple DESTINATION bin)

编译与运行

编译步骤

# 创建构建目录
mkdir build && cd build

# 配置
cmake ..

# 编译
make

# 安装 (可选)
sudo make install

运行示例

# 仅激活 (使用默认服务器)
./xiaozhi-simple -activate-only

# 完整流程 (使用默认服务器和 token)
./xiaozhi-simple

# 指定 WebSocket 服务器和 token
./xiaozhi-simple -ws wss://api.tenclass.net/xiaozhi/v1/ -token your-token

# 仅激活,指定 OTA 服务器
./xiaozhi-simple -activate-only -ota https://api.tenclass.net/ota/activate

# 跳过 TLS 验证 (用于测试)
./xiaozhi-simple -skip-tls-verify

验证测试计划

1. OTA HTTP 请求测试

# 使用默认 OTA 服务器测试激活
./xiaozhi-simple -activate-only

# 或指定自定义 OTA 服务器
./xiaozhi-simple -activate-only -ota https://your-server.com/ota

# 预期输出:
# - 设备信息 (MAC 地址, UUID)
# - HTTP 请求状态
# - 激活响应 (服务器 URL, 激活码, 固件版本)

2. WebSocket 握手测试

# 使用默认设置测试 WebSocket 连接
./xiaozhi-simple

# 或指定自定义服务器
./xiaozhi-simple -ws wss://your-server.com/ws -token your-token

# 预期输出:
# - TLS 握手成功
# - WebSocket 握手成功
# - Hello 消息发送/接收

3. 消息处理测试

# 运行程序,观察服务器消息
./xiaozhi-simple

# 验证:
# - Ping/Pong 消息
# - JSON 消息解析
# - 各类服务器消息处理


故障排除

TLS 连接失败

# 检查 OpenSSL 版本
openssl version

# 测试 HTTPS 连接
openssl s_client -connect api.tenclass.net:443

# 跳过证书验证 (仅测试)
./xiaozhi-simple -skip-tls-verify

WebSocket 连接失败

# 检查网络连接
ping api.tenclass.net

# 检查 DNS
nslookup api.tenclass.net

# 使用 curl 测试 HTTPS
curl -v https://api.tenclass.net/

MAC 地址获取失败

# 查看网络接口
ip link show

# 检查 eth0 是否存在
ip link show eth0

# 修改代码使用其他接口 (wlan0, enp0s3 等)

版本历史

版本 日期 说明
V1.2 2026-03-07 简化验证版本 - OTA HTTP + WebSocket 连接

文档最后更新: 2026-03-07