控制中心功能开发
https功能开发
HTTP基础概念及操作
HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网上应用最广泛的一种网络协议,用于客户端(如浏览器、程序)与服务器之间的通信。简单来说,就是客户端向服务器发送请求,服务器根据请求返回相应的数据。
HTTP有如下几种操作方法。
(1)GET方法:从服务器获取数据(相当于 "读取" 操作)
特点:
-
参数会直接附加在 URL 后面(例如:https://example.com/get?name=test&age=18)
-
由于参数在 URL 中可见,不适合传输敏感信息(如密码)
-
传输的数据量有限制(取决于浏览器和服务器的限制)
-
典型场景:浏览网页、搜索内容、获取列表数据等
(2)POST方法:服务器提交数据(相当于 "写入" 操作)
特点:
-
参数不会显示在 URL 中,而是放在请求体(Request Body)中
-
适合传输敏感信息和大量数据
-
数据传输量没有严格限制(由服务器配置决定)
-
典型场景:用户登录、表单提交、上传文件等
(3)其他常见方法
-
PUT:更新服务器上的资源(全量更新)
-
DELETE:删除服务器上的资源
-
PATCH:部分更新服务器上的资源
使用HTTP的通信流程为:
-
客户端建立与服务器的连接
-
客户端发送 HTTP 请求(包含请求方法、目标 URL、请求头、请求体等)
-
服务器接收请求并处理
-
服务器返回 HTTP 响应(包含状态码、响应头、响应体等)
-
客户端接收响应并处理数据
-
连接关闭(短连接模式下)
libcurl库的使用
第1步:初始化 libcurl 全局环境
curl_global_init(CURL_GLOBAL_ALL);
功能:初始化 libcurl 的全局资源,必须在所有其他 curl 函数之前调用
参数CURL_GLOBAL_ALL表示初始化所有可能的全局资源
第2步:创建 curl 会话句柄
CURL \*curl = curl_easy_init();
if(curl == NULL) {
// 初始化失败处理
return 1;
}
功能:创建一个 curl 会话实例,后续的操作都基于此句柄
必须检查返回值,确保创建成功
第3步:设置请求选项
使用curl_easy_setopt函数设置各种请求参数,常用选项:
// 设置请求的URL
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com");
// 设置是否跟随重定向(1L表示启用)
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
// SSL相关设置(示例中禁用了验证,实际生产环境不推荐)
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // 禁用服务器证书验证
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); // 禁用主机名验证
第4步:通过回调函数处理服务器返回的数据。
// 定义存储响应数据的结构体
struct MemoryStruct {
char \*memory; // 存储数据的缓冲区
size_t size; // 数据大小
};
// 回调函数:用于接收服务器返回的数据
static size_t WriteMemoryCallback(void \*contents, size_t size, size_t nmemb, void \*userp) {
size_t realsize = size \* nmemb;
struct MemoryStruct \*mem = (struct MemoryStruct \*)userp;
// 重新分配内存以容纳新数据
char \*ptr = realloc(mem-\>memory, mem-\>size + realsize + 1);
if(!ptr) {
printf("内存不足\n");
return 0;
}
mem-\>memory = ptr;
// 复制新数据到缓冲区
memcpy(&(mem-\>memory\[mem-\>size\]), contents, realsize);
mem-\>size += realsize;
mem->memory[mem->size] = 0; // 添加字符串结束符
return realsize; // 返回实际处理的数据大小
}
// 初始化缓冲区
struct MemoryStruct chunk;
chunk.memory = malloc(1);
chunk.size = 0;
// 设置回调函数和数据存储结构体
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void \*)&chunk);
第5步:执行请求
CURLcode res = curl_easy_perform(curl);
if(res != CURLE_OK) {
// 请求失败处理
syslog(LOG_INFO, "请求失败: %s\n", curl_easy_strerror(res));
} else {
// 请求成功,处理响应数据
syslog(LOG_INFO,"接收数据大小: %lu 字节\n", (unsigned long)chunk.size);
// chunk.memory中存储了服务器返回的数据
}
第6步:释放资源
// 清理curl会话句柄
curl_easy_cleanup(curl);
// 清理libcurl全局环境
curl_global_cleanup();
// 释放存储响应数据的内存
free(chunk.memory);
在第3步中添加以下设置即可发送 POST 请求:
// 设置为POST方法
curl_easy_setopt(curl, CURLOPT_POST, 1L);
// 设置POST数据
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "name=test&age=18"); // 表单数据
示例程序
源码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <syslog.h>
#include <curl/curl.h>
// 定义宏将syslog替换为printf
#define syslog(priority, ...) printf(__VA_ARGS__)
struct MemoryStruct {
char \*memory;
size_t size;
};
static size_t WriteMemoryCallback(void \*contents, size_t size, size_t nmemb, void \*userp) {
size_t realsize = size \* nmemb;
struct MemoryStruct \*mem = (struct MemoryStruct \*)userp;
char \*ptr = realloc(mem-\>memory, mem-\>size + realsize + 1);
if(!ptr) {
printf("not enough memory (realloc returned NULL)\n");
return 0;
}
mem-\>memory = ptr;
memcpy(&(mem-\>memory\[mem-\>size\]), contents, realsize);
mem-\>size += realsize;
mem-\>memory\[mem-\>size\] = 0;
return realsize;
}
int main(int argc, char \*argv\[\])
{
CURL \*curl;
CURLcode res;
syslog(LOG_INFO,"to curl_global_init\n");
curl_global_init(CURL_GLOBAL_ALL);
// 初始化curl句柄
curl = curl_easy_init();
if (!curl) {
fprintf(stderr, "curl_easy_init() failed\n");
return 1;
}
// 准备接收数据的结构
struct MemoryStruct chunk;
// 先执行POST请求
syslog(LOG_INFO, "=== Performing POST request ===\n");
// 重置并设置POST请求选项
curl_easy_reset(curl);
curl_easy_setopt(curl, CURLOPT_URL, "https://httpbin.org/post");
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36");
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "name=test&age=18");
// 设置接收数据的回调
chunk.memory = malloc(1);
chunk.size = 0;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void \*)&chunk);
res = curl_easy_perform(curl);
if(res != CURLE_OK) {
syslog(LOG_INFO, "POST request failed: %s\n", curl_easy_strerror(res));
} else {
syslog(LOG_INFO, "POST request successful, %lu bytes retrieved\n", (unsigned long)chunk.size);
if(chunk.size \> 0) {
syslog(LOG_INFO, "POST Response data:\n%.\*s\n", (int)chunk.size, chunk.memory);
}
}
// 清理POST请求的数据
if (chunk.memory) {
free(chunk.memory);
}
// 再执行GET请求
syslog(LOG_INFO, "=== Performing GET request ===\n");
// 重置并设置GET请求选项
curl_easy_reset(curl);
curl_easy_setopt(curl, CURLOPT_URL, "https://httpbin.org/get");
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36");
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
// 设置接收数据的回调
chunk.memory = malloc(1);
chunk.size = 0;
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void \*)&chunk);
res = curl_easy_perform(curl);
if(res != CURLE_OK) {
syslog(LOG_INFO, "GET request failed: %s\n", curl_easy_strerror(res));
} else {
syslog(LOG_INFO, "GET request successful, %lu bytes retrieved\n", (unsigned long)chunk.size);
if(chunk.size \> 0) {
syslog(LOG_INFO, "GET Response data:\n%.\*s\n", (int)chunk.size, chunk.memory);
}
}
// 清理
if (chunk.memory) {
free(chunk.memory);
}
curl_easy_cleanup(curl);
curl_global_cleanup();
return 0;
}
上机测试
按照视频把http加入工程、配置:
CONFIG_EXAMPLES_HTTP=y
CONFIG_LIB_CURL=y
CONFIG_DEV_URANDOM=y # 不加的话会卡死
CONFIG_DEV_URANDOM_ARCH=y
然后先清除再编译、打包,命令如下:
m c \# 必须先清除
m
pack
烧写映像文件后启动,先配置、测试WIFI(最好接上天线):
ifup wlan0
wapi mode wlan0 2
# 100asktech 是WIFI密码,需要改成你自己的
wapi psk wlan0 100asktech 3
# Programmers是WIFI AP名字,需要改成你自己的
wapi essid wlan0 Programmers 1
renew wlan0
# 查看IP
ifconfig
# ping互联网, Ctrl+C退出
ping news.qq.com
再执行http命令:
http
websocket功能开发
WebSocket 基本概念
WebSocket是一种在单个 TCP 连接上进行全双工通信的协议,与传统的 HTTP 协议相比,它最大的特点是持久连接和双向通信。这意味着客户端和服务器建立连接后,可以随时向对方发送数据,而无需像 HTTP 那样每次通信都重新建立连接。
WebSocket 定义了两种主要的数据传输格式:
(1)文本数据(Text):
-
基于 UTF-8 编码的字符串数据
-
适合传输 JSON、XML、普通文本等可读性强的数据
-
在 libwebsockets 中对应 LWS_WRITE_TEXT 标志
(2)二进制数据(Binary):
-
原始字节流,没有编码限制
-
适合传输图片、音频、视频等二进制文件
-
在 libwebsockets 中对应 LWS_WRITE_BINARY 标志
两种类型的主要区别在于:文本数据必须符合 UTF-8 编码规范,而二进制数据可以是任意字节组合。接收方需要根据数据类型采用不同的处理方式。
libwebsockets 的使用
libwebsockets 是一个轻量级的 C 语言 WebSocket 库,支持客户端和服务器模式,同时提供了对 HTTP、SSL/TLS 等协议的支持。下面结合示例代码,讲解如何使用 libwebsockets 开发 WebSocket 客户端。
示例代码实现了一个简单的 WebSocket 客户端,功能包括:
-
连接到公共回声服务器(echo.websocket.org)
-
发送 5 条文本消息
-
接收服务器返回的回声消息
-
支持优雅退出
第1步:实现核心回调函数(处理事件逻辑)。
回调函数是 libwebsockets 的 “大脑”,所有网络事件(连接、发送、接收、关闭)都通过回调函数触发。需按步骤处理不同事件类型,代码如下:
static int callback(struct lws \*wsi, enum lws_callback_reasons reason, void \*user, void \*in, size_t len) {
struct session_data \*data = (struct session_data \*)user;
switch (reason) {
case LWS_CALLBACK_CLIENT_ESTABLISHED:
printf("连接已建立到服务器\n");
data-\>message_count = 0;
data-\>message_received = 0;
// 连接建立后,标记可写状态以发送第一条消息
lws_callback_on_writable(wsi);
break;
case LWS_CALLBACK_CLIENT_WRITEABLE: {
// 准备并发送消息
if (data-\>message_count \< 5) { // 示例:发送5条消息
char message\[128\];
int msg_len = snprintf(message, sizeof(message), "你好, 服务器! 这是消息 \#%d", data-\>message_count + 1);
// 分配发送缓冲区,libwebsockets 要求在数据前后预留空间
unsigned char \*buf = (unsigned char \*)malloc(LWS_PRE + msg_len + 1);
if (!buf) {
return -1;
}
memcpy(buf + LWS_PRE, message, msg_len);
buf[LWS_PRE + msg_len] = '\0'; // 确保字符串终止
int ret = lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
free(buf);
if (ret \< msg_len) {
fprintf(stderr, "部分消息发送失败\n");
return -1;
} else {
printf("已发送: %s\n", message);
data-\>message_count++;
}
// 再次请求可写回调,准备发送下一条消息(或等待回复)
lws_callback_on_writable(wsi);
}
break;
}
case LWS_CALLBACK_CLIENT_RECEIVE:
// 处理接收到的消息
printf("收到回声: %.*s\n", (int)len, (char *)in);
data-\>message_received++;
// 收到回复后,可以计划发送下一条消息,或者在此示例中,我们发送5条后主要等待接收
// 在实际程序中,你可能需要更复杂的逻辑来控制流程
if (data-\>message_received \>= 5) {
printf("已收到所有预期回复。准备断开连接。\n");
interrupted = 1; // 触发断开连接和退出
}
break;
case LWS_CALLBACK_CLIENT_CLOSED:
printf("连接已关闭\n");
interrupted = 1;
break;
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
fprintf(stderr, "连接错误: %s\n", in ? (char *)in : "未知错误");
interrupted = 1;
break;
default:
break;
}
return 0;
}
第2步:定义协议配置。将回调函数与协议名称绑定,告知 libwebsockets 用哪个回调处理事件。
// 定义我们使用的协议
static struct lws_protocols protocols\[\] = {
{
"echo-protocol",
callback,
sizeof(struct session_data), // 每个会话数据大小
0, // 最大帧大小,0表示使用库默认值
},
{ NULL, NULL, 0, 0 } // 结束标记
};
第3步:创建 libwebsockets 上下文(全局管理对象)
上下文(lws_context)是 libwebsockets 的全局管理对象,负责管理所有连接和资源,需按步骤初始化:
// 定义上下文创建信息结构体,并初始化清零
memset(&info, 0, sizeof(info));
// 配置上下文参数(客户端模式)
info.port = CONTEXT_PORT_NO_LISTEN; // 不作为服务器监听端口
info.iface = NULL;
info.protocols = protocols; // 关联协议数组(绑定回调函数)
info.gid = -1;
info.uid = -1;
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; // 启用 SSL
// 创建 libwebsockets 上下文
context = lws_create_context(&info);
if (!context) {
fprintf(stderr, "创建 libwebsockets 上下文失败\n");
return -1;
}
第3步:配置并发起服务器连接。
设置服务器地址、端口、路径等信息,发起 WebSocket 连接:
// 创建 libwebsockets 上下文
context = lws_create_context(&info);
if (!context) {
fprintf(stderr, "创建 libwebsockets 上下文失败\n");
return -1;
}
// 配置连接信息
// 先初始化清零
memset(&connect_info, 0, sizeof(connect_info));
// 配置连接参数
connect_info.context = context; // 关联上下文
connect_info.address = url; // 服务器地址
connect_info.port = port; // 端口
connect_info.path = "/"; // 请求路径
connect_info.host = connect_info.address; // 主机名(与地址一致,用于 SSL 验证)
connect_info.origin = connect_info.address;
connect_info.protocol = protocols\[0\].name;
connect_info.ssl_connection = LCCSCF_USE_SSL; // 使用 SSL (WSS)
printf("正在连接到 %s ...\n", url);
// 发起连接
wsi = lws_client_connect_via_info(&connect_info);
if (!wsi) {
fprintf(stderr, "初始连接尝试失败\n");
lws_context_destroy(context);
return -1;
}
第4步:启动事件循环(处理网络事件)。
事件循环是客户端的 “心脏”,通过 lws_service 函数持续处理网络事件(如连接、发送、接收):
// 事件循环
// 进入事件循环,直到 interrupted 为 1(触发退出)
while (!interrupted) {
// 调用 lws_service 处理事件,超时 50 毫秒(避免阻塞)
// 检查是否有新事件(如可写、接收),并触发对应回调函数
lws_service(context, 50); // 50毫秒超时
}
第5步:清理资源(避免内存泄漏)
事件循环退出后,需按步骤销毁连接和上下文,释放所有资源:
// 销毁上下文(自动关闭所有连接)
lws_context_destroy(context);
示例程序
源码如下:
#include <libwebsockets.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
static int interrupted = 0;
void signal_handler(int sig) {
interrupted = 1;
}
struct session_data {
int message_count;
int message_received;
};
static int callback(struct lws \*wsi, enum lws_callback_reasons reason, void \*user, void \*in, size_t len) {
struct session_data \*data = (struct session_data \*)user;
switch (reason) {
case LWS_CALLBACK_CLIENT_ESTABLISHED:
printf("连接已建立到服务器\n");
data-\>message_count = 0;
data-\>message_received = 0;
// 连接建立后,标记可写状态以发送第一条消息
lws_callback_on_writable(wsi);
break;
case LWS_CALLBACK_CLIENT_WRITEABLE: {
// 准备并发送消息
if (data-\>message_count \< 5) { // 示例:发送5条消息
char message\[128\];
int msg_len = snprintf(message, sizeof(message), "你好, 服务器! 这是消息 \#%d", data-\>message_count + 1);
// 分配发送缓冲区,libwebsockets 要求在数据前后预留空间
unsigned char \*buf = (unsigned char \*)malloc(LWS_PRE + msg_len + 1);
if (!buf) {
return -1;
}
memcpy(buf + LWS_PRE, message, msg_len);
buf[LWS_PRE + msg_len] = '\0'; // 确保字符串终止
int ret = lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
free(buf);
if (ret \< msg_len) {
fprintf(stderr, "部分消息发送失败\n");
return -1;
} else {
printf("已发送: %s\n", message);
data-\>message_count++;
}
// 再次请求可写回调,准备发送下一条消息(或等待回复)
lws_callback_on_writable(wsi);
}
break;
}
case LWS_CALLBACK_CLIENT_RECEIVE:
// 处理接收到的消息
printf("收到回声: %.*s\n", (int)len, (char *)in);
data-\>message_received++;
// 收到回复后,可以计划发送下一条消息,或者在此示例中,我们发送5条后主要等待接收
// 在实际程序中,你可能需要更复杂的逻辑来控制流程
if (data-\>message_received \>= 5) {
printf("已收到所有预期回复。准备断开连接。\n");
interrupted = 1; // 触发断开连接和退出
}
break;
case LWS_CALLBACK_CLIENT_CLOSED:
printf("连接已关闭\n");
interrupted = 1;
break;
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
fprintf(stderr, "连接错误: %s\n", in ? (char *)in : "未知错误");
interrupted = 1;
break;
default:
break;
}
return 0;
}
// 定义我们使用的协议
static struct lws_protocols protocols\[\] = {
{
"echo-protocol",
callback,
sizeof(struct session_data), // 每个会话数据大小
0, // 最大帧大小,0表示使用库默认值
},
{ NULL, NULL, 0, 0 } // 结束标记
};
int main() {
// 设置信号处理,用于优雅退出
signal(SIGINT, signal_handler);
struct lws_context_creation_info info;
struct lws_context \*context;
struct lws_client_connect_info connect_info;
struct lws \*wsi;
const char \*url = "echo.websocket.org";
int port = 443; // SSL 端口
struct session_data data;
// 定义上下文创建信息结构体,并初始化清零
memset(&info, 0, sizeof(info));
// 配置上下文参数(客户端模式)
info.port = CONTEXT_PORT_NO_LISTEN; // 不作为服务器监听端口
info.iface = NULL;
info.protocols = protocols; // 关联协议数组(绑定回调函数)
info.gid = -1;
info.uid = -1;
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; // 启用 SSL
// 创建 libwebsockets 上下文
context = lws_create_context(&info);
if (!context) {
fprintf(stderr, "创建 libwebsockets 上下文失败\n");
return -1;
}
// 配置连接信息
// 先初始化清零
memset(&connect_info, 0, sizeof(connect_info));
// 配置连接参数
connect_info.context = context; // 关联上下文
connect_info.address = url; // 服务器地址
connect_info.port = port; // 端口
connect_info.path = "/"; // 请求路径
connect_info.host = connect_info.address; // 主机名(与地址一致,用于 SSL 验证)
connect_info.origin = connect_info.address;
connect_info.protocol = protocols\[0\].name;
connect_info.ssl_connection = LCCSCF_USE_SSL; // 使用 SSL (WSS)
printf("正在连接到 %s ...\n", url);
// 发起连接
wsi = lws_client_connect_via_info(&connect_info);
if (!wsi) {
fprintf(stderr, "初始连接尝试失败\n");
lws_context_destroy(context);
return -1;
}
// 事件循环
// 进入事件循环,直到 interrupted 为 1(触发退出)
while (!interrupted) {
// 调用 lws_service 处理事件,超时 50 毫秒(避免阻塞)
// 检查是否有新事件(如可写、接收),并触发对应回调函数
lws_service(context, 50); // 50毫秒超时
}
// 销毁上下文(自动关闭所有连接)
lws_context_destroy(context);
printf("程序结束\n");
return 0;
}
上机测试
按照视频把websocket_test加入工程、配置:
CONFIG_EXAMPLES_WEBSOCKET_TEST=y
CONFIG_NETUTILS_CJSON=y
CONFIG_OPENSSL_MBEDTLS_WRAPPER=y
CONFIG_NETUTILS_LIBWEBSOCKETS=y
CONFIG_CRYPTO_MBEDTLS=y
CONFIG_MBEDTLS_NET_C=y
CONFIG_NET_TCPPROTO_OPTIONS=y
修改“apps/netutils/libwebsockets/Makefile”,加入最后3行:
CFLAGS += \
-I. \
-I$(LIBWEBSOCKETS_UNPACK)/lib/core \
-I$(LIBWEBSOCKETS_UNPACK)/lib/plat/unix \
-I$(LIBWEBSOCKETS_UNPACK)/lib/event-libs \
-I$(LIBWEBSOCKETS_UNPACK)/lib/system/smd \
-I$(LIBWEBSOCKETS_UNPACK)/lib/system/metrics \
-I$(LIBWEBSOCKETS_UNPACK)/lib/core-net \
-I$(LIBWEBSOCKETS_UNPACK)/lib/roles \
-I$(LIBWEBSOCKETS_UNPACK)/lib/roles/http \
-I$(LIBWEBSOCKETS_UNPACK)/lib/roles/h1 \
-I$(LIBWEBSOCKETS_UNPACK)/lib/roles/h2 \
-I$(LIBWEBSOCKETS_UNPACK)/lib/roles/ws \
-I$(LIBWEBSOCKETS_UNPACK)/lib/tls \
-I$(LIBWEBSOCKETS_UNPACK)/lib/tls/mbedtls/wrapper/include/openssl \
-I$(LIBWEBSOCKETS_UNPACK)/lib/tls/mbedtls/wrapper/include/internal \
-I$(LIBWEBSOCKETS_UNPACK)/lib/tls/mbedtls/wrapper/include/platform
继续修改“apps/netutils/libwebsockets/Makefile”,加入最后1行:
CSRCS += \
$(LIBWEBSOCKETS_UNPACK)/lib/tls/tls.c \
$(LIBWEBSOCKETS_UNPACK)/lib/tls/tls-network.c \
$(LIBWEBSOCKETS_UNPACK)/lib/tls/tls-sessions.c \
$(LIBWEBSOCKETS_UNPACK)/lib/tls/tls-client.c \
$(LIBWEBSOCKETS_UNPACK)/lib/tls/mbedtls/mbedtls-tls.c \
$(LIBWEBSOCKETS_UNPACK)/lib/tls/mbedtls/mbedtls-extensions.c \
$(LIBWEBSOCKETS_UNPACK)/lib/tls/mbedtls/mbedtls-x509.c \
$(LIBWEBSOCKETS_UNPACK)/lib/tls/mbedtls/mbedtls-ssl.c \
$(LIBWEBSOCKETS_UNPACK)/lib/tls/mbedtls/mbedtls-session.c \
$(LIBWEBSOCKETS_UNPACK)/lib/tls/mbedtls/mbedtls-client.c \
$(LIBWEBSOCKETS_UNPACK)/lib/tls/mbedtls/wrapper/platform/ssl_pm.c # 100ask
修改“apps/crypto/openssl_mbedtls_wrapper/mbedtls/ssl_lib.c”,加入下面的代码:
/* 100ask */
int SSL_want_write(const SSL \*ssl)
{
SSL_ASSERT1(ssl);
if (ssl-\>err)
return 0;
return (SSL_want(ssl) == SSL_WRITING);
}
修改“apps/crypto/openssl_mbedtls_wrapper/mbedtls/err.c”,加入下面的代码:
/* 100ask */
char \*ERR_error_string(unsigned long e, char \*buf)
{
return "unknown";
}
然后先清除再编译、打包,命令如下:
m c \# 必须先清除
m
pack
烧写映像文件后启动,先配置、测试WIFI(最好接上天线):
ifup wlan0
wapi mode wlan0 2
# 100asktech 是WIFI密码,需要改成你自己的
wapi psk wlan0 100asktech 3
# Programmers是WIFI AP名字,需要改成你自己的
wapi essid wlan0 Programmers 1
renew wlan0
# 查看IP
ifconfig
# ping互联网, Ctrl+C退出
ping news.qq.com
再执行http命令:
websocket_test
任务间通信功能开发
代码为:ipc_udp.c、ipc_udp.h。
接口函数
我们使用UDP网络通信实现任务之间的数据传输,它有如下优点:
-
无需创建连接,想发送时可以即刻发送
-
通信双方没有依赖,不必等对方先运行,大不了数据丢失
-
不必维护数据缓冲区
ipc_endpoint_t是核心结构体,它的内部成员为:
-
send:发送数据的函数
-
recv:接受数据的函数
-
cb:回调函数,如果提供cb函数,会创建一个线程循环等待、读取数据,读到后使用cb来处理。
结构体定义如下:
/**
* 传输的各方被称为endpoint
* 数据结构体,包含套接字、端口、服务器地址和回调函数
*/
typedef struct ipc_endpoint_t {
void \*priv;
void \*user_data;
transfer_callback_t cb; // 接收到远端的客户端发来的信息后使用它来处理
int (\*send)(struct ipc_endpoint_t \*self, const char \*data, int len); // 发送数据的函数指针
int (\*recv)(struct ipc_endpoint_t \*self, unsigned char \*data, int maxlen, int \*retlen); // 接收数据的函数指针
} ipc_endpoint_t, \*p_ipc_endpoint_t;
接口函数只有2个:
// 创建一个UDP类型的IPC端点
p_ipc_endpoint_t ipc_endpoint_create_udp(int port_local, int port_remote, transfer_callback_t cb, void \*user_data);
// 销毁IPC端点,释放相关资源
void ipc_endpoint_destroy_udp(p_ipc_endpoint_t pendpoint);
使用示例
以arecord为例,它读取到声音数据后要发送给控制中心,它如下初始化、使用IPC:
static p_ipc_endpoint_t g_ipc_ep_audio_upload;
g_ipc_ep_audio_upload = ipc_endpoint_create_udp(0, AUDIO_PORT_UP, NULL, NULL);
g_ipc_ep_audio_upload-\>send(g_ipc_ep_audio_upload, opus_buffer, encoded_bytes);
第2行:创建IPC对象时,只传入了远端端口AUDIO_PORT_UP,以后调用第3行来发送数据时,它内部会把数据通过UPD发送给远端。
以aplay为例,它调用recv函数等待网络数据,读到网络数据后再解码、播发:
static p_ipc_endpoint_t g_ipc_ep_audio_download;
g_ipc_ep_audio_download = ipc_endpoint_create_udp(AUDIO_PORT_DOWN, 0, NULL, NULL);
while (0 != g_ipc_ep_audio_download-\>recv(g_ipc_ep_audio_download, opusbuf,
MAX_PACKET_SIZE, &opuslen));
第2行:创建IPC对象时,只传入了本地端口AUDIO_PORT_DOWN,以后调用第3行来读取数据时,内部就是读取该端口。
以控制中心为例,它要等待arecrod上报的音频数据,接受到之后马上使用websocket发给服务器。创建IPC时可以提供回调函数,这会导致控制中心创建了一个后台线程,它会读网络数据、然后调用回调函数发送给服务器:
static p_ipc_endpoint_t g_ipc_ep_audio;
g_ipc_ep_audio = ipc_endpoint_create_udp(AUDIO_PORT_UP, AUDIO_PORT_DOWN, process_opus_data_uploaded, NULL);
声音程序与控制中心演示
源码位于如下目录:
上传源码
把aplay.c、arecord.c放到如下目录:
/home/ubuntu/vela-opensource/vendor/allwinnertech/chips/r528/drivers/rtos-hal/hal/test/sound
把“control_center”整个目录放到如下目录:
/home/ubuntu/vela-opensource/apps/examples
修改编译脚本
修改“/home/ubuntu/vela-opensource/apps/examples/Kconfig”,添加:
source "/home/ubuntu/vela-opensource/apps/examples/control_center/Kconfig"
修改“/home/ubuntu/vela-opensource/vendor/allwinnertech/chips/r528/drivers/rtos-hal/hal/test/sound/Makefile”,添加:
CFLAGS += \${INCDIR_PREFIX}\$(APPDIR)/examples/control_center/
配置openvela
CONFIG_EXAMPLES_CONTROL_CENTER=y
CONFIG_EXAMPLES_CONTROL_CENTER_STACKSIZE=204800 # 这个栈要大一点
CONFIG_NET_UDP_MAX_CONNS=0 # 不限制UDP socket的数量
使用小智AI机器人
先连接WIFI:
ifup wlan0
wapi mode wlan0 2
wapi psk wlan0 100asktech 3
wapi essid wlan0 Programmers 1
renew wlan0
ifconfig
ping news.qq.com
再设置声卡:
amixer set 19 255
amixer set 20 255
amixer set 21 255
amixer set 6 180
amixer set 7 180
amixer set 15 7
接着运行程序:
arecord -D hw:snddmic -r 48000 -f 16 -c 2 -o &
aplay -D hw:audiocodec -r 48000 -f 16 -c 2 -o &
control_center &
根据提示信息,在<https://xiaozhi.me/>登录后激活设备,即可跟板子进行语音聊天。
声音程序与控制中心代码阅读
代码框架:

arecord
功能:读取声卡麦克风数据,使用opus编码,发送给control_center。

aplay
功能:等待control_center发来声音数据,使用opus解码,播放。

control_center
核心作用有4:
① 创建IPC端点:跟arecord、aplay、GUI程序通信
② 使用HTTP激活、认证设备
③ 建立websocket连接
④ 通过websocket发送音频数据给服务器,接收并处理服务器下发的音频数据、文本数据
先创建2个IPC线程:线程A用于等待arecord上报的音频数据然后发送给服务器,线程B用于等待GUI发来的数据然后处理(为空函数,留待扩展)。

其中“process_opus_data_uploaded”函数为:

然后使用http协议激活、验证设备:

最后启动websocket线程:
① 提供函数给线程A发送音频数据给服务器
② 等待服务器的数据:
对于bin数据(就是音频)直接发送给aplay进程:

对于txt数据则分别处理:
收到hello信息(握手)时,上报设备的iot参数(比如设备上的LED信息等)、通知服务器启动监听(start,开始处理上报的语音)。
收到stt(用户说话后,服务器转换为文字):就发送给GUI显示出来。
收到llm:把表情转发给GUI显示出来。
收到tts(服务器的回复,先传文本):发送给GUI显示出来。
