项目名称: 小智 (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-configsudo dnf install openssl-devel libcurl-devel gcc cmake pkg-configsudo pacman -S openssl curl gcc cmake pkg-config/* 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 连接 */
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);#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 帧头 (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;/* 生成 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;
}/* 掩码/反掩码 */
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;
}#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;
}/* 设备信息 */
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 消息 */
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;
}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# 使用默认 OTA 服务器测试激活
./xiaozhi-simple -activate-only
# 或指定自定义 OTA 服务器
./xiaozhi-simple -activate-only -ota https://your-server.com/ota
# 预期输出:
# - 设备信息 (MAC 地址, UUID)
# - HTTP 请求状态
# - 激活响应 (服务器 URL, 激活码, 固件版本)# 使用默认设置测试 WebSocket 连接
./xiaozhi-simple
# 或指定自定义服务器
./xiaozhi-simple -ws wss://your-server.com/ws -token your-token
# 预期输出:
# - TLS 握手成功
# - WebSocket 握手成功
# - Hello 消息发送/接收# 运行程序,观察服务器消息
./xiaozhi-simple
# 验证:
# - Ping/Pong 消息
# - JSON 消息解析
# - 各类服务器消息处理# 检查 OpenSSL 版本
openssl version
# 测试 HTTPS 连接
openssl s_client -connect api.tenclass.net:443
# 跳过证书验证 (仅测试)
./xiaozhi-simple -skip-tls-verify# 检查网络连接
ping api.tenclass.net
# 检查 DNS
nslookup api.tenclass.net
# 使用 curl 测试 HTTPS
curl -v https://api.tenclass.net/# 查看网络接口
ip link show
# 检查 eth0 是否存在
ip link show eth0
# 修改代码使用其他接口 (wlan0, enp0s3 等)| 版本 | 日期 | 说明 |
|---|---|---|
| V1.2 | 2026-03-07 | 简化验证版本 - OTA HTTP + WebSocket 连接 |
文档最后更新: 2026-03-07