chore: migrate project into clean repository

This commit is contained in:
yuuux
2026-08-13 16:50:52 +08:00
commit d1d25a09e7
27405 changed files with 9422808 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(listenai-cmake REQUIRED HINTS $ENV{ARCS_BASE})
project(arcs)
listenai_include_directories(
./
)
add_subdirectory(src)
listenai_add_executable(${PROJECT_NAME})
listenai_link_libraries(mbedtls)

View File

@@ -0,0 +1 @@
osource "$ARCS_BASE/Kconfig"

View File

@@ -0,0 +1,368 @@
# HTTP 客户端基础示例
## 功能说明
此示例演示如何使用 LISA HTTP 客户端库进行基本的 HTTP 通信,包括:
- HTTP GET 请求
- HTTP POST 请求(普通表单数据)
- HTTP POST 请求Chunked 传输编码)
示例通过 WiFi 连接到测试服务器,执行多个 HTTP 请求并接收响应,展示了 HTTP 客户端的基本用法。
## 硬件连接
本示例使用芯片内部 WiFi 外设,无需额外接线。
**串口输出:**
- 串口 TX: PA3
- 波特率921600
## 测试环境准备
### 1. 启动 HTTP 测试服务器
在与设备**同一局域网**的 Linux/Mac 主机上运行测试服务器:
```bash
cd samples/network/http
python3 http-server.py
```
服务器启动后将显示:
```
Serving at http://0.0.0.0:8000 (IPv4)
```
### 2. 配置 WiFi 和服务器地址
**重要:** 本示例强制要求在编译前配置 WiFi 和服务器信息,否则会编译失败。
`src/main.c` 中找到配置区域(第 21-27 行),**取消注释**并填写实际配置:
**修改前(默认状态):**
```c
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
// #define SERVER_URL // 修改为测试服务器的 URL例如"http://192.168.1.100:8000"
// #define TARGET_WIFI_SSID // 修改为目标 WiFi 的 SSID
// #define TARGET_WIFI_PWD // 修改为目标 WiFi 的密码
```
**修改后(配置示例):**
```c
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
#define SERVER_URL "http://192.168.1.100:8000" // 测试服务器的 URL
#define TARGET_WIFI_SSID "MyHomeWiFi" // 你的 WiFi 名称
#define TARGET_WIFI_PWD "mypassword123" // 你的 WiFi 密码
```
**操作步骤:**
1. 移除 `SERVER_URL` 前的 `//` 注释符
2.`SERVER_URL` 后面修改为实际的服务器 URL例如"http://192.168.1.100:8000"
3. 移除 `TARGET_WIFI_SSID` 前的 `//` 注释符
4.`TARGET_WIFI_SSID` 后面修改为实际的 WiFi 名称
5. 移除 `TARGET_WIFI_PWD` 前的 `//` 注释符
6.`TARGET_WIFI_PWD` 后面修改为实际的 WiFi 密码
**获取服务器 IP 地址:**
在运行 `http-server.py` 的主机上执行:
```bash
# Linux/Mac
ifconfig | grep "inet "
# 或者
ip addr show | grep "inet "
```
查找类似 `192.168.x.x` 的局域网地址。
## 编译运行
### 编译
```{eval-rst}
.. include:: /sample_build.rst
```
### 烧录
```{eval-rst}
.. include:: /sample_flash.rst
```
## 预期输出
系统启动后,终端将输出以下内容:
### 1. WiFi 连接日志
```
[INFO] HTTP Test Starting...
[INFO] WiFi auto-connect started
[INFO] custom_get_wifi_mac: 0, XX:XX:XX:XX:XX:XX
[INFO] WiFi connection status: 1
[INFO] WiFi connected to AP
[INFO] WiFi event: module=1, id=4
[INFO] Got IP address
[INFO] WiFi connected and IP obtained, starting HTTP tests...
```
### 2. HTTP 测试输出
```
========================================
HTTP 功能测试
========================================
=== 测试 1: HTTP GET 请求 ===
http response : <html><p>Done</p></html>
=== 测试 2: HTTP POST 请求 ===
http response : <html><p>Done</p></html>
=== 测试 3: HTTP POST 请求 (Chunked 编码) ===
http response : <html><p>Done</p></html>
========================================
所有 HTTP 测试完成
========================================
```
### 3. 服务器端日志
服务器端将显示接收到的 POST 请求详情:
```
===== 收到 POST 请求 =====
路径: /foobar
头部:
Host: 192.168.1.100:8000
Content-Length: 6
Body 数据:
foobar
===== 收到 POST 请求 =====
路径: /chunked-test
头部:
Host: 192.168.1.100:8000
Transfer-Encoding: chunked
Body 数据:
6
foobar
7
chunked
4
last
0
```
## 核心 API
| API | 说明 |
|-----|------|
| `lisa_http_init()` | 初始化 HTTP 客户端 |
| `lisa_http_perform()` | 执行 HTTP 请求 |
| `lisa_http_cleanup()` | 清理并释放 HTTP 客户端资源 |
| `wifi_mgr_init()` | 初始化 WiFi 管理器 |
| `wifi_mgr_auto_connect_start()` | 启动自动连接 |
| `ls_event_register_cb()` | 注册 WiFi 事件回调 |
## 关键代码
### 1. HTTP GET 请求
使用 LISA HTTP 库发送 GET 请求:
```c
lisa_http_request_t req;
memset(&req, 0, sizeof(lisa_http_request_t));
req.method = LISA_HTTP_GET;
req.url = SERVER_URL;
req.timeout = 10;
req.on_data = http_data_cb;
req.body = NULL;
req.body_len = 0;
req.headers = NULL;
http = lisa_http_init(&req);
if (!http) {
LOGE("[http] init err\n");
goto ERR;
}
if (lisa_http_perform(http) != 0) {
LOGE("http get request info err..\n");
goto ERR;
}
lisa_http_cleanup(http);
```
### 2. HTTP POST 请求(普通表单数据)
发送普通 POST 数据:
```c
memset(&req, 0, sizeof(lisa_http_request_t));
req.method = LISA_HTTP_POST;
req.url = SERVER_URL "/foobar";
req.timeout = 10;
req.on_data = http_data_cb;
req.body = "foobar";
req.body_len = strlen(req.body);
req.headers = NULL;
http = lisa_http_init(&req);
lisa_http_perform(http);
lisa_http_cleanup(http);
```
### 4. HTTP 响应回调
处理服务器响应数据:
```c
static void http_data_cb(lisa_http_data_t *data)
{
if (!data || !data->buf) {
LOGE("http data is null");
return;
} else if (data->len <= 0) {
LOGE("http data len is 0");
return;
} else {
LOGI("\nhttp response : %s\n", data->buf);
}
}
```
### 5. WiFi 连接和 IP 获取
等待 WiFi 连接成功并获取 IP 地址:
```c
// 注册 ls_event 回调监听 GOT_IP 事件
ls_event_init();
ls_event_register_cb(EVENT_WIFI, EVENT_WIFI_GOT_IP, app_wifi_event_cb, NULL);
// WiFi 事件回调
static int app_wifi_event_cb(void *arg, event_module_t event_module,
int event_id, void *event_data)
{
switch (event_id) {
case EVENT_WIFI_GOT_IP:
LOGI("Got IP address");
g_get_ip_success = true;
break;
}
return 0;
}
// 等待 WiFi 连接和 IP 获取
static int wait_for_wifi_connection(void)
{
while (timeout > 0) {
if (g_wifi_connected && g_get_ip_success) {
LOGI("WiFi connected and IP obtained, starting HTTP tests...");
return 0;
}
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
}
return -1;
}
```
## 配置说明
### HTTP 请求参数
`lisa_http_request_t` 结构体包含以下主要字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `method` | `lisa_http_method_t` | HTTP 方法GET/POST/PUT/DELETE 等) |
| `url` | `const char *` | 完整的 HTTP URL |
| `timeout` | `int` | 请求超时时间(秒) |
| `on_data` | `lisa_http_data_cb_t` | 响应数据回调函数 |
| `body` | `const char *` | 请求 Body 数据POST/PUT |
| `body_len` | `size_t` | Body 数据长度 |
| `headers` | `void *` | 自定义 HTTP 头部(可选) |
### prj.conf 关键配置
```kconfig
# LWIP 网络栈
CONFIG_LWIP=y
CONFIG_LWIP_OPTION_LWIP_DNS=y
CONFIG_LWIP_OPTION_SO_REUSE=y
# WiFi 管理器
CONFIG_WIFI_MANAGER=y
# HTTP 客户端
CONFIG_MODULE_HTTPCLIENT=y
# LISA 组件
CONFIG_LISA_OS=y
CONFIG_LISA_KV=y
CONFIG_LISA_NETWORK=y
CONFIG_LISA_HTTP=y
# 堆内存配置
CONFIG_HEAP_SIZE=0x3C00 # 主堆: 15KB
CONFIG_PSRAM_HEAP_SIZE=0x5b0000 # PSRAM 堆: 5.6MB
```
## HTTP 方法说明
LISA HTTP 库支持以下 HTTP 方法:
| 方法 | 枚举值 | 用途 |
|------|--------|------|
| GET | `LISA_HTTP_GET` | 获取资源 |
| POST | `LISA_HTTP_POST` | 提交数据 |
| PUT | `LISA_HTTP_PUT` | 更新资源 |
| DELETE | `LISA_HTTP_DELETE` | 删除资源 |
| HEAD | `LISA_HTTP_HEAD` | 获取响应头 |
## 注意事项
1. **编译前配置(重要)**:首次编译前**必须**取消注释并修改 `src/main.c` 中的 WiFi 和服务器配置,否则会触发编译错误:
```
main.c:31:2: error: #error "请修改 SERVER_URL 为实际的测试服务器 URL例如http://192.168.1.100:8000"
main.c:35:2: error: #error "请修改 TARGET_WIFI_SSID 为实际的 WiFi SSID"
main.c:39:2: error: #error "请修改 TARGET_WIFI_PWD 为实际的 WiFi 密码"
```
这是为了防止使用默认配置导致连接失败。配置方法请参考上方的"配置 WiFi 和服务器地址"章节。
2. **网络环境**:确保设备与测试服务器在同一局域网内,避免跨网段访问
3. **服务器地址**`SERVER_URL` 必须包含完整的 URL协议 + IP + 端口),例如:"http://192.168.1.100:8000"
4. **WiFi 配置**:将 `TARGET_WIFI_SSID` 和 `TARGET_WIFI_PWD` 修改为实际的 WiFi SSID 和密码
5. **超时设置**HTTP 超时设置为 10 秒,如果网络较慢可适当增加 `req.timeout` 值
6. **响应处理**`on_data` 回调函数会在接收到响应数据时被调用,可能会被多次调用(分块接收)
7. **资源清理**:每次 HTTP 请求完成后必须调用 `lisa_http_cleanup()` 释放资源,避免内存泄漏
8. **Python 版本**:测试服务器需要 Python 3.6 或更高版本
## 相关文档
- [LISA WiFi 组件文档](../../../components/lisa_wifi/README.md) - WiFi 初始化和连接
- [WiFi Manager 文档](../../../modules/wifi_manager/README.md) - WiFi 连接管理
- [LISA HTTP 组件文档](../../../components/lisa_http/README.md) - HTTP 客户端详细说明

View File

@@ -0,0 +1,177 @@
#!/bin/bash
set -e
usage() {
echo "使用方式: $0 [选项]"
echo "选项:"
echo " -S, --Source <path> 指定项目源码路径 (默认为当前脚本所在目录)"
echo " -t, --target <target> 指定构建目标 (如 menuconfig)"
echo " -C, --Clean 清理构建目录"
echo " -B, --build 构建输出目录"
echo " -h, --help 显示此帮助信息"
echo " -r, --release 以 Release 模式构建 (移除 DEBUG_PATH 信息)"
echo " -w, --warnings-as-errors 将警告视为错误"
echo " -D<var>=<value> 传递 CMake 变量 (可多次使用)"
echo ""
echo "示例:"
echo " $0 -S samples/helloworld -DBOARD=arcs_mini 指定板型构建"
echo " $0 -S samples/helloworld -DBOARD=arcs_evb 使用 EVB 板型"
echo " $0 -S samples/helloworld -t menuconfig -DBOARD=arcs_mini 运行 menuconfig"
echo " $0 -C -S samples/helloworld -DBOARD=arcs_mini 清理并重新构建"
echo " $0 -S samples/helloworld -DBOARD=my_board -DBOARD_SEARCH_PATH=/path/to/boards 使用自定义板型"
exit 1
}
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_PATH="$SCRIPT_DIR"
TARGET=""
CLEAN=false
OUTPUT="build"
WARNINGS_AS_ERRORS=false
RELEASE=false
ARCS_BASE_DIR_NAME="arcs-sdk"
ARCS_DEV_TOOLS_DIR_NAME="listenai-dev-tools"
ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME="gcc"
ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME="listenai-tools"
find_arcs_base() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_BASE_DIR_NAME"
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found ARCS_BASE: $current_dir/$dir_name"
export ARCS_BASE="$current_dir/$dir_name"
return 0
fi
current_dir=$(dirname "$current_dir")
done
echo "ARCS_BASE not found, Please add ARCS_BASE environment variable or set ARCS_BASE_DIR_NAME to the correct directory."
echo "Current Target ARCS_BASE directory name: $ARCS_BASE_DIR_NAME."
exit 1
}
find_dev_tools() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_DEV_TOOLS_DIR_NAME"
echo "trying to find $dir_name in parent directories..."
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found $dir_name: $current_dir/$dir_name"
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME" ]; then
echo "Found LISTENAI_TOOLS_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
export LISTENAI_TOOLS_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
fi
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME" ]; then
echo "Found NUCLEI_TOOLCHAIN_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
export NUCLEI_TOOLCHAIN_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
fi
return 0
fi
current_dir=$(dirname "$current_dir")
done
}
while [[ $# -gt 0 ]]; do
case $1 in
-S|--Source)
PROJECT_PATH="$2"
shift 2
;;
-B|--build)
OUTPUT="$2"
shift 2
;;
-t|--target)
TARGET="$2"
shift 2
;;
-C|--Clean)
CLEAN=true
shift 1
;;
-w|--warnings-as-errors)
WARNINGS_AS_ERRORS=true
shift 1
;;
-h|--help)
usage
;;
-r|--release)
RELEASE=true
shift 1
;;
-D*)
CMAKE_VARS+=("$1")
shift 1
;;
*)
echo "未知参数: $1"
usage
;;
esac
done
echo "Source: $PROJECT_PATH"
echo "Target: $TARGET"
echo "Clean : $CLEAN"
if [ -z "$LISTENAI_TOOLS_PATH" ] || [ -z "$NUCLEI_TOOLCHAIN_PATH" ]; then
find_dev_tools
fi
if [ -z "${LISTENAI_TOOLS_PATH}" ]; then
export LISTENAI_TOOLS_PATH="请添加 LISTENAI_TOOLS_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 LISTENAI_TOOLS_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
if [ -z "${NUCLEI_TOOLCHAIN_PATH}" ]; then
export NUCLEI_TOOLCHAIN_PATH="请添加 NUCLEI_TOOLCHAIN_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 NUCLEI_TOOLCHAIN_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
############### 下面代码不用修改 ##################
# 构建工具的位置
CMAKE_PROGRAM="$LISTENAI_TOOLS_PATH/cmake/bin/cmake"
NINJA_PROGRAM="$LISTENAI_TOOLS_PATH/ninja/ninja"
# 配置环境变量 ARCS_BASE
if [ -z "$ARCS_BASE" ]; then
find_arcs_base
fi
if [ "$CLEAN" = true ]; then
rm -rf $OUTPUT
fi
# Initialize CMAKE_VARS array if it doesn't exist
declare -a CMAKE_VARS
# Add warnings-as-errors flag if enabled
if [ "$WARNINGS_AS_ERRORS" = true ]; then
CMAKE_VARS+=("-DCMAKE_C_FLAGS=-Werror")
CMAKE_VARS+=("-DCMAKE_CXX_FLAGS=-Werror")
echo "Treating warnings as errors"
fi
# Add release flags if enabled
if [ "$RELEASE" = true ]; then
CMAKE_VARS+=("-DENABLE_DEBUG_PATH=OFF")
echo "Release mode enabled (-DENABLE_DEBUG_PATH=OFF)"
fi
$CMAKE_PROGRAM -B "$OUTPUT" -G Ninja -S "$PROJECT_PATH" \
-DCMAKE_MAKE_PROGRAM="$NINJA_PROGRAM" \
"${CMAKE_VARS[@]}"
if [ -z "$TARGET" ]; then
$CMAKE_PROGRAM --build "$OUTPUT" -j4
else
$CMAKE_PROGRAM --build "$OUTPUT" --target "$TARGET"
fi

View File

@@ -0,0 +1,43 @@
#!/usr/bin/python
import socket
from http.server import HTTPServer
from http.server import SimpleHTTPRequestHandler
PORT = 8080
class RequestHandler(SimpleHTTPRequestHandler):
length = 0
def _set_headers(self):
self.send_response(200)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', str(self.length))
self.end_headers()
def do_POST(self):
# 1. 打印请求信息
print("\n===== 收到 POST 请求 =====")
print(f"路径: {self.path}")
print("头部:")
for header, value in self.headers.items():
print(f" {header}: {value}")
# 2. 读取请求 body 并打印
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length)
print("Body 数据:")
print(post_data.decode('utf-8'))
payload = "<html><p>Done</p></html>"
self.length = len(payload)
self._set_headers()
self.wfile.write(payload.encode())
def main():
httpd = HTTPServer(("0.0.0.0", PORT), RequestHandler)
print(f"Serving at http://0.0.0.0:{PORT} (IPv4)")
httpd.serve_forever()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,148 @@
# 项目可根据需要在此文件中设置对应的配置
CONFIG_ARCS_HAL_WARNING_DISABLE=y
CONFIG_SDK_MODULE_EASYLOGGER=n
# CONFIG_ARCS_HAL_DEBUG=y
CONFIG_MODULE_FREERTOS=y
CONFIG_MODULE_HEAP=y
CONFIG_LINK_OPTION_NOSYS_SPECS=y
CONFIG_EASYLOGGER_ASYNC_BUF_SIZE=0x1000
# DISK驱动使能
CONFIG_DISK_DRIVER=y
#using mbedtls for hardware decoding(IF CONFIG_WX_SQLITE3_HARD_CODEC)
CONFIG_SDK_MODULE_MBEDTLS=y
CONFIG_CUSTOM_MBEDTLS_CFG_FILE=y
CONFIG_MBEDTLS_CFG_FILE="config-tls-generic.h"
CONFIG_MBEDTLS_HARDWARE_ENTROPY=y
# CONFIG_MBEDTLS_HARDWARE_AES=y
# CONFIG_MBEDTLS_HARDWARE_SHA1=y
# CONFIG_MBEDTLS_HARDWARE_SHA256=y
# CONFIG_MBEDTLS_HARDWARE_SHA512=y
# CONFIG_MBEDTLS_HARDWARE_AES_CCM=y
# CONFIG_MBEDTLS_HARDWARE_AES_GCM=y
# CONFIG_MBEDTLS_HARDWARE_ECC=y
# System support
CONFIG_MBEDTLS_HAVE_ASM=y
CONFIG_MBEDTLS_HAVE_TIME_DATE=y
CONFIG_MBEDTLS_SSL_ALPN=y
# Save RAM at the expense of ROM
CONFIG_MBEDTLS_AES_ROM_TABLES=y
# mbed TLS feature support
CONFIG_MBEDTLS_CIPHER_MODE_CBC_ENABLED=y
CONFIG_MBEDTLS_KEY_EXCHANGE_RSA_ENABLED=y
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED=y
# Supported TLS versions
CONFIG_MBEDTLS_TLS_VERSION_1_0=y
CONFIG_MBEDTLS_TLS_VERSION_1_1=y
CONFIG_MBEDTLS_TLS_VERSION_1_2=y
# mbed TLS modules
CONFIG_MBEDTLS_CIPHER_AES_ENABLED=y
CONFIG_MBEDTLS_CIPHER=y
CONFIG_MBEDTLS_CTR_DRBG_ENABLED=y
CONFIG_MBEDTLS_ENTROPY_ENABLED=y
CONFIG_MBEDTLS_MD=y
CONFIG_MBEDTLS_MAC_MD5_ENABLED=y
CONFIG_MBEDTLS_HASH_MD5_ENABLED=y
CONFIG_MBEDTLS_MAC_SHA1_ENABLED=y
CONFIG_MBEDTLS_HASH_SHA1_ENABLED=y
CONFIG_MBEDTLS_MAC_SHA256_ENABLED=y
CONFIG_MBEDTLS_SHA256_SMALLER=y
CONFIG_MBEDTLS_HASH_SHA256_ENABLED=y
CONFIG_MBEDTLS_DTLS=y
CONFIG_MBEDTLS_SERVER_NAME_INDICATION=y
CONFIG_MBEDTLS_ECDH_C=y
CONFIG_MBEDTLS_ECP_C=y
CONFIG_MBEDTLS_CIPHER_GCM_ENABLED=y
CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y
CONFIG_MBEDTLS_MAC_SHA512_ENABLED=y
CONFIG_MBEDTLS_HASH_SHA512_ENABLED=y
CONFIG_MBEDTLS_PEM_CERTIFICATE_FORMAT=y
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384
CONFIG_MBEDTLS_CTR_DRBG_ENABLED=y
# CONFIG_MBEDTLS_DEBUG=y
CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=n
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y
# CONFIG_ARCS_HAL_DEBUG=y
CONFIG_MODULE_HEAP=y
CONFIG_LOG=y
CONFIG_SYSLOG_PRINTK_REDIRECT=y
CONFIG_SYSLOG_UART_TX_PIN=3
CONFIG_SYSLOG_UART_TX_PIN_FUNC_SEL=2
CONFIG_SYSLOG_UART_PORT=0
CONFIG_SYSLOG_UART_BAUDRATE=921600
CONFIG_LWIP=y
CONFIG_WIFI=y
CONFIG_WIFI_TX_BUF_COPY=y
CONFIG_WIFI_LWIP_SAME_CORE=y
CONFIG_LWIP_TX_BUF_COPY=y
CONFIG_LWIP_OPTION_LWIP_DNS=y
CONFIG_LWIP_OPTION_SO_REUSE=y
CONFIG_FREERTOS_STATIC_ALLOCATION_ENABLE=y
CONFIG_DCACHE_ENABLE=y
CONFIG_MODULE_ZE_TLS=y
CONFIG_MODULE_HTTPCLIENT=y
CONFIG_MODULE_NOPOLL=y
CONFIG_MODULE_CORESNTP=y
CONFIG_LWIP_OPTION_SO_REUSE=y
CONFIG_LWIP_OPTION_LWIP_DNS=y
CONFIG_HEAP_SIZE=0x3C00
CONFIG_PSRAM_HEAP_SIZE=0x5b0000
# 文件系统配置
CONFIG_FILE_SYSTEM=y
CONFIG_FATFS_FILESYSTEM=y
# 抽象文件系统
CONFIG_LSFS=y
CONFIG_LSFS_FAT=y
CONFIG_LSFS_REGISTER_VFS=y
CONFIG_LVFS_POSIX_API_ALIAS=y
CONFIG_LVFS=y
CONFIG_LVFS_POSIX_API=y
CONFIG_FS_ENV_MEMORY_MANAGEMENT_USE_PSRAM=y
# LISA WIFI
CONFIG_LISA_WIFI=y
# MAC Manager
CONFIG_MAC_MANAGER=y
CONFIG_MAC_MANAGER_MAC_BY_CHIP_ID=y
#WIFI MANAGER
CONFIG_WIFI_MANAGER=y
# LISA Shell
CONFIG_LISA_SHELL=y
# LISA Porting
CONFIG_LISA_PORTING=y
# SD Card drivers
CONFIG_DISK_DRIVER_SDMMC=y
CONFIG_LISA_SDMMC_DEVICE=y
CONFIG_CJSON=y
CONFIG_LISA_OS=y
CONFIG_LISA_KV=y
CONFIG_LISA_NETWORK=y
CONFIG_SDK_MODULE_ICO=y
CONFIG_LISA_HTTP=y

View File

@@ -0,0 +1,3 @@
tests:
samples.http:
build_only: true

View File

@@ -0,0 +1,10 @@
listenai_library_named(app)
listenai_library_sources(
main.c
fs/user_fs.c
)
listenai_include_directories(
./
./fs
)

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lsfs.h"
#include "disk/disk_access.h"
#include "lisa_sdmmc.h"
#include "log_print.h"
#if CONFIG_LVFS
#include "lvfs.h"
#endif
// #define FLASHDISK_DEVICE "NAND:"
#define DISK_DEVICE "SD:"
#define DISK_MOUNT_POINT "/" DISK_DEVICE
static struct lsfs_mount_t flash_lsfs_mnt = {
.type = LSFS_FATFS,
.mnt_point = DISK_MOUNT_POINT,
.fs_data = NULL,
};
static int fs_mount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_mount(mp);
if (ret != 0)
{
CLOG("No file system, try reformatting, mount ret: %d", ret);
/* 格式化底层设备,去掉挂载点前面的'/' 作为设备名,例如 "/SD:" -> "SD:" */
ret = lsfs_mkfs(mp->type, &mp->mnt_point[1], NULL, 0);
if (ret == 0)
{
CLOG("mkfs success, retry mount");
ret = lsfs_mount(mp);
}
else
{
CLOG("mkfs failed, ret: %d", ret);
}
}
if (ret != 0)
{
CLOG("Failed to mount filesystem: %d", ret);
return ret;
}
CLOG("%s mounted successfully", mp->mnt_point);
return 0;
}
static int fs_unmount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_unmount(mp);
if (ret != 0)
{
CLOG("%s unmount failed!\n", mp->mnt_point);
}
CLOG("%s unmount success!\n", mp->mnt_point);
}
static int list_dir(const char *path)
{
struct lsfs_dir_t dir;
struct lsfs_dirent entry;
int ret;
int nfile = 0, ndir = 0;
/* 初始化目录对象 */
lsfs_dir_t_init(&dir);
ret = lsfs_opendir(&dir, path);
CLOG("%s opendir: %d", path, ret);
if (ret == 0)
{
while (1)
{
ret = lsfs_readdir(&dir, &entry);
if (ret != 0 || entry.name[0] == 0)
break; /* Error or end of dir */
if (entry.type == LSFS_DIR_ENTRY_DIR)
{
/* Directory */
CLOG(" <DIR> %s", entry.name);
ndir++;
}
else
{
/* File */
CLOG("%10lu %s", entry.size, entry.name);
nfile++;
}
}
lsfs_closedir(&dir);
CLOG("%d dirs, %d files.", ndir, nfile);
}
else
{
CLOG("Failed to open \"%s\". (%u)", path, ret);
}
return ret;
}
static void user_fs_change_to_root_folder(void)
{
int ret;
ret = lvfs_chdrive(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change drive to %s, error: %d\n", DISK_MOUNT_POINT, ret);
}
ret = lvfs_chdir(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change directory to /SD:/, error: %d\n", ret);
} else {
CLOG("Changed to directory /SD:/\n");
}
}
int user_fs_init(void){
lisa_sdmmc_probe(lisa_device_get("sdmmc0"));
disk_init(NULL);
#if CONFIG_LVFS
lvfs_init();
#endif
lsfs_init();
if(0 != fs_mount(&flash_lsfs_mnt)){
return -1;
}
user_fs_change_to_root_folder();
return 0;
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
int user_fs_init(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,305 @@
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "stdbool.h"
#include "assert.h"
#include "FreeRTOS.h"
#include "task.h"
#define TAG "http-test"
#include "lisa_log.h"
#include "lisa_kv.h"
#include "mac_manager.h"
#include "mac_manager_ops.h"
#include "wifi_manager/wifi_manager.h"
#include "user_fs.h"
#include "lisa_wifi.h"
#include "ls_wifi_type.h"
#include "net_al.h"
#include "net_def.h"
#include "lisa_http.h"
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
#define SERVER_URL "http://192.168.1.100:8080"
#define TARGET_WIFI_SSID "your_wifi_ssid"
#define TARGET_WIFI_PWD "your_wifi_pwd"
// 编译时检查:确保用户已修改默认配置
#ifndef SERVER_URL
#error "请修改 SERVER_URL 为实际的测试服务器 URL例如http://192.168.1.100:8080"
#endif
#ifndef TARGET_WIFI_SSID
#error "请修改 TARGET_WIFI_SSID 为实际的 WiFi SSID"
#endif
#ifndef TARGET_WIFI_PWD
#error "请修改 TARGET_WIFI_PWD 为实际的 WiFi 密码"
#endif
#define WIFI_MGR_AUTO_CONNECT_INTERVAL_MS 2000
#define WIFI_MGR_SEARCH_AP_BUFFER_SIZE 10
#define HTTP_TEST_HEAD "Transfer-Encoding: chunked\r\n"
static mac_manager_t *m_mac_manager;
static wifi_mgr_sta_config_t list[WIFI_MGR_SEARCH_AP_BUFFER_SIZE] = { 0 };
static int8_t custom_get_wifi_mac(uint8_t mac_addr[6])
{
int8_t ret = 0;
if (!mac_addr)
return -1;
ret = mac_manager_get(m_mac_manager, mac_addr, 6);
LOGI("custom_get_wifi_mac: %d, %02X:%02X:%02X:%02X:%02X:%02X", ret, mac_addr[0], mac_addr[1], mac_addr[2],
mac_addr[3], mac_addr[4], mac_addr[5]);
return ret;
}
static volatile bool g_wifi_connected = false;
static volatile bool g_get_ip_success = false;
static void dhcp_status_callback(int vif_idx, bool success, uint32_t ip_addr, uint32_t netmask, uint32_t gateway, void *arg)
{
if (success) {
LOGI("DHCP Success on VIF-%d: IP=%d.%d.%d.%d, Mask=%d.%d.%d.%d, GW=%d.%d.%d.%d",
vif_idx,
ip_addr & 0xff, (ip_addr >> 8) & 0xff, (ip_addr >> 16) & 0xff, (ip_addr >> 24) & 0xff,
netmask & 0xff, (netmask >> 8) & 0xff, (netmask >> 16) & 0xff, (netmask >> 24) & 0xff,
gateway & 0xff, (gateway >> 8) & 0xff, (gateway >> 16) & 0xff, (gateway >> 24) & 0xff);
g_get_ip_success = true;
} else {
LOGI("DHCP Failed on VIF-%d", vif_idx);
}
}
static void wifi_mgr_connection_status_cb(wifi_mgr_connection_info_t *connection_info, void *arg)
{
net_if_t *net_if;
LOGI("WiFi connection status: %d", connection_info->status);
switch (connection_info->status) {
case WIFI_MGR_STA_CONNECTED:
LOGI("WiFi connected to AP");
g_wifi_connected = true;
net_if = net_if_get(WIFI_VIF_STA_IDX);
net_if_up(net_if);
if (!net_if->static_ip) {
ls_dhcpc_start(WIFI_VIF_STA_IDX);
}
break;
case WIFI_MGR_STA_DISCONNECTED:
LOGI("WiFi disconnected from AP");
g_wifi_connected = false;
ls_dhcpc_stop(WIFI_VIF_STA_IDX);
net_if_down(net_if_get(WIFI_VIF_STA_IDX));
break;
default:
break;
}
}
static void user_mac_manager_init(void)
{
mac_manager_config_t config = {
.random_mac_if_mac_invalid = false,
};
m_mac_manager = mac_manager_init(mac_manager_ops_get()->mem_ops, mac_manager_ops_get()->content_ops, &config);
if (m_mac_manager == NULL) {
LOGI("mac_manager_init failed\n");
assert(0);
}
}
static void user_wifi_manager_init(void)
{
wifi_mgr_sta_config_t cfg = {
.ssid = TARGET_WIFI_SSID,
.pwd = TARGET_WIFI_PWD,
};
wifi_mgr_autoconn_config_t autoconn_cfg = {
.interval_ms = WIFI_MGR_AUTO_CONNECT_INTERVAL_MS,
};
wifi_mgr_init(wifi_mgr_ops_get());
wifi_mgr_sta_enable();
// 注册 WiFi 连接状态回调
wifi_mgr_sta_add_connection_cb(wifi_mgr_connection_status_cb, NULL);
/* 搜索并删除所有已保存的 AP */
int count = wifi_mgr_storage_search_ap(list, WIFI_MGR_SEARCH_AP_BUFFER_SIZE, SEARCH_ALL, NULL);
for (int i = 0; i < count; i++) {
LOGI("Deleting saved AP: ssid=%s", list[i].ssid);
wifi_mgr_storage_delete_ap(&list[i]);
}
// 保存 AP 配置并启动自动连接
wifi_mgr_storage_save_ap(&cfg);
wifi_mgr_auto_connect_start(&autoconn_cfg);
LOGI("WiFi auto-connect started");
}
static int wait_for_wifi_connection(void)
{
int timeout = 30; // 30 秒超时
LOGI("Waiting for WiFi connection and IP address...");
// 等待 WiFi 连接和 IP 获取
while (timeout > 0) {
if (g_wifi_connected && g_get_ip_success) {
LOGI("WiFi connected and IP obtained, starting HTTP tests...");
vTaskDelay(pdMS_TO_TICKS(500)); // 短暂延迟确保网络稳定
return 0;
}
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
if (timeout % 5 == 0) {
LOGI("Waiting... (WiFi:%d, IP:%d, timeout:%d)", g_wifi_connected, g_get_ip_success, timeout);
}
}
LOGI("WiFi connection or IP acquisition timeout (WiFi:%d, IP:%d)", g_wifi_connected, g_get_ip_success);
return -1;
}
static void http_data_cb(lisa_http_data_t *data)
{
if (!data || !data->buf) {
LOGE("http data is null");
return;
} else if (data->len <= 0) {
LOGE("http data len is 0");
return;
} else {
LOGI("\nhttp response : %s\n", data->buf);
}
}
static void *http_request_heads(void)
{
return HTTP_TEST_HEAD;
}
int main(int argc, char **argv)
{
int ret;
lisa_http_t *http = NULL;
LOGI("HTTP Test Starting...");
// 初始化 MAC 管理器
user_mac_manager_init();
// 注册 DHCP 状态回调
net_dhcp_register_status_callback(dhcp_status_callback, NULL);
// 初始化 LISA WiFi
lisa_wifi_ops_t ops = {
.custom_mac = custom_get_wifi_mac,
};
lisa_wifi_init(&ops);
// 初始化文件系统和 KV 存储
user_fs_init();
lisa_kv_init();
// 初始化 WiFi 管理器并启动自动连接
user_wifi_manager_init();
// 等待 WiFi 连接成功
ret = wait_for_wifi_connection();
if (ret != 0) {
LOGI("Failed to connect to WiFi");
return -1;
}
LOGI("\n========================================");
LOGI("HTTP 功能测试");
LOGI("========================================\n");
// 测试 1: HTTP GET 请求
LOGI("=== 测试 1: HTTP GET 请求 ===");
lisa_http_request_t req;
memset(&req, 0, sizeof(lisa_http_request_t));
req.method = LISA_HTTP_GET;
req.url = SERVER_URL;
req.timeout = 10;
req.on_data = http_data_cb;
req.body = NULL;
req.body_len = 0;
req.headers = NULL;
http = lisa_http_init(&req);
if (!http) {
LOGE("[http] init err\n");
goto ERR;
}
if (lisa_http_perform(http) != 0) {
LOGE("http get request info err..\n");
goto ERR;
}
if (http) {
lisa_http_cleanup(http);
http = NULL;
}
vTaskDelay(pdMS_TO_TICKS(1000));
// 测试 2: HTTP POST 请求
LOGI("\n=== 测试 2: HTTP POST 请求 ===");
memset(&req, 0, sizeof(lisa_http_request_t));
req.method = LISA_HTTP_POST;
req.url = SERVER_URL "/foobar";
req.timeout = 10;
req.on_data = http_data_cb;
req.body = "foobar";
req.body_len = strlen(req.body);
req.headers = NULL;
http = lisa_http_init(&req);
if (!http) {
LOGE("[http] init err\n");
goto ERR;
}
if (lisa_http_perform(http) != 0) {
LOGE("http post request info err..\n");
goto ERR;
}
if (http) {
lisa_http_cleanup(http);
http = NULL;
}
LOGI("\n========================================");
LOGI("所有 HTTP 测试完成");
LOGI("========================================\n");
ERR:
if (http) {
lisa_http_cleanup(http);
http = NULL;
}
return 0;
}

View File

@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(listenai-cmake REQUIRED HINTS $ENV{ARCS_BASE})
project(arcs)
listenai_include_directories(
./
)
add_subdirectory(src)
listenai_add_executable(${PROJECT_NAME})
listenai_link_libraries(mbedtls)

View File

@@ -0,0 +1 @@
osource "$ARCS_BASE/Kconfig"

View File

@@ -0,0 +1,427 @@
# HTTP 下载性能测试示例
## 功能说明
此示例演示如何测试 HTTP 下载性能,通过不同的缓冲区大小和文件大小组合,评估网络下载的最佳配置。
示例使用 XRADIO HTTP 客户端库进行流式下载,实时统计下载速度、数据块分布等性能指标,并生成详细的性能测试报告。
### 测试方案
**测试维度:**
- **文件大小测试**10MB、50MB、100MB 三种文件大小
- **缓冲区大小测试**2KB、4KB、8KB、16KB、32KB、64KB、128KB 多种缓冲区配置
**性能指标:**
- 平均下载速度KB/s 和 Mbps
- 总下载时间
- 数据块统计(数量、最小/最大/平均大小)
- 不同文件大小下的最佳缓冲区配置
## 硬件连接
本示例使用芯片内部 WiFi 外设,无需额外接线。
**串口输出:**
- 串口 TX: PA3
- 波特率921600
## 测试环境准备
### 1. 启动 HTTP 测试服务器
在与设备**同一局域网**的 Linux/Mac 主机上运行测试服务器:
```bash
cd samples/network/http_download_perf
python3 http-server.py
```
服务器启动后将显示:
```
========================================
ARCS HTTP 测试服务器
========================================
监听地址: http://0.0.0.0:8080
默认文件大小: 5MB
最大文件大小: 100MB
使用示例:
下载默认大小: http://0.0.0.0:8080/random
下载 10MB: http://0.0.0.0:8080/random?size=10
下载 50MB: http://0.0.0.0:8080/random?size=50
========================================
```
### 2. 配置 WiFi 和服务器地址
**重要:** 本示例强制要求在编译前配置 WiFi 和服务器信息,否则会编译失败。
`src/main.c` 中找到配置区域(第 21-28 行),**取消注释**并填写实际配置:
**修改前(默认状态):**
```c
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
// #define SERVER_HOST // 修改为测试服务器的 IP 地址,例如:"192.168.1.100"
#define SERVER_PORT 8080 // 修改为测试服务器的端口号,默认 8080
// #define TARGET_WIFI_SSID // 修改为目标 WiFi 的 SSID
// #define TARGET_WIFI_PWD // 修改为目标 WiFi 的密码
```
**修改后(配置示例):**
```c
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
#define SERVER_HOST "192.168.1.100" // 测试服务器的局域网 IP
#define SERVER_PORT 8080 // 测试服务器端口
#define TARGET_WIFI_SSID "MyHomeWiFi" // 你的 WiFi 名称
#define TARGET_WIFI_PWD "mypassword123" // 你的 WiFi 密码
```
**操作步骤:**
1. 移除 `SERVER_HOST` 前的 `//` 注释符
2.`SERVER_HOST` 后面修改为实际的服务器 IP例如"192.168.1.100"
3. 移除 `TARGET_WIFI_SSID` 前的 `//` 注释符
4.`TARGET_WIFI_SSID` 后面修改为实际的 WiFi 名称
5. 移除 `TARGET_WIFI_PWD` 前的 `//` 注释符
6.`TARGET_WIFI_PWD` 后面修改为实际的 WiFi 密码
**获取服务器 IP 地址:**
在运行 `http-server.py` 的主机上执行:
```bash
# Linux/Mac
ifconfig | grep "inet "
# 或者
ip addr show | grep "inet "
```
查找类似 `192.168.x.x` 的局域网地址。
## 编译运行
### 编译
```{eval-rst}
.. include:: /sample_build.rst
```
### 烧录
```{eval-rst}
.. include:: /sample_flash.rst
```
## 预期输出
系统启动后,终端将输出以下内容:
### 1. WiFi 连接日志
```
[INFO] HTTP Performance Test Starting...
[INFO] WiFi auto-connect started
[INFO] custom_get_wifi_mac: 0, XX:XX:XX:XX:XX:XX
[INFO] WiFi connection status: 1
[INFO] WiFi connected to AP
[INFO] WiFi event: module=1, id=4
[INFO] Got IP address
[INFO] WiFi connected and IP obtained, starting HTTP tests...
```
### 2. 性能测试进度
每个测试显示实时下载进度:
```
========================================
10MB - 2KB 缓冲区
缓冲区大小: 2.00 KB (2048 bytes)
========================================
URL: http://192.168.1.100:8080/random?size=10
========== 开始下载 ==========
注意: 数据以小块方式接收,不会占用大量内存
下载中: 1.23 MB (634.25 KB/s) | 已接收 634 个数据块
下载中: 2.56 MB (681.19 KB/s) | 已接收 1319 个数据块
下载中: 4.02 MB (749.88 KB/s) | 已接收 2067 个数据块
...
========== 下载完成 ==========
总大小: 10.00 MB (10485760 bytes)
总时间: 14235 ms (14.24 秒)
平均速度: 719.67 KB/s (5.62 Mbps)
--- 数据块统计 ---
总数据块数: 5120
数据块大小: 最小=1.46 KB, 最大=2.00 KB, 平均=2.00 KB
================================
```
### 3. 性能测试汇总报告
所有测试完成后生成汇总报告:
```
╔════════════════════════════════════════════════════════════════╗
║ HTTP 下载性能测试汇总报告 ║
╚════════════════════════════════════════════════════════════════╝
序号 | 文件大小 | 缓冲区 | 平均速度 | 总时间 | 数据块数
-----|----------|----------|---------------|-----------|----------
1 | 10MB | 2KB | 719.67 KB/s | 14.24s | 5120
2 | 10MB | 8KB | 856.34 KB/s | 11.95s | 1280
3 | 10MB | 16KB | 923.45 KB/s | 11.09s | 640
4 | 10MB | 32KB | 981.23 KB/s | 10.43s | 320
5 | 10MB | 64KB | 1024.56 KB/s | 10.00s | 160
6 | 10MB | 128KB | 1067.89 KB/s | 9.59s | 80
7 | 50MB | 2KB | 734.21 KB/s | 69.87s | 25600
8 | 50MB | 16KB | 945.67 KB/s | 54.23s | 3200
9 | 50MB | 64KB | 1089.34 KB/s | 47.08s | 800
10 | 50MB | 128KB | 1124.78 KB/s | 45.58s | 400
11 | 100MB | 16KB | 967.45 KB/s | 105.92s | 6400
12 | 100MB | 64KB | 1102.34 KB/s | 93.02s | 1600
13 | 100MB | 128KB | 1145.89 KB/s | 89.46s | 800
═══════════════════════════════════════════════════════════════
性能分析
═══════════════════════════════════════════════════════════════
• 10MB 文件: 最佳缓冲区 = 128KB, 速度 = 1067.89 KB/s (8.34 Mbps)
• 50MB 文件: 最佳缓冲区 = 128KB, 速度 = 1124.78 KB/s (8.79 Mbps)
• 100MB 文件: 最佳缓冲区 = 128KB, 速度 = 1145.89 KB/s (8.95 Mbps)
═══════════════════════════════════════════════════════════════
========================================
所有测试完成
========================================
```
## 核心 API
| API | 说明 |
|-----|------|
| `HTTPC_open()` | 打开 HTTP 连接 |
| `HTTPC_request()` | 发送 HTTP 请求 |
| `HTTPC_read()` | 流式读取响应数据 |
| `HTTPC_close()` | 关闭 HTTP 连接 |
| `wifi_mgr_init()` | 初始化 WiFi 管理器 |
| `wifi_mgr_auto_connect_start()` | 启动自动连接 |
| `ls_event_register_cb()` | 注册 WiFi 事件回调 |
## 关键代码
### 1. 流式 HTTP 下载
使用流式读取,避免大文件占用过多内存:
```c
// 分配响应缓冲区
response_buf = (CHAR *)malloc(buffer_size);
// 初始化 HTTP 参数
memset(&http_params, 0, sizeof(HTTPParameters));
snprintf(http_params.Uri, HTTP_CLIENT_MAX_URL_LENGTH,
"http://%s:%d%s", SERVER_HOST, SERVER_PORT, uri);
http_params.HttpVerb = VerbGet;
http_params.nTimeout = 120;
// 打开连接并发送请求
HTTPC_open(&http_params);
HTTPC_request(&http_params, NULL);
// 流式读取数据
while (1) {
ret = HTTPC_read(&http_params, response_buf, buffer_size, &recv_size);
if (ret != 0 || recv_size <= 0) {
break; // 读取完成或出错
}
// 更新下载统计
update_download_stats(recv_size);
}
// 关闭连接
HTTPC_close(&http_params);
free(response_buf);
```
### 2. WiFi 连接和 IP 获取
等待 WiFi 连接成功并获取 IP 地址:
```c
// 注册 ls_event 回调监听 GOT_IP 事件
ls_event_init();
ls_event_register_cb(EVENT_WIFI, EVENT_WIFI_GOT_IP, app_wifi_event_cb, NULL);
// WiFi 事件回调
static int app_wifi_event_cb(void *arg, event_module_t event_module,
int event_id, void *event_data)
{
switch (event_id) {
case EVENT_WIFI_GOT_IP:
LOGI("Got IP address");
g_get_ip_success = true;
break;
}
return 0;
}
// 等待 WiFi 连接和 IP 获取
static int wait_for_wifi_connection(void)
{
while (timeout > 0) {
if (g_wifi_connected && g_get_ip_success) {
LOGI("WiFi connected and IP obtained, starting HTTP tests...");
return 0;
}
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
}
return -1;
}
```
### 3. 性能统计
实时统计下载速度和数据块信息:
```c
static void update_download_stats(uint32_t chunk_size)
{
uint32_t current_time = xTaskGetTickCount() * portTICK_PERIOD_MS;
// 首次接收,初始化统计
if (g_download_stats.start_time == 0) {
g_download_stats.start_time = current_time;
g_download_stats.last_print_time = current_time;
}
// 统计数据块信息
g_download_stats.chunk_count++;
if (chunk_size < g_download_stats.min_chunk_size) {
g_download_stats.min_chunk_size = chunk_size;
}
if (chunk_size > g_download_stats.max_chunk_size) {
g_download_stats.max_chunk_size = chunk_size;
}
// 累加接收字节数
g_download_stats.total_bytes += chunk_size;
// 每隔 1 秒打印一次速度
if (current_time - g_download_stats.last_print_time >= 1000) {
uint32_t interval = current_time - g_download_stats.last_print_time;
uint32_t bytes_in_interval = g_download_stats.total_bytes - g_download_stats.last_bytes;
float speed_kbps = (float)bytes_in_interval * 1000.0f / interval / 1024.0f;
LOGI("下载中: %.2f MB (%.2f KB/s) | 已接收 %u 个数据块",
(float)g_download_stats.total_bytes / (1024.0f * 1024.0f),
speed_kbps,
g_download_stats.chunk_count);
g_download_stats.last_print_time = current_time;
g_download_stats.last_bytes = g_download_stats.total_bytes;
}
}
```
## 配置说明
### 缓冲区大小配置
在 `main.c` 中定义了多种缓冲区大小供测试:
```c
#define BUF_SIZE_2KB (2 * 1024) // 2KB
#define BUF_SIZE_4KB (4 * 1024) // 4KB
#define BUF_SIZE_8KB (8 * 1024) // 8KB
#define BUF_SIZE_16KB (16 * 1024) // 16KB
#define BUF_SIZE_32KB (32 * 1024) // 32KB
#define BUF_SIZE_64KB (64 * 1024) // 64KB
#define BUF_SIZE_128KB (128 * 1024) // 128KB (最大)
```
### prj.conf 关键配置
```kconfig
# LWIP 网络栈
CONFIG_LWIP=y
CONFIG_LWIP_OPTION_LWIP_DNS=y
CONFIG_LWIP_OPTION_SO_REUSE=y
# WiFi 管理器
CONFIG_WIFI_MANAGER=y
# HTTP 客户端
CONFIG_MODULE_HTTPCLIENT=y
# LISA 组件
CONFIG_LISA_OS=y
CONFIG_LISA_KV=y
CONFIG_LISA_NETWORK=y
CONFIG_LISA_HTTP=y
# 堆内存配置
CONFIG_HEAP_SIZE=0x3C00 # 主堆: 15KB
CONFIG_PSRAM_HEAP_SIZE=0x5b0000 # PSRAM 堆: 5.6MB
```
## 性能优化建议
### 1. 缓冲区大小选择
根据测试结果:
- **小文件(<10MB**:推荐使用 64KB-128KB 缓冲区
- **中等文件10-50MB**:推荐使用 128KB 缓冲区
- **大文件(>50MB**:推荐使用 128KB 缓冲区
### 2. 内存占用
- 流式下载**不会**一次性将整个文件加载到内存
- 内存占用 = 缓冲区大小2KB-128KB
- 即使下载 100MB 文件,实际内存占用也仅为缓冲区大小
### 3. 网络性能
影响下载速度的因素:
- WiFi 信号强度
- 网络带宽
- 服务器性能
- TCP 窗口大小
- 缓冲区大小
## 注意事项
1. **编译前配置(重要)**:首次编译前**必须**取消注释并修改 `src/main.c` 中的 WiFi 和服务器配置,否则会触发编译错误:
```
main.c:32:2: error: #error "请修改 SERVER_HOST 为实际的测试服务器 IP 地址例如192.168.1.100"
main.c:36:2: error: #error "请修改 TARGET_WIFI_SSID 为实际的 WiFi SSID"
main.c:40:2: error: #error "请修改 TARGET_WIFI_PWD 为实际的 WiFi 密码"
```
这是为了防止使用默认配置导致连接失败。配置方法请参考上方的"配置 WiFi 和服务器地址"章节。
2. **网络环境**:确保设备与测试服务器在同一局域网内,避免跨网段访问影响测试结果
3. **服务器地址**:将 `SERVER_HOST` 修改为运行 `http-server.py` 的主机的局域网 IP例如192.168.1.100
4. **WiFi 配置**:将 `TARGET_WIFI_SSID` 和 `TARGET_WIFI_PWD` 修改为实际的 WiFi SSID 和密码
5. **测试时间**:完整测试需要较长时间(约 10-20 分钟),请耐心等待
6. **内存限制**:缓冲区大小受 PSRAM 堆大小限制,最大建议不超过 128KB
7. **超时设置**HTTP 超时设置为 120 秒,适合大文件下载,可根据需要调整
8. **Python 版本**:测试服务器需要 Python 3.6 或更高版本
## 相关文档
- [LISA WiFi 组件文档](../../../components/lisa_wifi/README.md) - WiFi 初始化和连接
- [WiFi Manager 文档](../../../modules/wifi_manager/README.md) - WiFi 连接管理
- [LWIP 配置说明](../../../docs/lwip.md) - 网络栈配置

View File

@@ -0,0 +1,177 @@
#!/bin/bash
set -e
usage() {
echo "使用方式: $0 [选项]"
echo "选项:"
echo " -S, --Source <path> 指定项目源码路径 (默认为当前脚本所在目录)"
echo " -t, --target <target> 指定构建目标 (如 menuconfig)"
echo " -C, --Clean 清理构建目录"
echo " -B, --build 构建输出目录"
echo " -h, --help 显示此帮助信息"
echo " -r, --release 以 Release 模式构建 (移除 DEBUG_PATH 信息)"
echo " -w, --warnings-as-errors 将警告视为错误"
echo " -D<var>=<value> 传递 CMake 变量 (可多次使用)"
echo ""
echo "示例:"
echo " $0 -S samples/helloworld -DBOARD=arcs_mini 指定板型构建"
echo " $0 -S samples/helloworld -DBOARD=arcs_evb 使用 EVB 板型"
echo " $0 -S samples/helloworld -t menuconfig -DBOARD=arcs_mini 运行 menuconfig"
echo " $0 -C -S samples/helloworld -DBOARD=arcs_mini 清理并重新构建"
echo " $0 -S samples/helloworld -DBOARD=my_board -DBOARD_SEARCH_PATH=/path/to/boards 使用自定义板型"
exit 1
}
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_PATH="$SCRIPT_DIR"
TARGET=""
CLEAN=false
OUTPUT="build"
WARNINGS_AS_ERRORS=false
RELEASE=false
ARCS_BASE_DIR_NAME="arcs-sdk"
ARCS_DEV_TOOLS_DIR_NAME="listenai-dev-tools"
ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME="gcc"
ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME="listenai-tools"
find_arcs_base() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_BASE_DIR_NAME"
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found ARCS_BASE: $current_dir/$dir_name"
export ARCS_BASE="$current_dir/$dir_name"
return 0
fi
current_dir=$(dirname "$current_dir")
done
echo "ARCS_BASE not found, Please add ARCS_BASE environment variable or set ARCS_BASE_DIR_NAME to the correct directory."
echo "Current Target ARCS_BASE directory name: $ARCS_BASE_DIR_NAME."
exit 1
}
find_dev_tools() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_DEV_TOOLS_DIR_NAME"
echo "trying to find $dir_name in parent directories..."
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found $dir_name: $current_dir/$dir_name"
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME" ]; then
echo "Found LISTENAI_TOOLS_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
export LISTENAI_TOOLS_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
fi
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME" ]; then
echo "Found NUCLEI_TOOLCHAIN_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
export NUCLEI_TOOLCHAIN_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
fi
return 0
fi
current_dir=$(dirname "$current_dir")
done
}
while [[ $# -gt 0 ]]; do
case $1 in
-S|--Source)
PROJECT_PATH="$2"
shift 2
;;
-B|--build)
OUTPUT="$2"
shift 2
;;
-t|--target)
TARGET="$2"
shift 2
;;
-C|--Clean)
CLEAN=true
shift 1
;;
-w|--warnings-as-errors)
WARNINGS_AS_ERRORS=true
shift 1
;;
-h|--help)
usage
;;
-r|--release)
RELEASE=true
shift 1
;;
-D*)
CMAKE_VARS+=("$1")
shift 1
;;
*)
echo "未知参数: $1"
usage
;;
esac
done
echo "Source: $PROJECT_PATH"
echo "Target: $TARGET"
echo "Clean : $CLEAN"
if [ -z "$LISTENAI_TOOLS_PATH" ] || [ -z "$NUCLEI_TOOLCHAIN_PATH" ]; then
find_dev_tools
fi
if [ -z "${LISTENAI_TOOLS_PATH}" ]; then
export LISTENAI_TOOLS_PATH="请添加 LISTENAI_TOOLS_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 LISTENAI_TOOLS_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
if [ -z "${NUCLEI_TOOLCHAIN_PATH}" ]; then
export NUCLEI_TOOLCHAIN_PATH="请添加 NUCLEI_TOOLCHAIN_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 NUCLEI_TOOLCHAIN_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
############### 下面代码不用修改 ##################
# 构建工具的位置
CMAKE_PROGRAM="$LISTENAI_TOOLS_PATH/cmake/bin/cmake"
NINJA_PROGRAM="$LISTENAI_TOOLS_PATH/ninja/ninja"
# 配置环境变量 ARCS_BASE
if [ -z "$ARCS_BASE" ]; then
find_arcs_base
fi
if [ "$CLEAN" = true ]; then
rm -rf $OUTPUT
fi
# Initialize CMAKE_VARS array if it doesn't exist
declare -a CMAKE_VARS
# Add warnings-as-errors flag if enabled
if [ "$WARNINGS_AS_ERRORS" = true ]; then
CMAKE_VARS+=("-DCMAKE_C_FLAGS=-Werror")
CMAKE_VARS+=("-DCMAKE_CXX_FLAGS=-Werror")
echo "Treating warnings as errors"
fi
# Add release flags if enabled
if [ "$RELEASE" = true ]; then
CMAKE_VARS+=("-DENABLE_DEBUG_PATH=OFF")
echo "Release mode enabled (-DENABLE_DEBUG_PATH=OFF)"
fi
$CMAKE_PROGRAM -B "$OUTPUT" -G Ninja -S "$PROJECT_PATH" \
-DCMAKE_MAKE_PROGRAM="$NINJA_PROGRAM" \
"${CMAKE_VARS[@]}"
if [ -z "$TARGET" ]; then
$CMAKE_PROGRAM --build "$OUTPUT" -j4
else
$CMAKE_PROGRAM --build "$OUTPUT" --target "$TARGET"
fi

View File

@@ -0,0 +1,129 @@
#!/usr/bin/python
import socket
import os
from http.server import HTTPServer
from http.server import SimpleHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
PORT = 8080
DEFAULT_FILE_SIZE_MB = 5 # 默认文件大小MB
MAX_FILE_SIZE_MB = 100 # 最大允许文件大小MB
class RequestHandler(SimpleHTTPRequestHandler):
length = 0
def _set_headers(self, content_type='text/html', content_length=0):
self.send_response(200)
self.send_header('Content-Type', content_type)
self.send_header('Content-Length', str(content_length))
self.end_headers()
def do_GET(self):
# 解析 URL 和查询参数
parsed_url = urlparse(self.path)
path = parsed_url.path
query_params = parse_qs(parsed_url.query)
# 处理下载随机文件的请求
if path == '/random' or path == '/random.bin':
print(f"\n===== 收到 GET 请求 =====")
print(f"路径: {self.path}")
# 从查询参数获取文件大小MB默认为 DEFAULT_FILE_SIZE_MB
size_mb = DEFAULT_FILE_SIZE_MB
if 'size' in query_params:
try:
size_mb = int(query_params['size'][0])
# 限制文件大小范围
if size_mb < 1:
size_mb = 1
elif size_mb > MAX_FILE_SIZE_MB:
size_mb = MAX_FILE_SIZE_MB
print(f"⚠️ 请求的大小超过最大限制,使用最大值: {MAX_FILE_SIZE_MB}MB")
except ValueError:
print(f"⚠️ 无效的 size 参数,使用默认值: {DEFAULT_FILE_SIZE_MB}MB")
file_size = size_mb * 1024 * 1024
print(f"生成 {size_mb}MB ({file_size} bytes) 随机数据...")
# 动态生成指定大小的随机数据
random_data = os.urandom(file_size)
print(f"发送 {size_mb}MB 随机数据文件")
self._set_headers(
content_type='application/octet-stream',
content_length=len(random_data)
)
self.wfile.write(random_data)
print("文件发送完成")
else:
# 返回简单的 HTML 页面
payload = f"""<html>
<head><title>ARCS HTTP Server</title></head>
<body>
<h1>ARCS HTTP Server</h1>
<p>服务器运行正常</p>
<h2>使用说明</h2>
<ul>
<li><a href="/random">下载随机文件(默认 {DEFAULT_FILE_SIZE_MB}MB</a></li>
<li><a href="/random?size=10">下载 10MB 随机文件</a></li>
<li><a href="/random?size=50">下载 50MB 随机文件</a></li>
<li><a href="/random?size=100">下载 100MB 随机文件</a></li>
</ul>
<p>使用方法: <code>/random?size=N</code> (N 为文件大小,单位 MB最大 {MAX_FILE_SIZE_MB}MB)</p>
</body>
</html>"""
self._set_headers(
content_type='text/html',
content_length=len(payload.encode())
)
self.wfile.write(payload.encode())
def do_POST(self):
# 1. 打印请求信息
print("\n===== 收到 POST 请求 =====")
print(f"路径: {self.path}")
print("头部:")
for header, value in self.headers.items():
print(f" {header}: {value}")
# 2. 读取请求 body 并打印
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length)
print("Body 数据:")
try:
print(post_data.decode('utf-8'))
except UnicodeDecodeError:
print(f"<二进制数据,{len(post_data)} 字节>")
payload = "<html><p>Done</p></html>"
self._set_headers(content_length=len(payload))
self.wfile.write(payload.encode())
def main():
# 创建 HTTP 服务器并设置端口重用
httpd = HTTPServer(("0.0.0.0", PORT), RequestHandler)
httpd.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
print(f"\n========================================")
print(f" ARCS HTTP 测试服务器")
print(f"========================================")
print(f"监听地址: http://0.0.0.0:{PORT}")
print(f"默认文件大小: {DEFAULT_FILE_SIZE_MB}MB")
print(f"最大文件大小: {MAX_FILE_SIZE_MB}MB")
print(f"\n使用示例:")
print(f" 下载默认大小: http://0.0.0.0:{PORT}/random")
print(f" 下载 10MB: http://0.0.0.0:{PORT}/random?size=10")
print(f" 下载 50MB: http://0.0.0.0:{PORT}/random?size=50")
print(f"========================================\n")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\n\n服务器已停止")
httpd.shutdown()
httpd.server_close()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,149 @@
# 项目可根据需要在此文件中设置对应的配置
CONFIG_ARCS_HAL_WARNING_DISABLE=y
CONFIG_SDK_MODULE_EASYLOGGER=n
# CONFIG_ARCS_HAL_DEBUG=y
CONFIG_MODULE_FREERTOS=y
CONFIG_MODULE_HEAP=y
CONFIG_LINK_OPTION_NOSYS_SPECS=y
CONFIG_EASYLOGGER_ASYNC_BUF_SIZE=0x1000
# DISK驱动使能
CONFIG_DISK_DRIVER=y
#using mbedtls for hardware decoding(IF CONFIG_WX_SQLITE3_HARD_CODEC)
CONFIG_SDK_MODULE_MBEDTLS=y
CONFIG_CUSTOM_MBEDTLS_CFG_FILE=y
CONFIG_MBEDTLS_CFG_FILE="config-tls-generic.h"
CONFIG_MBEDTLS_HARDWARE_ENTROPY=y
# CONFIG_MBEDTLS_HARDWARE_AES=y
# CONFIG_MBEDTLS_HARDWARE_SHA1=y
# CONFIG_MBEDTLS_HARDWARE_SHA256=y
# CONFIG_MBEDTLS_HARDWARE_SHA512=y
# CONFIG_MBEDTLS_HARDWARE_AES_CCM=y
# CONFIG_MBEDTLS_HARDWARE_AES_GCM=y
# CONFIG_MBEDTLS_HARDWARE_ECC=y
# System support
CONFIG_MBEDTLS_HAVE_ASM=y
CONFIG_MBEDTLS_HAVE_TIME_DATE=y
CONFIG_MBEDTLS_SSL_ALPN=y
# Save RAM at the expense of ROM
CONFIG_MBEDTLS_AES_ROM_TABLES=y
# mbed TLS feature support
CONFIG_MBEDTLS_CIPHER_MODE_CBC_ENABLED=y
CONFIG_MBEDTLS_KEY_EXCHANGE_RSA_ENABLED=y
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED=y
# Supported TLS versions
CONFIG_MBEDTLS_TLS_VERSION_1_0=y
CONFIG_MBEDTLS_TLS_VERSION_1_1=y
CONFIG_MBEDTLS_TLS_VERSION_1_2=y
# mbed TLS modules
CONFIG_MBEDTLS_CIPHER_AES_ENABLED=y
CONFIG_MBEDTLS_CIPHER=y
CONFIG_MBEDTLS_CTR_DRBG_ENABLED=y
CONFIG_MBEDTLS_ENTROPY_ENABLED=y
CONFIG_MBEDTLS_MD=y
CONFIG_MBEDTLS_MAC_MD5_ENABLED=y
CONFIG_MBEDTLS_HASH_MD5_ENABLED=y
CONFIG_MBEDTLS_MAC_SHA1_ENABLED=y
CONFIG_MBEDTLS_HASH_SHA1_ENABLED=y
CONFIG_MBEDTLS_MAC_SHA256_ENABLED=y
CONFIG_MBEDTLS_SHA256_SMALLER=y
CONFIG_MBEDTLS_HASH_SHA256_ENABLED=y
CONFIG_MBEDTLS_DTLS=y
CONFIG_MBEDTLS_SERVER_NAME_INDICATION=y
CONFIG_MBEDTLS_ECDH_C=y
CONFIG_MBEDTLS_ECP_C=y
CONFIG_MBEDTLS_CIPHER_GCM_ENABLED=y
CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y
CONFIG_MBEDTLS_MAC_SHA512_ENABLED=y
CONFIG_MBEDTLS_HASH_SHA512_ENABLED=y
CONFIG_MBEDTLS_PEM_CERTIFICATE_FORMAT=y
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384
CONFIG_MBEDTLS_CTR_DRBG_ENABLED=y
# CONFIG_MBEDTLS_DEBUG=y
CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=n
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y
# CONFIG_ARCS_HAL_DEBUG=y
CONFIG_MODULE_HEAP=y
CONFIG_LOG=y
CONFIG_SYSLOG_PRINTK_REDIRECT=y
CONFIG_SYSLOG_UART_TX_PIN=3
CONFIG_SYSLOG_UART_TX_PIN_FUNC_SEL=2
CONFIG_SYSLOG_UART_PORT=0
CONFIG_SYSLOG_UART_BAUDRATE=921600
CONFIG_LWIP=y
CONFIG_WIFI=y
CONFIG_WIFI_TX_BUF_COPY=y
CONFIG_WIFI_LWIP_SAME_CORE=y
CONFIG_LWIP_TX_BUF_COPY=y
CONFIG_LWIP_OPTION_LWIP_DNS=y
CONFIG_LWIP_OPTION_SO_REUSE=y
CONFIG_FREERTOS_STATIC_ALLOCATION_ENABLE=y
CONFIG_DCACHE_ENABLE=y
CONFIG_MODULE_ZE_TLS=y
CONFIG_MODULE_HTTPCLIENT=y
CONFIG_MODULE_NOPOLL=y
CONFIG_MODULE_CORESNTP=y
CONFIG_LWIP_OPTION_SO_REUSE=y
CONFIG_LWIP_OPTION_LWIP_DNS=y
CONFIG_HEAP_SIZE=0x3C00
CONFIG_PSRAM_HEAP_SIZE=0x5b0000
# 文件系统配置
CONFIG_FILE_SYSTEM=y
CONFIG_FATFS_FILESYSTEM=y
# 抽象文件系统
CONFIG_LSFS=y
CONFIG_LSFS_FAT=y
CONFIG_LSFS_REGISTER_VFS=y
CONFIG_LVFS_POSIX_API_ALIAS=y
CONFIG_LVFS=y
CONFIG_LVFS_POSIX_API=y
CONFIG_FS_ENV_MEMORY_MANAGEMENT_USE_PSRAM=y
# LISA WIFI
CONFIG_LISA_WIFI=y
# MAC Manager
CONFIG_MAC_MANAGER=y
CONFIG_MAC_MANAGER_MAC_BY_CHIP_ID=y
# WIFI MANAGER
CONFIG_WIFI_MANAGER=y
# LISA Shell
CONFIG_LISA_SHELL=y
# LISA Porting
CONFIG_LISA_PORTING=y
CONFIG_CJSON=y
CONFIG_LISA_OS=y
CONFIG_LISA_KV=y
CONFIG_LISA_NETWORK=y
CONFIG_SDK_MODULE_ICO=y
CONFIG_LISA_HTTP=y
CONFIG_BACK_TRACE=y
# SD卡驱动
CONFIG_DISK_DRIVER_SDMMC=y
CONFIG_LISA_SDMMC_DEVICE=y

View File

@@ -0,0 +1,3 @@
tests:
samples.http.download.perf:
build_only: true

View File

@@ -0,0 +1,10 @@
listenai_library_named(app)
listenai_library_sources(
main.c
fs/user_fs.c
)
listenai_include_directories(
./
./fs
)

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lsfs.h"
#include "disk/disk_access.h"
#include "lisa_sdmmc.h"
#include "log_print.h"
#if CONFIG_LVFS
#include "lvfs.h"
#endif
// #define FLASHDISK_DEVICE "NAND:"
#define DISK_DEVICE "SD:"
#define DISK_MOUNT_POINT "/" DISK_DEVICE
static struct lsfs_mount_t flash_lsfs_mnt = {
.type = LSFS_FATFS,
.mnt_point = DISK_MOUNT_POINT,
.fs_data = NULL,
};
static int fs_mount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_mount(mp);
if (ret != 0)
{
CLOG("No file system, try reformatting, mount ret: %d", ret);
/* 格式化底层设备,去掉挂载点前面的'/' 作为设备名,例如 "/SD:" -> "SD:" */
ret = lsfs_mkfs(mp->type, &mp->mnt_point[1], NULL, 0);
if (ret == 0)
{
CLOG("mkfs success, retry mount");
ret = lsfs_mount(mp);
}
else
{
CLOG("mkfs failed, ret: %d", ret);
}
}
if (ret != 0)
{
CLOG("Failed to mount filesystem: %d", ret);
return ret;
}
CLOG("%s mounted successfully", mp->mnt_point);
return 0;
}
static int fs_unmount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_unmount(mp);
if (ret != 0)
{
CLOG("%s unmount failed!\n", mp->mnt_point);
}
CLOG("%s unmount success!\n", mp->mnt_point);
}
static int list_dir(const char *path)
{
struct lsfs_dir_t dir;
struct lsfs_dirent entry;
int ret;
int nfile = 0, ndir = 0;
/* 初始化目录对象 */
lsfs_dir_t_init(&dir);
ret = lsfs_opendir(&dir, path);
CLOG("%s opendir: %d", path, ret);
if (ret == 0)
{
while (1)
{
ret = lsfs_readdir(&dir, &entry);
if (ret != 0 || entry.name[0] == 0)
break; /* Error or end of dir */
if (entry.type == LSFS_DIR_ENTRY_DIR)
{
/* Directory */
CLOG(" <DIR> %s", entry.name);
ndir++;
}
else
{
/* File */
CLOG("%10lu %s", entry.size, entry.name);
nfile++;
}
}
lsfs_closedir(&dir);
CLOG("%d dirs, %d files.", ndir, nfile);
}
else
{
CLOG("Failed to open \"%s\". (%u)", path, ret);
}
return ret;
}
static void user_fs_change_to_root_folder(void)
{
int ret;
ret = lvfs_chdrive(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change drive to %s, error: %d\n", DISK_MOUNT_POINT, ret);
}
ret = lvfs_chdir(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change directory to /SD:/, error: %d\n", ret);
} else {
CLOG("Changed to directory /SD:/\n");
}
}
int user_fs_init(void){
lisa_sdmmc_probe(lisa_device_get("sdmmc0"));
disk_init(NULL);
#if CONFIG_LVFS
lvfs_init();
#endif
lsfs_init();
if(0 != fs_mount(&flash_lsfs_mnt)){
return -1;
}
user_fs_change_to_root_folder();
return 0;
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
int user_fs_init(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,537 @@
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "stdbool.h"
#include "assert.h"
#include "FreeRTOS.h"
#include "task.h"
#define TAG "http-test"
#include "lisa_log.h"
#include "lisa_kv.h"
#include "mac_manager.h"
#include "mac_manager_ops.h"
#include "wifi_manager/wifi_manager.h"
#include "user_fs.h"
#include "lisa_wifi.h"
#include "ls_wifi_type.h"
#include "net_al.h"
#include "net_def.h"
#include "HTTPCUsr_api.h"
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
#define SERVER_HOST "your_server_ip"// 修改为测试服务器的 IP 地址,例如:"192.168.1.100"
#define SERVER_PORT 8080 // 修改为测试服务器的端口号,默认 8080
#define TARGET_WIFI_SSID "your_wifi_ssid"// 修改为目标 WiFi 的 SSID
#define TARGET_WIFI_PWD "your_wifi_pwd"// 修改为目标 WiFi 的密码
// 编译时检查:确保用户已修改默认配置
#ifndef SERVER_HOST
#error "请修改 SERVER_HOST 为实际的测试服务器 IP 地址例如192.168.1.100"
#endif
#ifndef TARGET_WIFI_SSID
#error "请修改 TARGET_WIFI_SSID 为实际的 WiFi SSID"
#endif
#ifndef TARGET_WIFI_PWD
#error "请修改 TARGET_WIFI_PWD 为实际的 WiFi 密码"
#endif
#define WIFI_MGR_AUTO_CONNECT_INTERVAL_MS 2000
// 测试用的缓冲区大小(字节)
#define BUF_SIZE_2KB (2 * 1024) // 2KB
#define BUF_SIZE_4KB (4 * 1024) // 4KB
#define BUF_SIZE_8KB (8 * 1024) // 8KB
#define BUF_SIZE_16KB (16 * 1024) // 16KB
#define BUF_SIZE_32KB (32 * 1024) // 32KB
#define BUF_SIZE_64KB (64 * 1024) // 64KB
#define BUF_SIZE_128KB (128 * 1024) // 128KB (最大)
// 下载速度统计结构
typedef struct {
uint32_t total_bytes; // 总下载字节数
uint32_t start_time; // 开始时间(毫秒)
uint32_t last_print_time; // 上次打印时间
uint32_t last_bytes; // 上次打印时的字节数
uint32_t chunk_count; // 数据块计数
uint32_t min_chunk_size; // 最小数据块大小
uint32_t max_chunk_size; // 最大数据块大小
} download_stats_t;
// 测试结果记录
typedef struct {
uint32_t file_size_mb; // 文件大小(MB)
uint32_t buffer_size; // 缓冲区大小(字节)
float avg_speed_kbps; // 平均速度(KB/s)
uint32_t total_time_ms; // 总时间(毫秒)
uint32_t chunk_count; // 数据块数量
} test_result_t;
#define MAX_TEST_RESULTS 20
#define WIFI_MGR_SEARCH_AP_BUFFER_SIZE 10
static download_stats_t g_download_stats = {0};
static test_result_t g_test_results[MAX_TEST_RESULTS] = {0};
static int g_test_count = 0;
static mac_manager_t *m_mac_manager;
static wifi_mgr_sta_config_t list[WIFI_MGR_SEARCH_AP_BUFFER_SIZE] = { 0 };
static int8_t custom_get_wifi_mac(uint8_t mac_addr[6])
{
int8_t ret = 0;
if (!mac_addr)
return -1;
ret = mac_manager_get(m_mac_manager, mac_addr, 6);
LOGI("custom_get_wifi_mac: %d, %02X:%02X:%02X:%02X:%02X:%02X", ret, mac_addr[0], mac_addr[1], mac_addr[2],
mac_addr[3], mac_addr[4], mac_addr[5]);
return ret;
}
static volatile bool g_wifi_connected = false;
static volatile bool g_get_ip_success = false;
static void dhcp_status_callback(int vif_idx, bool success, uint32_t ip_addr, uint32_t netmask, uint32_t gateway, void *arg)
{
if (success) {
LOGI("DHCP Success on VIF-%d: IP=%d.%d.%d.%d, Mask=%d.%d.%d.%d, GW=%d.%d.%d.%d",
vif_idx,
ip_addr & 0xff, (ip_addr >> 8) & 0xff, (ip_addr >> 16) & 0xff, (ip_addr >> 24) & 0xff,
netmask & 0xff, (netmask >> 8) & 0xff, (netmask >> 16) & 0xff, (netmask >> 24) & 0xff,
gateway & 0xff, (gateway >> 8) & 0xff, (gateway >> 16) & 0xff, (gateway >> 24) & 0xff);
g_get_ip_success = true;
} else {
LOGI("DHCP Failed on VIF-%d", vif_idx);
}
}
static void wifi_mgr_connection_status_cb(wifi_mgr_connection_info_t *connection_info, void *arg)
{
net_if_t *net_if;
LOGI("WiFi connection status: %d", connection_info->status);
switch (connection_info->status) {
case WIFI_MGR_STA_CONNECTED:
LOGI("WiFi connected to AP");
g_wifi_connected = true;
net_if = net_if_get(WIFI_VIF_STA_IDX);
net_if_up(net_if);
if (!net_if->static_ip) {
ls_dhcpc_start(WIFI_VIF_STA_IDX);
}
break;
case WIFI_MGR_STA_DISCONNECTED:
LOGI("WiFi disconnected from AP");
g_wifi_connected = false;
ls_dhcpc_stop(WIFI_VIF_STA_IDX);
net_if_down(net_if_get(WIFI_VIF_STA_IDX));
break;
default:
break;
}
}
static void user_mac_manager_init(void)
{
mac_manager_config_t config = {
.random_mac_if_mac_invalid = false,
};
m_mac_manager = mac_manager_init(mac_manager_ops_get()->mem_ops, mac_manager_ops_get()->content_ops, &config);
if (m_mac_manager == NULL) {
LOGI("mac_manager_init failed\n");
assert(0);
}
}
static void user_wifi_manager_init(void)
{
wifi_mgr_sta_config_t cfg = {
.ssid = TARGET_WIFI_SSID,
.pwd = TARGET_WIFI_PWD,
};
wifi_mgr_autoconn_config_t autoconn_cfg = {
.interval_ms = WIFI_MGR_AUTO_CONNECT_INTERVAL_MS,
};
wifi_mgr_init(wifi_mgr_ops_get());
wifi_mgr_sta_enable();
// 注册 WiFi 连接状态回调
wifi_mgr_sta_add_connection_cb(wifi_mgr_connection_status_cb, NULL);
/* 搜索并删除所有已保存的 AP */
int count = wifi_mgr_storage_search_ap(list, WIFI_MGR_SEARCH_AP_BUFFER_SIZE, SEARCH_ALL, NULL);
for (int i = 0; i < count; i++) {
LOGI("Deleting saved AP: ssid=%s", list[i].ssid);
wifi_mgr_storage_delete_ap(&list[i]);
}
// 保存 AP 配置并启动自动连接
wifi_mgr_storage_save_ap(&cfg);
wifi_mgr_auto_connect_start(&autoconn_cfg);
LOGI("WiFi auto-connect started");
}
static int wait_for_wifi_connection(void)
{
int timeout = 30; // 30 秒超时
LOGI("Waiting for WiFi connection and IP address...");
// 等待 WiFi 连接和 IP 获取
while (timeout > 0) {
if (g_wifi_connected && g_get_ip_success) {
LOGI("WiFi connected and IP obtained, starting HTTP tests...");
vTaskDelay(pdMS_TO_TICKS(500)); // 短暂延迟确保网络稳定
return 0;
}
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
if (timeout % 5 == 0) {
LOGI("Waiting... (WiFi:%d, IP:%d, timeout:%d)", g_wifi_connected, g_get_ip_success, timeout);
}
}
LOGI("WiFi connection or IP acquisition timeout (WiFi:%d, IP:%d)", g_wifi_connected, g_get_ip_success);
return -1;
}
// 格式化字节数为人类可读的格式
static void format_bytes(uint32_t bytes, char *buf, size_t buf_size)
{
float value;
const char *unit;
if (bytes >= 1024 * 1024) {
value = (float)bytes / (1024.0f * 1024.0f);
unit = "MB";
} else if (bytes >= 1024) {
value = (float)bytes / 1024.0f;
unit = "KB";
} else {
value = (float)bytes;
unit = "B";
}
snprintf(buf, buf_size, "%.2f %s", value, unit);
}
// 打印下载完成统计信息并保存结果
static void print_download_stats(uint32_t file_size_mb, uint32_t buffer_size)
{
if (g_download_stats.start_time == 0) {
return;
}
uint32_t end_time = xTaskGetTickCount() * portTICK_PERIOD_MS;
uint32_t total_time = end_time - g_download_stats.start_time;
if (total_time > 0) {
float avg_speed_kbps = (float)g_download_stats.total_bytes * 1000.0f / total_time / 1024.0f;
uint32_t avg_chunk_size =
g_download_stats.chunk_count > 0 ? g_download_stats.total_bytes / g_download_stats.chunk_count : 0;
char size_str[32], min_str[32], max_str[32], avg_str[32];
format_bytes(g_download_stats.total_bytes, size_str, sizeof(size_str));
format_bytes(g_download_stats.min_chunk_size, min_str, sizeof(min_str));
format_bytes(g_download_stats.max_chunk_size, max_str, sizeof(max_str));
format_bytes(avg_chunk_size, avg_str, sizeof(avg_str));
LOGI("========== 下载完成 ==========");
LOGI("总大小: %s (%u bytes)", size_str, g_download_stats.total_bytes);
LOGI("总时间: %u ms (%.2f 秒)", total_time, (float)total_time / 1000.0f);
LOGI("平均速度: %.2f KB/s (%.2f Mbps)", avg_speed_kbps, avg_speed_kbps * 8 / 1024);
LOGI("--- 数据块统计 ---");
LOGI("总数据块数: %u", g_download_stats.chunk_count);
LOGI("数据块大小: 最小=%s, 最大=%s, 平均=%s", min_str, max_str, avg_str);
LOGI("================================");
// 保存测试结果
if (g_test_count < MAX_TEST_RESULTS) {
g_test_results[g_test_count].file_size_mb = file_size_mb;
g_test_results[g_test_count].buffer_size = buffer_size;
g_test_results[g_test_count].avg_speed_kbps = avg_speed_kbps;
g_test_results[g_test_count].total_time_ms = total_time;
g_test_results[g_test_count].chunk_count = g_download_stats.chunk_count;
g_test_count++;
}
}
// 重置统计数据
memset(&g_download_stats, 0, sizeof(download_stats_t));
}
// 打印测试结果汇总报告
static void print_summary_report(void)
{
if (g_test_count == 0) {
LOGI("没有测试结果");
return;
}
LOGI("\n");
LOGI("╔════════════════════════════════════════════════════════════════╗");
LOGI("║ HTTP 下载性能测试汇总报告 ║");
LOGI("╚════════════════════════════════════════════════════════════════╝");
LOGI("");
LOGI("序号 | 文件大小 | 缓冲区 | 平均速度 | 总时间 | 数据块数");
LOGI("-----|----------|----------|---------------|-----------|----------");
for (int i = 0; i < g_test_count; i++) {
test_result_t *r = &g_test_results[i];
char buf_str[16];
// 格式化缓冲区大小
if (r->buffer_size >= 1024) {
snprintf(buf_str, sizeof(buf_str), "%uKB", r->buffer_size / 1024);
} else {
snprintf(buf_str, sizeof(buf_str), "%uB", r->buffer_size);
}
LOGI(" %2d | %4uMB | %-8s | %7.2f KB/s | %7.2fs | %u", i + 1, r->file_size_mb, buf_str, r->avg_speed_kbps,
(float)r->total_time_ms / 1000.0f, r->chunk_count);
}
LOGI("");
// 分析不同文件大小下的最佳缓冲区
LOGI("═══════════════════════════════════════════════════════════════");
LOGI(" 性能分析");
LOGI("═══════════════════════════════════════════════════════════════");
// 找出每个文件大小的最佳缓冲区
uint32_t file_sizes[] = {10, 50, 100};
for (int f = 0; f < 3; f++) {
uint32_t target_size = file_sizes[f];
float max_speed = 0;
uint32_t best_buffer = 0;
for (int i = 0; i < g_test_count; i++) {
if (g_test_results[i].file_size_mb == target_size) {
if (g_test_results[i].avg_speed_kbps > max_speed) {
max_speed = g_test_results[i].avg_speed_kbps;
best_buffer = g_test_results[i].buffer_size;
}
}
}
if (max_speed > 0) {
char buf_str[16];
if (best_buffer >= 1024) {
snprintf(buf_str, sizeof(buf_str), "%uKB", best_buffer / 1024);
} else {
snprintf(buf_str, sizeof(buf_str), "%uB", best_buffer);
}
LOGI("• %uMB 文件: 最佳缓冲区 = %s, 速度 = %.2f KB/s (%.2f Mbps)", target_size, buf_str, max_speed,
max_speed * 8 / 1024);
}
}
LOGI("");
LOGI("═══════════════════════════════════════════════════════════════");
}
// 更新下载统计信息
static void update_download_stats(uint32_t chunk_size)
{
uint32_t current_time = xTaskGetTickCount() * portTICK_PERIOD_MS;
// 首次接收数据,初始化统计
if (g_download_stats.start_time == 0) {
g_download_stats.start_time = current_time;
g_download_stats.last_print_time = current_time;
g_download_stats.total_bytes = 0;
g_download_stats.last_bytes = 0;
g_download_stats.chunk_count = 0;
g_download_stats.min_chunk_size = 0xFFFFFFFF;
g_download_stats.max_chunk_size = 0;
LOGI("========== 开始下载 ==========");
LOGI("注意: 数据以小块方式接收,不会占用大量内存");
}
// 统计数据块信息
g_download_stats.chunk_count++;
if (chunk_size < g_download_stats.min_chunk_size) {
g_download_stats.min_chunk_size = chunk_size;
}
if (chunk_size > g_download_stats.max_chunk_size) {
g_download_stats.max_chunk_size = chunk_size;
}
// 累加接收的字节数
g_download_stats.total_bytes += chunk_size;
// 每隔 1 秒打印一次速度
if (current_time - g_download_stats.last_print_time >= 1000) {
uint32_t interval = current_time - g_download_stats.last_print_time;
uint32_t bytes_in_interval = g_download_stats.total_bytes - g_download_stats.last_bytes;
float speed_kbps = (float)bytes_in_interval * 1000.0f / interval / 1024.0f;
char size_str[32];
format_bytes(g_download_stats.total_bytes, size_str, sizeof(size_str));
LOGI("下载中: %s (%.2f KB/s) | 已接收 %u 个数据块", size_str, speed_kbps, g_download_stats.chunk_count);
g_download_stats.last_print_time = current_time;
g_download_stats.last_bytes = g_download_stats.total_bytes;
}
}
// 执行 HTTP GET 请求
static int http_get_request(const char *uri, const char *test_name, uint32_t file_size_mb, uint32_t buffer_size)
{
int ret;
HTTPParameters http_params;
CHAR *response_buf = NULL;
INT32 recv_size = 0;
char buf_size_str[32];
format_bytes(buffer_size, buf_size_str, sizeof(buf_size_str));
LOGI("\n========================================");
LOGI("%s", test_name);
LOGI("缓冲区大小: %s (%u bytes)", buf_size_str, buffer_size);
LOGI("========================================");
// 重置统计数据
memset(&g_download_stats, 0, sizeof(download_stats_t));
// 分配响应缓冲区
response_buf = (CHAR *)malloc(buffer_size);
if (response_buf == NULL) {
LOGE("Failed to allocate response buffer");
return -1;
}
// 初始化 HTTP 参数
memset(&http_params, 0, sizeof(HTTPParameters));
snprintf(http_params.Uri, HTTP_CLIENT_MAX_URL_LENGTH, "http://%s:%d%s", SERVER_HOST, SERVER_PORT, uri);
http_params.HttpVerb = VerbGet;
http_params.Verbose = FALSE;
http_params.nTimeout = 120; // 120 秒超时
LOGI("URL: %s", http_params.Uri);
// 打开 HTTP 连接
ret = HTTPC_open(&http_params);
if (ret != 0) {
LOGE("HTTPC_open failed: %d", ret);
free(response_buf);
return -1;
}
// 发送 HTTP 请求
ret = HTTPC_request(&http_params, NULL);
if (ret != 0) {
LOGE("HTTPC_request failed: %d", ret);
HTTPC_close(&http_params);
free(response_buf);
return -1;
}
// 读取响应数据(流式读取)
while (1) {
ret = HTTPC_read(&http_params, response_buf, buffer_size, &recv_size);
if (ret != 0 || recv_size <= 0) {
// 读取完成或出错
break;
}
// 更新下载统计
update_download_stats(recv_size);
}
// 关闭 HTTP 连接
HTTPC_close(&http_params);
free(response_buf);
// 打印统计信息
print_download_stats(file_size_mb, buffer_size);
// 等待一段时间再进行下一个请求
vTaskDelay(pdMS_TO_TICKS(2000));
return 0;
}
int main(int argc, char **argv)
{
int ret;
LOGI("HTTP Performance Test Starting...");
// 初始化 MAC 管理器
user_mac_manager_init();
// 注册 DHCP 状态回调
net_dhcp_register_status_callback(dhcp_status_callback, NULL);
// 初始化 LISA WiFi
lisa_wifi_ops_t ops = {
.custom_mac = custom_get_wifi_mac,
};
lisa_wifi_init(&ops);
// 初始化文件系统和 KV 存储
user_fs_init();
lisa_kv_init();
// 初始化 WiFi 管理器并启动自动连接
user_wifi_manager_init();
// 等待 WiFi 连接成功
ret = wait_for_wifi_connection();
if (ret != 0) {
LOGI("Failed to connect to WiFi");
return -1;
}
LOGI("\n========================================");
LOGI("HTTP 下载性能测试");
LOGI("测试不同缓冲区大小对下载速度的影响");
LOGI("========================================");
// 测试方案 1: 使用 10MB 文件测试不同缓冲区大小
LOGI("\n***** 第一组: 10MB 文件,测试不同缓冲区 *****");
http_get_request("/random?size=10", "10MB - 2KB 缓冲区", 10, BUF_SIZE_2KB);
http_get_request("/random?size=10", "10MB - 8KB 缓冲区", 10, BUF_SIZE_8KB);
http_get_request("/random?size=10", "10MB - 16KB 缓冲区", 10, BUF_SIZE_16KB);
http_get_request("/random?size=10", "10MB - 32KB 缓冲区", 10, BUF_SIZE_32KB);
http_get_request("/random?size=10", "10MB - 64KB 缓冲区", 10, BUF_SIZE_64KB);
http_get_request("/random?size=10", "10MB - 128KB 缓冲区", 10, BUF_SIZE_128KB);
// 测试方案 2: 使用 50MB 文件测试关键缓冲区大小
LOGI("\n***** 第二组: 50MB 文件,测试关键缓冲区 *****");
http_get_request("/random?size=50", "50MB - 2KB 缓冲区", 50, BUF_SIZE_2KB);
http_get_request("/random?size=50", "50MB - 16KB 缓冲区", 50, BUF_SIZE_16KB);
http_get_request("/random?size=50", "50MB - 64KB 缓冲区", 50, BUF_SIZE_64KB);
http_get_request("/random?size=50", "50MB - 128KB 缓冲区", 50, BUF_SIZE_128KB);
// 测试方案 3: 使用 100MB 文件测试最优缓冲区
LOGI("\n***** 第三组: 100MB 文件,测试最优缓冲区 *****");
http_get_request("/random?size=100", "100MB - 16KB 缓冲区", 100, BUF_SIZE_16KB);
http_get_request("/random?size=100", "100MB - 64KB 缓冲区", 100, BUF_SIZE_64KB);
http_get_request("/random?size=100", "100MB - 128KB 缓冲区", 100, BUF_SIZE_128KB);
// 打印汇总报告
print_summary_report();
LOGI("\n========================================");
LOGI("所有测试完成");
LOGI("========================================");
return 0;
}

View File

@@ -0,0 +1,21 @@
.. _samples_network:
网络示例
========
.. toctree::
:maxdepth: 1
wifi_single_core/README.md
wifi_ble_single_core/README.md
wifi_bt_single_core/README.md
wifi_dual_core/index_zh
http/README.md
http_download_perf/README.md
websocket/README.md
mqtt/tcp/README.md
mqtt/ssl/README.md
mqtt/ws/README.md
mqtt/wss/README.md
mqtt/agent/README.md

View File

@@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(listenai-cmake REQUIRED HINTS $ENV{ARCS_BASE})
project(arcs)
# 定义宏,使用 MQTT 默认配置而不是自定义配置
add_definitions(-DMQTT_DO_NOT_USE_CUSTOM_CONFIG)
add_definitions(-DMQTT_AGENT_DO_NOT_USE_CUSTOM_CONFIG)
listenai_include_directories(
./
)
add_subdirectory(src)
listenai_add_executable(${PROJECT_NAME})

View File

@@ -0,0 +1 @@
osource "$ARCS_BASE/Kconfig"

View File

@@ -0,0 +1,310 @@
# MQTT Agent 示例
本示例展示了如何使用 coreMQTT-Agent 库实现线程安全的 MQTT 客户端应用。
## 功能特性
- 基于 coreMQTT-Agent 的线程安全 MQTT 操作
- 使用 FreeRTOS 队列进行线程间消息传递
- 支持异步订阅和发布操作
- 命令完成回调机制
- 多任务环境下的 MQTT 通信
## coreMQTT-Agent 简介
coreMQTT-Agent 是对 coreMQTT 库的线程安全封装,主要特性包括:
### 1. 线程安全
- 所有 MQTT 操作通过命令队列序列化执行
- 避免多线程环境下的竞态条件
- 支持从多个任务中安全调用 MQTT API
### 2. 异步操作模型
- 订阅/发布操作不会阻塞调用者
- 通过回调函数通知操作完成
- 可使用信号量实现同步等待
### 3. 专用 Agent 任务
- 运行 `MQTTAgent_CommandLoop()` 处理所有 MQTT 操作
- 统一管理网络收发和 MQTT 协议处理
- 简化多线程 MQTT 应用开发
## 架构设计
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Publisher │ │ Subscriber │ │ Other Task │
│ Task │ │ Task │ │ │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ Command Queue │ Command Queue │
├───────────────────┼───────────────────┤
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────┐
│ MQTT Agent Task (Command Loop) │
│ ┌──────────────────────────────────────────────┐ │
│ │ MQTTAgent_CommandLoop() │ │
│ │ - 接收命令队列中的命令 │ │
│ │ - 执行 MQTT 操作 (Subscribe/Publish/etc) │ │
│ │ - 处理网络事件和消息 │ │
│ │ - 调用完成回调 │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────┬─────────────────────────────┘
┌─────────────────┐
│ MQTT Broker │
└─────────────────┘
```
## 核心 API
### Agent 初始化
```c
MQTTStatus_t MQTTAgent_Init(
MQTTAgentContext_t * pMqttAgentContext,
const MQTTAgentMessageInterface_t * pMsgInterface,
const MQTTFixedBuffer_t * pNetworkBuffer,
const TransportInterface_t * pTransportInterface,
MQTTGetCurrentTimeFunc_t getCurrentTimeMs,
IncomingPubCallback_t incomingCallback,
void * pIncomingPacketContext
);
```
### Agent 命令循环 (在专用任务中运行)
```c
MQTTStatus_t MQTTAgent_CommandLoop(MQTTAgentContext_t * pMqttAgentContext);
```
### 线程安全的订阅操作
```c
MQTTStatus_t MQTTAgent_Subscribe(
const MQTTAgentContext_t * pMqttAgentContext,
MQTTAgentSubscribeArgs_t * pSubscriptionArgs,
MQTTAgentCommandInfo_t * pCommandInfo
);
```
### 线程安全的发布操作
```c
MQTTStatus_t MQTTAgent_Publish(
const MQTTAgentContext_t * pMqttAgentContext,
MQTTPublishInfo_t * pPublishInfo,
MQTTAgentCommandInfo_t * pCommandInfo
);
```
## 示例代码说明
### 1. 消息接口实现
示例实现了 `MQTTAgentMessageInterface_t` 结构,使用 FreeRTOS 队列:
```c
MQTTAgentMessageInterface_t messageInterface = {
.pMsgCtx = &g_command_queue_context,
.send = agent_send_command, // 发送命令到队列
.recv = agent_recv_command, // 从队列接收命令
.getCommand = agent_get_command, // 获取空闲命令结构
.releaseCommand = agent_release_command // 释放命令结构
};
```
### 2. Agent 任务
运行 `MQTTAgent_CommandLoop()` 的专用任务:
```c
static void mqtt_agent_task(void *arg)
{
MQTTStatus_t status = MQTTAgent_CommandLoop(&g_agent_context);
// 命令循环退出表示连接断开或发生错误
}
```
### 3. 异步订阅
从任意任务中调用,不会阻塞:
```c
MQTTAgentSubscribeArgs_t subscribeArgs = {
.pSubscribeInfo = subscribeInfo,
.numSubscriptions = 1
};
MQTTAgentCommandInfo_t commandInfo = {
.cmdCompleteCallback = subscribe_command_callback,
.pCmdCompleteCallbackContext = &subscribe_sem
};
MQTTAgent_Subscribe(&g_agent_context, &subscribeArgs, &commandInfo);
```
### 4. 异步发布
支持 QoS 0/1/2,使用回调通知完成:
```c
MQTTPublishInfo_t publishInfo = {
.qos = MQTTQoS1,
.pTopicName = "/test/topic",
.topicNameLength = strlen("/test/topic"),
.pPayload = "Hello from Agent!",
.payloadLength = strlen("Hello from Agent!")
};
MQTTAgentCommandInfo_t commandInfo = {
.cmdCompleteCallback = publish_command_callback,
.pCmdCompleteCallbackContext = &publish_sem
};
MQTTAgent_Publish(&g_agent_context, &publishInfo, &commandInfo);
```
### 5. 命令完成回调
操作完成后在 Agent 任务上下文中调用:
```c
static void subscribe_command_callback(MQTTAgentCommandContext_t *pCommandContext,
MQTTAgentReturnInfo_t *pReturnInfo)
{
SemaphoreHandle_t sem = (SemaphoreHandle_t)pCommandContext;
if (pReturnInfo->returnCode == MQTTSuccess) {
CLOG("Subscribe command succeeded");
} else {
CLOG("Subscribe command failed: %d", pReturnInfo->returnCode);
}
xSemaphoreGive(sem); // 通知等待的任务
}
```
## 配置说明
### prj.conf 配置
```ini
# 启用 coreMQTT 基础库
CONFIG_SDK_MODULE_COREMQTT=y
# 启用 coreMQTT-Agent (线程安全封装)
CONFIG_SDK_MODULE_COREMQTT_AGENT=y
```
### 网络配置
修改 `main.c` 中的连接参数:
```c
#define MQTT_BROKER_ADDR "broker.emqx.io" // MQTT Broker 地址
#define MQTT_BROKER_PORT "1883" // MQTT Broker 端口
#define MQTT_CLIENT_ID "arcs_mqtt_agent_client" // 客户端 ID
```
### 编译
```{eval-rst}
.. include:: /sample_build.rst
```
### 配置 WiFi
在设备串口终端中执行:
```bash
# 配置 WiFi SSID
kv set wifi_ssid "your_wifi_name"
# 配置 WiFi 密码
kv set wifi_passwd "your_wifi_password"
# 重启设备
reboot
```
### 运行效果
设备启动后会自动:
1. 初始化文件系统
2. 连接 WiFi 网络
3. 连接到 MQTT Broker
4. 启动 Agent 任务
5. 订阅 `/test/topic` 主题
6. 定期发布消息到 `/test/topic`
## 与标准 coreMQTT 的对比
| 特性 | coreMQTT | coreMQTT-Agent |
|------|----------|----------------|
| 线程安全 | 否,需要手动加锁 | 是,内置线程安全 |
| 阻塞模式 | 同步阻塞 | 异步非阻塞 |
| 多任务支持 | 需要手动同步 | 原生支持 |
| 复杂度 | 较低 | 中等 |
| 适用场景 | 单线程应用 | 多线程 RTOS 应用 |
| 性能开销 | 较小 | 略高(队列和回调) |
## 测试方法
### 使用 MQTTX 订阅消息
```bash
# 订阅设备发布的消息
mqttx sub -h broker.emqx.io -p 1883 -t "arcs/test/pub"
```
### 使用 MQTTX 发布消息
```bash
# 向设备发布消息
mqttx pub -h broker.emqx.io -p 1883 -t "arcs/test/sub" -m "Hello ARCS Mqtt Agent"
```
设备会收到消息并在串口打印。
## 注意事项
1. **任务优先级**: Agent 任务应有足够高的优先级,确保及时处理网络事件
2. **栈大小**: Agent 任务需要较大的栈空间(建议 ≥ 4KB)
3. **队列深度**: 命令队列深度根据并发命令数量调整
4. **回调上下文**: 回调函数在 Agent 任务中执行,避免长时间阻塞操作
5. **命令池**: 示例使用静态命令池,可根据需要调整大小
## 故障排除
### 编译错误: 找不到 core_mqtt_agent.h
确保 `prj.conf` 中启用了:
```ini
CONFIG_SDK_MODULE_COREMQTT_AGENT=y
```
### Agent 命令循环退出
检查:
- 网络连接是否稳定
- Broker 地址和端口是否正确
- Keep-alive 超时设置是否合理
### 订阅/发布超时
- 检查命令队列是否已满
- 确认 Agent 任务正常运行
- 验证网络连接状态
## 参考资料
- [coreMQTT-Agent GitHub](https://github.com/FreeRTOS/coreMQTT-Agent)
- [coreMQTT GitHub](https://github.com/FreeRTOS/coreMQTT)
- [MQTT 协议规范](https://mqtt.org/mqtt-specification/)

View File

@@ -0,0 +1,177 @@
#!/bin/bash
set -e
usage() {
echo "使用方式: $0 [选项]"
echo "选项:"
echo " -S, --Source <path> 指定项目源码路径 (默认为当前脚本所在目录)"
echo " -t, --target <target> 指定构建目标 (如 menuconfig)"
echo " -C, --Clean 清理构建目录"
echo " -B, --build 构建输出目录"
echo " -h, --help 显示此帮助信息"
echo " -r, --release 以 Release 模式构建 (移除 DEBUG_PATH 信息)"
echo " -w, --warnings-as-errors 将警告视为错误"
echo " -D<var>=<value> 传递 CMake 变量 (可多次使用)"
echo ""
echo "示例:"
echo " $0 -S samples/helloworld -DBOARD=arcs_mini 指定板型构建"
echo " $0 -S samples/helloworld -DBOARD=arcs_evb 使用 EVB 板型"
echo " $0 -S samples/helloworld -t menuconfig -DBOARD=arcs_mini 运行 menuconfig"
echo " $0 -C -S samples/helloworld -DBOARD=arcs_mini 清理并重新构建"
echo " $0 -S samples/helloworld -DBOARD=my_board -DBOARD_SEARCH_PATH=/path/to/boards 使用自定义板型"
exit 1
}
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_PATH="$SCRIPT_DIR"
TARGET=""
CLEAN=false
OUTPUT="build"
WARNINGS_AS_ERRORS=false
RELEASE=false
ARCS_BASE_DIR_NAME="arcs-sdk"
ARCS_DEV_TOOLS_DIR_NAME="listenai-dev-tools"
ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME="gcc"
ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME="listenai-tools"
find_arcs_base() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_BASE_DIR_NAME"
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found ARCS_BASE: $current_dir/$dir_name"
export ARCS_BASE="$current_dir/$dir_name"
return 0
fi
current_dir=$(dirname "$current_dir")
done
echo "ARCS_BASE not found, Please add ARCS_BASE environment variable or set ARCS_BASE_DIR_NAME to the correct directory."
echo "Current Target ARCS_BASE directory name: $ARCS_BASE_DIR_NAME."
exit 1
}
find_dev_tools() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_DEV_TOOLS_DIR_NAME"
echo "trying to find $dir_name in parent directories..."
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found $dir_name: $current_dir/$dir_name"
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME" ]; then
echo "Found LISTENAI_TOOLS_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
export LISTENAI_TOOLS_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
fi
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME" ]; then
echo "Found NUCLEI_TOOLCHAIN_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
export NUCLEI_TOOLCHAIN_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
fi
return 0
fi
current_dir=$(dirname "$current_dir")
done
}
while [[ $# -gt 0 ]]; do
case $1 in
-S|--Source)
PROJECT_PATH="$2"
shift 2
;;
-B|--build)
OUTPUT="$2"
shift 2
;;
-t|--target)
TARGET="$2"
shift 2
;;
-C|--Clean)
CLEAN=true
shift 1
;;
-w|--warnings-as-errors)
WARNINGS_AS_ERRORS=true
shift 1
;;
-h|--help)
usage
;;
-r|--release)
RELEASE=true
shift 1
;;
-D*)
CMAKE_VARS+=("$1")
shift 1
;;
*)
echo "未知参数: $1"
usage
;;
esac
done
echo "Source: $PROJECT_PATH"
echo "Target: $TARGET"
echo "Clean : $CLEAN"
if [ -z "$LISTENAI_TOOLS_PATH" ] || [ -z "$NUCLEI_TOOLCHAIN_PATH" ]; then
find_dev_tools
fi
if [ -z "${LISTENAI_TOOLS_PATH}" ]; then
export LISTENAI_TOOLS_PATH="请添加 LISTENAI_TOOLS_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 LISTENAI_TOOLS_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
if [ -z "${NUCLEI_TOOLCHAIN_PATH}" ]; then
export NUCLEI_TOOLCHAIN_PATH="请添加 NUCLEI_TOOLCHAIN_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 NUCLEI_TOOLCHAIN_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
############### 下面代码不用修改 ##################
# 构建工具的位置
CMAKE_PROGRAM="$LISTENAI_TOOLS_PATH/cmake/bin/cmake"
NINJA_PROGRAM="$LISTENAI_TOOLS_PATH/ninja/ninja"
# 配置环境变量 ARCS_BASE
if [ -z "$ARCS_BASE" ]; then
find_arcs_base
fi
if [ "$CLEAN" = true ]; then
rm -rf $OUTPUT
fi
# Initialize CMAKE_VARS array if it doesn't exist
declare -a CMAKE_VARS
# Add warnings-as-errors flag if enabled
if [ "$WARNINGS_AS_ERRORS" = true ]; then
CMAKE_VARS+=("-DCMAKE_C_FLAGS=-Werror")
CMAKE_VARS+=("-DCMAKE_CXX_FLAGS=-Werror")
echo "Treating warnings as errors"
fi
# Add release flags if enabled
if [ "$RELEASE" = true ]; then
CMAKE_VARS+=("-DENABLE_DEBUG_PATH=OFF")
echo "Release mode enabled (-DENABLE_DEBUG_PATH=OFF)"
fi
$CMAKE_PROGRAM -B "$OUTPUT" -G Ninja -S "$PROJECT_PATH" \
-DCMAKE_MAKE_PROGRAM="$NINJA_PROGRAM" \
"${CMAKE_VARS[@]}"
if [ -z "$TARGET" ]; then
$CMAKE_PROGRAM --build "$OUTPUT" -j4
else
$CMAKE_PROGRAM --build "$OUTPUT" --target "$TARGET"
fi

View File

@@ -0,0 +1,54 @@
CONFIG_HEAP_SIZE=0xC000
CONFIG_PSRAM_HEAP_SIZE=0x5b0000
# DISK驱动使能
CONFIG_DISK_DRIVER=y
# 文件系统配置
CONFIG_FILE_SYSTEM=y
CONFIG_FATFS_FILESYSTEM=y
CONFIG_LSFS=y
CONFIG_LSFS_FAT=y
CONFIG_LSFS_REGISTER_VFS=y
CONFIG_LVFS_POSIX_API_ALIAS=y
CONFIG_LVFS=y
CONFIG_LVFS_POSIX_API=y
CONFIG_FS_ENV_MEMORY_MANAGEMENT_USE_PSRAM=y
# 网络配置
CONFIG_LISA_WIFI=y
CONFIG_WIFI_LWIP_SAME_CORE=y
# MAC Manager
CONFIG_MAC_MANAGER=y
CONFIG_MAC_MANAGER_MAC_BY_CHIP_ID=y
# WIFI MANAGER
CONFIG_WIFI_MANAGER=y
# LISA Shell
CONFIG_LISA_SHELL=y
CONFIG_SYSLOG_PRINTF_REDIRECT=y
# LISA Porting
CONFIG_LISA_PORTING=y
# SD Card drivers
CONFIG_DISK_DRIVER_SDMMC=y
CONFIG_LISA_SDMMC_DEVICE=y
CONFIG_CJSON=y
CONFIG_LISA_OS=y
CONFIG_LISA_KV=y
CONFIG_LISA_NETWORK=y
# coreMQTT
CONFIG_SDK_MODULE_COREMQTT=y
# coreMQTT-Agent (线程安全的MQTT封装)
CONFIG_SDK_MODULE_COREMQTT_AGENT=y
CONFIG_BACK_TRACE=y

View File

@@ -0,0 +1,3 @@
tests:
samples.mqtt.agent:
build_only: true

View File

@@ -0,0 +1,13 @@
listenai_library_named(app)
add_definitions(-DMQTT_AGENT_DO_NOT_USE_CUSTOM_CONFIG)
listenai_library_sources(
main.c
fs/user_fs.c
)
listenai_include_directories(
./
./fs
)

View File

@@ -0,0 +1,86 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lsfs.h"
#include "disk/disk_access.h"
#include "lisa_sdmmc.h"
#include "log_print.h"
#if CONFIG_LVFS
#include "lvfs.h"
#endif
#define DISK_DEVICE "SD:"
#define DISK_MOUNT_POINT "/" DISK_DEVICE
static struct lsfs_mount_t flash_lsfs_mnt = {
.type = LSFS_FATFS,
.mnt_point = DISK_MOUNT_POINT,
.fs_data = NULL,
};
static int fs_mount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_mount(mp);
if (ret != 0)
{
CLOG("No file system, try reformatting, mount ret: %d", ret);
ret = lsfs_mkfs(mp->type, &mp->mnt_point[1], NULL, 0);
if (ret == 0)
{
CLOG("mkfs success, retry mount");
ret = lsfs_mount(mp);
}
else
{
CLOG("mkfs failed, ret: %d", ret);
}
}
if (ret != 0)
{
CLOG("Failed to mount filesystem: %d", ret);
return ret;
}
CLOG("%s mounted successfully", mp->mnt_point);
return 0;
}
static void user_fs_change_to_root_folder(void)
{
int ret;
ret = lvfs_chdrive(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change drive to %s, error: %d\n", DISK_MOUNT_POINT, ret);
}
ret = lvfs_chdir(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change directory to /SD:/, error: %d\n", ret);
} else {
CLOG("Changed to directory /SD:/\n");
}
}
int user_fs_init(void){
lisa_sdmmc_probe(lisa_device_get("sdmmc0"));
disk_init(NULL);
#if CONFIG_LVFS
lvfs_init();
#endif
lsfs_init();
if(0 != fs_mount(&flash_lsfs_mnt)){
return -1;
}
user_fs_change_to_root_folder();
return 0;
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef USER_FS_H
#define USER_FS_H
int user_fs_init(void);
#endif /* USER_FS_H */

View File

@@ -0,0 +1,617 @@
#include "lisa_thread.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "stdbool.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "queue.h"
#define TAG "mqtt-agent-test"
#include "lisa_log.h"
#include "lisa_kv.h"
#include "mac_manager.h"
#include "mac_manager_ops.h"
#include "wifi_manager/wifi_manager.h"
#include "user_fs.h"
#include "lisa_wifi.h"
#include "ls_wifi_type.h"
#include "net_al.h"
#include "net_def.h"
#include "core_mqtt_agent.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
#define MQTT_BROKER_HOST "broker.emqx.io"
#define MQTT_BROKER_PORT 1883
#define MQTT_CLIENT_ID "arcs_mqtt_agent_client"
#define TARGET_WIFI_SSID "Xiaomi_listenai_2.4G"
#define TARGET_WIFI_PWD "a12345678"
#define MQTT_TOPIC_PUB "arcs/test/pub"
#define MQTT_TOPIC_SUB "arcs/test/sub"
#define WIFI_MGR_AUTO_CONNECT_INTERVAL_MS 2000
#define WIFI_MGR_SEARCH_AP_BUFFER_SIZE 10
#define NETWORK_BUFFER_SIZE 2048
#define AGENT_CMD_QUEUE_LENGTH 25
#define AGENT_TASK_STACK_SIZE 4096
#define AGENT_TASK_PRIORITY 5
static mac_manager_t *m_mac_manager;
static wifi_mgr_sta_config_t list[WIFI_MGR_SEARCH_AP_BUFFER_SIZE] = { 0 };
static volatile bool g_wifi_connected = false;
static volatile bool g_get_ip_success = false;
static volatile bool g_agent_connected = false;
/* 网络上下文 */
struct NetworkContext {
int socket;
};
/* 定义 MQTTAgentMessageContext 结构体 */
struct MQTTAgentMessageContext {
QueueHandle_t queue;
};
/* 定义 MQTTAgentCommandContext 结构体 */
struct MQTTAgentCommandContext {
SemaphoreHandle_t semaphore;
MQTTStatus_t returnStatus;
};
/* MQTT Agent 上下文和缓冲区 */
static MQTTAgentContext_t g_agent_context;
static uint8_t g_network_buffer[NETWORK_BUFFER_SIZE];
static struct MQTTAgentMessageContext g_command_queue_context;
static MQTTAgentCommand_t g_command_queue[AGENT_CMD_QUEUE_LENGTH];
static struct NetworkContext g_network_context = {0};
/* FreeRTOS 队列用于命令传递 */
static QueueHandle_t g_agent_queue;
static SemaphoreHandle_t g_connect_semaphore;
/* MQTT Agent 任务句柄 */
static TaskHandle_t g_agent_task_handle = NULL;
static int8_t custom_get_wifi_mac(uint8_t mac_addr[6])
{
int8_t ret = 0;
if (!mac_addr)
return -1;
ret = mac_manager_get(m_mac_manager, mac_addr, 6);
LOGI("custom_get_wifi_mac: %d, %02X:%02X:%02X:%02X:%02X:%02X", ret,
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
return ret;
}
static void dhcp_status_callback(int vif_idx, bool success, uint32_t ip_addr,
uint32_t netmask, uint32_t gateway, void *arg)
{
if (success) {
LOGI("DHCP Success: IP=%d.%d.%d.%d",
ip_addr & 0xff, (ip_addr >> 8) & 0xff,
(ip_addr >> 16) & 0xff, (ip_addr >> 24) & 0xff);
g_get_ip_success = true;
} else {
LOGI("DHCP Failed on VIF-%d", vif_idx);
}
}
static void wifi_mgr_connection_status_cb(wifi_mgr_connection_info_t *connection_info, void *arg)
{
net_if_t *net_if;
LOGI("WiFi connection status: %d", connection_info->status);
switch (connection_info->status) {
case WIFI_MGR_STA_CONNECTED:
LOGI("WiFi connected to AP");
g_wifi_connected = true;
net_if = net_if_get(WIFI_VIF_STA_IDX);
net_if_up(net_if);
if (!net_if->static_ip) {
ls_dhcpc_start(WIFI_VIF_STA_IDX);
}
break;
case WIFI_MGR_STA_DISCONNECTED:
LOGI("WiFi disconnected from AP");
g_wifi_connected = false;
g_agent_connected = false;
ls_dhcpc_stop(WIFI_VIF_STA_IDX);
net_if_down(net_if_get(WIFI_VIF_STA_IDX));
break;
default:
break;
}
}
static void user_mac_manager_init(void)
{
mac_manager_config_t config = {
.random_mac_if_mac_invalid = false,
};
m_mac_manager = mac_manager_init(mac_manager_ops_get()->mem_ops,
mac_manager_ops_get()->content_ops, &config);
if (m_mac_manager == NULL) {
LOGI("mac_manager_init failed\n");
}
}
static void user_wifi_manager_init(void)
{
wifi_mgr_sta_config_t cfg = {
.ssid = TARGET_WIFI_SSID,
.pwd = TARGET_WIFI_PWD,
};
wifi_mgr_autoconn_config_t autoconn_cfg = {
.interval_ms = WIFI_MGR_AUTO_CONNECT_INTERVAL_MS,
};
wifi_mgr_init(wifi_mgr_ops_get());
wifi_mgr_sta_enable();
wifi_mgr_sta_add_connection_cb(wifi_mgr_connection_status_cb, NULL);
int count = wifi_mgr_storage_search_ap(list, WIFI_MGR_SEARCH_AP_BUFFER_SIZE, SEARCH_ALL, NULL);
for (int i = 0; i < count; i++) {
LOGI("Deleting saved AP: ssid=%s", list[i].ssid);
wifi_mgr_storage_delete_ap(&list[i]);
}
wifi_mgr_storage_save_ap(&cfg);
wifi_mgr_auto_connect_start(&autoconn_cfg);
LOGI("WiFi auto-connect started");
}
static int wait_for_wifi_connection(void)
{
int timeout = 30;
LOGI("Waiting for WiFi connection and IP address...");
while (timeout > 0) {
if (g_wifi_connected && g_get_ip_success) {
LOGI("WiFi connected and IP obtained");
vTaskDelay(pdMS_TO_TICKS(500));
return 0;
}
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
if (timeout % 5 == 0) {
LOGI("Waiting... (WiFi:%d, IP:%d, timeout:%d)", g_wifi_connected, g_get_ip_success, timeout);
}
}
LOGI("WiFi connection timeout");
return -1;
}
/* Transport 接口实现 - 处理非阻塞 socket */
static int32_t transport_recv(NetworkContext_t *pNetworkContext, void *pBuffer, size_t bytesToRecv)
{
int bytes_received = recv(pNetworkContext->socket, pBuffer, bytesToRecv, 0);
/* 非阻塞 socket: EAGAIN/EWOULDBLOCK 表示暂时没有数据 */
if (bytes_received < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* 没有数据可读,返回 0 让 MQTT 库知道需要稍后重试 */
return 0;
}
/* 其他错误返回负值 */
return bytes_received;
}
return (int32_t)bytes_received;
}
static int32_t transport_send(NetworkContext_t *pNetworkContext, const void *pBuffer, size_t bytesToSend)
{
int bytes_sent = send(pNetworkContext->socket, pBuffer, bytesToSend, 0);
return (int32_t)bytes_sent;
}
static uint32_t get_time_ms(void)
{
return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
/* Agent 消息队列接口实现 */
static bool agent_send_command(MQTTAgentMessageContext_t *pMsgCtx,
MQTTAgentCommand_t * const * pCommandToSend,
uint32_t blockTimeMs)
{
BaseType_t ret;
TickType_t xTicksToWait = pdMS_TO_TICKS(blockTimeMs);
ret = xQueueSend(g_agent_queue, pCommandToSend, xTicksToWait);
return (ret == pdPASS);
}
static bool agent_recv_command(MQTTAgentMessageContext_t *pMsgCtx,
MQTTAgentCommand_t **pReceivedCommand,
uint32_t blockTimeMs)
{
BaseType_t ret;
TickType_t xTicksToWait = pdMS_TO_TICKS(blockTimeMs);
ret = xQueueReceive(g_agent_queue, pReceivedCommand, xTicksToWait);
return (ret == pdPASS);
}
static MQTTAgentCommand_t *agent_get_command(uint32_t blockTimeMs)
{
/* 简单实现:从静态数组中分配 */
static uint32_t cmd_index = 0;
MQTTAgentCommand_t *pCommand = NULL;
/* 在实际应用中,这里应该使用内存池或其他线程安全的分配机制 */
taskENTER_CRITICAL();
if (cmd_index < AGENT_CMD_QUEUE_LENGTH) {
pCommand = &g_command_queue[cmd_index];
cmd_index++;
}
taskEXIT_CRITICAL();
return pCommand;
}
static bool agent_release_command(MQTTAgentCommand_t *pCommandToRelease)
{
/* 简单实现:不做任何事,因为我们使用静态数组 */
return true;
}
/* MQTT Agent 接收消息回调 */
static void incoming_publish_callback(MQTTAgentContext_t *pAgentContext,
uint16_t packetId,
MQTTPublishInfo_t *pPublishInfo)
{
LOGI("Incoming publish received: topic=%.*s, payload=%.*s",
(int)pPublishInfo->topicNameLength, pPublishInfo->pTopicName,
(int)pPublishInfo->payloadLength, (char *)pPublishInfo->pPayload);
}
/* 命令完成回调 */
static void command_complete_callback(MQTTAgentCommandContext_t *pCommandContext,
MQTTAgentReturnInfo_t *pReturnInfo)
{
if (pCommandContext != NULL) {
SemaphoreHandle_t *pSem = (SemaphoreHandle_t *)pCommandContext;
xSemaphoreGive(*pSem);
}
if (pReturnInfo->returnCode != MQTTSuccess) {
LOGE("Command failed with return code: %d", pReturnInfo->returnCode);
}
}
/* MQTT 连接到 Broker */
static int connect_to_broker(struct NetworkContext *pNetworkContext)
{
struct sockaddr_in broker_addr;
struct hostent *host_entry;
LOGI("Resolving broker hostname: %s", MQTT_BROKER_HOST);
host_entry = gethostbyname(MQTT_BROKER_HOST);
if (host_entry == NULL) {
LOGE("Failed to resolve hostname");
return -1;
}
pNetworkContext->socket = socket(AF_INET, SOCK_STREAM, 0);
if (pNetworkContext->socket < 0) {
LOGE("Failed to create socket");
return -1;
}
memset(&broker_addr, 0, sizeof(broker_addr));
broker_addr.sin_family = AF_INET;
broker_addr.sin_port = htons(MQTT_BROKER_PORT);
memcpy(&broker_addr.sin_addr.s_addr, host_entry->h_addr, host_entry->h_length);
LOGI("Connecting to broker %s:%d...", MQTT_BROKER_HOST, MQTT_BROKER_PORT);
if (connect(pNetworkContext->socket, (struct sockaddr *)&broker_addr, sizeof(broker_addr)) < 0) {
LOGE("Failed to connect to broker");
close(pNetworkContext->socket);
return -1;
}
/* 设置 Socket 为非阻塞模式 - 使用 LwIP 的 ioctl */
int non_blocking = 1;
if (ioctlsocket(pNetworkContext->socket, FIONBIO, &non_blocking) < 0) {
LOGE("Failed to set socket to non-blocking");
close(pNetworkContext->socket);
return -1;
}
LOGI("Connected to MQTT broker successfully (non-blocking mode)");
return 0;
}
/* MQTT Agent 任务 */
static void mqtt_agent_task(void *pvParameters)
{
MQTTStatus_t mqttStatus;
MQTTConnectInfo_t connectInfo = {0};
TransportInterface_t transport = {0};
MQTTFixedBuffer_t networkBuffer;
bool sessionPresent = false;
LOGI("MQTT Agent task started");
/* 连接到 Broker */
if (connect_to_broker(&g_network_context) != 0) {
LOGE("Failed to connect to broker, agent task exiting");
vTaskDelete(NULL);
return;
}
/* 设置 Transport 接口 */
transport.recv = transport_recv;
transport.send = transport_send;
transport.pNetworkContext = &g_network_context;
/* 设置网络缓冲区 */
networkBuffer.pBuffer = g_network_buffer;
networkBuffer.size = NETWORK_BUFFER_SIZE;
/* 初始化消息接口 */
MQTTAgentMessageInterface_t messageInterface = {
.pMsgCtx = &g_command_queue_context,
.send = agent_send_command,
.recv = agent_recv_command,
.getCommand = agent_get_command,
.releaseCommand = agent_release_command
};
/* 初始化 MQTT Agent */
mqttStatus = MQTTAgent_Init(&g_agent_context,
&messageInterface,
&networkBuffer,
&transport,
get_time_ms,
incoming_publish_callback,
NULL);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTTAgent_Init failed: %d", mqttStatus);
close(g_network_context.socket);
vTaskDelete(NULL);
return;
}
/* 建立 MQTT 连接 */
connectInfo.cleanSession = true;
connectInfo.pClientIdentifier = MQTT_CLIENT_ID;
connectInfo.clientIdentifierLength = strlen(MQTT_CLIENT_ID);
connectInfo.keepAliveSeconds = 60;
mqttStatus = MQTT_Connect(&g_agent_context.mqttContext, &connectInfo, NULL, 5000, &sessionPresent);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Connect failed: %d", mqttStatus);
close(g_network_context.socket);
vTaskDelete(NULL);
return;
}
LOGI("MQTT Agent connected to broker");
g_agent_connected = true;
/* 通知主任务连接成功 */
xSemaphoreGive(g_connect_semaphore);
/* 进入 Agent 命令循环 */
LOGI("Entering MQTT Agent command loop...");
mqttStatus = MQTTAgent_CommandLoop(&g_agent_context);
LOGE("MQTT Agent command loop exited with status: %d", mqttStatus);
g_agent_connected = false;
/* 清理 */
close(g_network_context.socket);
vTaskDelete(NULL);
}
/* 订阅回调 */
static void subscribe_command_callback(MQTTAgentCommandContext_t *pCommandContext,
MQTTAgentReturnInfo_t *pReturnInfo)
{
if (pReturnInfo->returnCode == MQTTSuccess) {
LOGI("Subscribe command completed successfully");
} else {
LOGE("Subscribe command failed: %d", pReturnInfo->returnCode);
}
if (pCommandContext != NULL && pCommandContext->semaphore != NULL) {
pCommandContext->returnStatus = pReturnInfo->returnCode;
xSemaphoreGive(pCommandContext->semaphore);
}
}
/* 发布回调 */
static void publish_command_callback(MQTTAgentCommandContext_t *pCommandContext,
MQTTAgentReturnInfo_t *pReturnInfo)
{
LOGI("[CALLBACK] Publish callback invoked, returnCode=%d", pReturnInfo->returnCode);
if (pReturnInfo->returnCode == MQTTSuccess) {
LOGI("✓ Publish command completed successfully");
} else {
LOGE("✗ Publish command failed with code: %d", pReturnInfo->returnCode);
}
if (pCommandContext != NULL && pCommandContext->semaphore != NULL) {
pCommandContext->returnStatus = pReturnInfo->returnCode;
LOGI("[CALLBACK] Releasing semaphore");
xSemaphoreGive(pCommandContext->semaphore);
} else {
LOGE("[CALLBACK] Context or semaphore is NULL!");
}
}
int main(int argc, char **argv)
{
MQTTStatus_t mqttStatus;
MQTTAgentSubscribeArgs_t subscribeArgs = {0};
MQTTSubscribeInfo_t subscribeInfo = {0};
MQTTAgentCommandInfo_t commandInfo = {0};
MQTTPublishInfo_t publishInfo = {0};
SemaphoreHandle_t command_semaphore;
BaseType_t xReturned;
LOGI("MQTT Agent Test Starting...");
/* 初始化 */
user_mac_manager_init();
net_dhcp_register_status_callback(dhcp_status_callback, NULL);
lisa_wifi_ops_t ops = { .custom_mac = custom_get_wifi_mac };
lisa_wifi_init(&ops);
user_fs_init();
lisa_kv_init();
user_wifi_manager_init();
if (wait_for_wifi_connection() != 0) {
LOGE("Failed to connect to WiFi");
return -1;
}
LOGI("\n========================================");
LOGI("MQTT Agent 功能测试");
LOGI("========================================\n");
/* 创建命令队列 */
g_agent_queue = xQueueCreate(AGENT_CMD_QUEUE_LENGTH, sizeof(MQTTAgentCommand_t *));
if (g_agent_queue == NULL) {
LOGE("Failed to create agent queue");
return -1;
}
g_command_queue_context.queue = g_agent_queue;
/* 创建信号量 */
g_connect_semaphore = xSemaphoreCreateBinary();
command_semaphore = xSemaphoreCreateBinary();
if (g_connect_semaphore == NULL || command_semaphore == NULL) {
LOGE("Failed to create semaphores");
return -1;
}
/* 创建 MQTT Agent 任务 */
xReturned = xTaskCreate(mqtt_agent_task,
"mqtt_agent",
AGENT_TASK_STACK_SIZE,
NULL,
AGENT_TASK_PRIORITY,
&g_agent_task_handle);
if (xReturned != pdPASS) {
LOGE("Failed to create MQTT Agent task");
return -1;
}
/* 等待 Agent 连接成功 */
if (xSemaphoreTake(g_connect_semaphore, pdMS_TO_TICKS(10000)) != pdTRUE) {
LOGE("Timeout waiting for agent connection");
return -1;
}
LOGI("\n=== 测试 1: MQTT SUBSCRIBE (使用 Agent API) ===");
/* 订阅主题 */
subscribeInfo.qos = MQTTQoS0;
subscribeInfo.pTopicFilter = MQTT_TOPIC_SUB;
subscribeInfo.topicFilterLength = strlen(MQTT_TOPIC_SUB);
subscribeArgs.pSubscribeInfo = &subscribeInfo;
subscribeArgs.numSubscriptions = 1;
struct MQTTAgentCommandContext subscribeContext = {
.semaphore = command_semaphore,
.returnStatus = MQTTSuccess
};
commandInfo.cmdCompleteCallback = subscribe_command_callback;
commandInfo.pCmdCompleteCallbackContext = &subscribeContext;
commandInfo.blockTimeMs = 5000;
mqttStatus = MQTTAgent_Subscribe(&g_agent_context, &subscribeArgs, &commandInfo);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTTAgent_Subscribe failed: %d", mqttStatus);
} else {
/* 等待订阅完成 */
xSemaphoreTake(command_semaphore, pdMS_TO_TICKS(5000));
LOGI("Subscribed to topic: %s", MQTT_TOPIC_SUB);
}
vTaskDelay(pdMS_TO_TICKS(1000));
LOGI("\n=== 测试 2: MQTT PUBLISH (使用 Agent API, 无回调) ===");
/* 发布消息 - 使用 QoS 0不等待回调 */
publishInfo.qos = MQTTQoS0;
publishInfo.pTopicName = MQTT_TOPIC_PUB;
publishInfo.topicNameLength = strlen(MQTT_TOPIC_PUB);
publishInfo.pPayload = "Hello from ARCS MQTT Agent!";
publishInfo.payloadLength = strlen("Hello from ARCS MQTT Agent!");
/* 不使用回调,直接发布 */
commandInfo.cmdCompleteCallback = NULL;
commandInfo.pCmdCompleteCallbackContext = NULL;
commandInfo.blockTimeMs = 5000;
LOGI("Publishing message to: %s", MQTT_TOPIC_PUB);
LOGI("Payload: %s", (char *)publishInfo.pPayload);
mqttStatus = MQTTAgent_Publish(&g_agent_context, &publishInfo, &commandInfo);
if (mqttStatus != MQTTSuccess) {
LOGE("✗ MQTTAgent_Publish failed: %d", mqttStatus);
} else {
LOGI("✓ Publish command queued successfully");
LOGI("Note: QoS 0 message sent without waiting for ACK");
}
/* 给 Agent 任务一些时间处理命令 */
vTaskDelay(pdMS_TO_TICKS(2000));
LOGI("✓ Message should have been sent to broker");
LOGI("\n=== 测试 3: 持续接收消息 ===");
LOGI("Agent 任务将在后台持续处理消息...");
/* 主任务保持运行,让 Agent 任务在后台处理消息 */
while (1) {
if (!g_agent_connected) {
LOGE("Agent disconnected, exiting");
break;
}
/* 定期发布消息 */
vTaskDelay(pdMS_TO_TICKS(10000));
publishInfo.pPayload = "Periodic message from MQTT Agent";
publishInfo.payloadLength = strlen("Periodic message from MQTT Agent");
commandInfo.cmdCompleteCallback = NULL;
commandInfo.pCmdCompleteCallbackContext = NULL;
commandInfo.blockTimeMs = 1000;
mqttStatus = MQTTAgent_Publish(&g_agent_context, &publishInfo, &commandInfo);
if (mqttStatus == MQTTSuccess) {
LOGI("✓ Sent periodic message to %s", MQTT_TOPIC_PUB);
} else {
LOGE("✗ Failed to send periodic message: %d", mqttStatus);
}
}
LOGI("\n========================================");
LOGI("MQTT Agent 测试完成");
LOGI("========================================\n");
return 0;
}

View File

@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(listenai-cmake REQUIRED HINTS $ENV{ARCS_BASE})
project(arcs)
listenai_include_directories(
./
)
add_subdirectory(src)
listenai_add_executable(${PROJECT_NAME})

View File

@@ -0,0 +1 @@
osource "$ARCS_BASE/Kconfig"

View File

@@ -0,0 +1,313 @@
# MQTT SSL/TLS 示例
本示例演示如何在 ARCS 平台上使用 coreMQTT 库通过 SSL/TLS 加密连接到 MQTT broker。
## 功能特性
- 使用 mbedTLS 实现 SSL/TLS 加密连接
- 支持安全的 MQTT 通信(端口 8883)
- WiFi 自动连接
- MQTT 发布/订阅功能
- 完整的 SSL/TLS 握手流程
- 非阻塞 I/O 操作
## 硬件要求
- ARCS 开发板
- SD 卡(用于文件系统)
- WiFi 网络环境
## 配置说明
在使用前,请修改 [src/main.c](src/main.c) 中的以下配置:
```c
// MQTT Broker 配置
#define MQTT_BROKER_HOST "broker.emqx.io" // MQTT broker 地址
#define MQTT_BROKER_PORT 8883 // SSL 端口(标准为 8883)
#define MQTT_CLIENT_ID "arcs_mqtt_ssl_client"
// WiFi 配置
#define TARGET_WIFI_SSID "your_wifi_ssid" // 修改为你的WiFi名称
#define TARGET_WIFI_PWD "your_wifi_pwd" // 修改为你的WiFi密码
// MQTT 主题
#define MQTT_TOPIC_PUB "arcs/test/pub" // 发布主题
#define MQTT_TOPIC_SUB "arcs/test/sub" // 订阅主题
```
## SSL/TLS 配置
### 证书验证模式
当前配置为**不验证服务器证书**模式(适用于测试):
```c
mbedtls_ssl_conf_authmode(&pNetworkContext->conf, MBEDTLS_SSL_VERIFY_NONE);
```
### 生产环境配置
在生产环境中,**强烈建议启用证书验证**:
1. 修改验证模式:
```c
mbedtls_ssl_conf_authmode(&pNetworkContext->conf, MBEDTLS_SSL_VERIFY_REQUIRED);
```
2. 加载 CA 证书:
```c
/* 加载根证书 */
const char *ca_cert_pem = "-----BEGIN CERTIFICATE-----\n"
"...\n"
"-----END CERTIFICATE-----\n";
ret = mbedtls_x509_crt_parse(&pNetworkContext->cacert,
(const unsigned char *)ca_cert_pem,
strlen(ca_cert_pem) + 1);
mbedtls_ssl_conf_ca_chain(&pNetworkContext->conf, &pNetworkContext->cacert, NULL);
```
## 编译
```{eval-rst}
.. include:: /sample_build.rst
```
## 运行效果
程序启动后,将依次执行以下步骤:
1. **WiFi 连接**: 自动连接到配置的 WiFi 网络
2. **SSL/TLS 连接**: 建立到 MQTT broker 的加密连接
3. **MQTT 连接**: 通过 SSL 通道建立 MQTT 连接
4. **订阅主题**: 订阅指定的 MQTT 主题
5. **发布消息**: 发布测试消息
6. **接收消息**: 持续监听和处理接收的消息
### 预期日志输出
```
[mqtt-ssl-test] MQTT SSL/TLS Test Starting...
[mqtt-ssl-test] WiFi connected and IP obtained
========================================
MQTT SSL/TLS 功能测试
========================================
[mqtt-ssl-test] Initializing SSL/TLS connection to broker.emqx.io:8883...
[mqtt-ssl-test] TCP connection established
[mqtt-ssl-test] Performing SSL/TLS handshake...
[mqtt-ssl-test] SSL/TLS handshake completed successfully
[mqtt-ssl-test] Connected to MQTT broker broker.emqx.io:8883 via SSL/TLS
=== 测试 1: MQTT CONNECT ===
[mqtt-ssl-test] MQTT Connected successfully
=== 测试 2: MQTT SUBSCRIBE ===
[mqtt-ssl-test] Subscribe request sent for topic: arcs/test/sub
=== 测试 3: MQTT PUBLISH ===
[mqtt-ssl-test] Published message to topic: arcs/test/pub
=== 测试 4: 接收消息 ===
[mqtt-ssl-test] Received PUBLISH: topic=arcs/test/sub, payload=...
```
## 主要差异(相比 TCP 版本)
| 特性 | TCP 版本 | SSL 版本 |
|-----|---------|---------|
| 端口 | 1883 | 8883 |
| 加密 | 无 | TLS 1.2+ |
| 库依赖 | lwIP sockets | mbedTLS |
| 连接建立 | 直接 TCP | TCP + SSL握手 |
| 数据传输 | socket send/recv | mbedtls_ssl_write/read |
| 缓冲区大小 | 1024 字节 | 2048 字节(SSL开销) |
## mbedTLS 配置 (prj.conf)
```ini
# mbedTLS SSL/TLS支持
CONFIG_MBEDTLS=y
CONFIG_MBEDTLS_BUILTIN=y
CONFIG_MBEDTLS_ENABLE_HEAP=y
CONFIG_MBEDTLS_HEAP_SIZE=0x8000
# mbedTLS功能配置
CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
CONFIG_MBEDTLS_KEY_EXCHANGE_RSA_ENABLED=y
CONFIG_MBEDTLS_SSL_CLI_C=y
CONFIG_MBEDTLS_SSL_TLS_C=y
CONFIG_MBEDTLS_X509_CRT_PARSE_C=y
CONFIG_MBEDTLS_PEM_PARSE_C=y
```
## 调试技巧
### 启用 mbedTLS 调试输出
代码中已包含调试回调函数:
```c
static void my_debug(void *ctx, int level, const char *file, int line, const char *str)
{
LOGI("%s:%04d: %s", file, line, str);
}
mbedtls_ssl_conf_dbg(&pNetworkContext->conf, my_debug, NULL);
```
### 常见错误码
- `-0x7780`: SSL - Memory allocation failed
- `-0x7200`: SSL - Fatal handshake failure
- `-0x7100`: SSL - Bad input data
- `-0x6800`: SSL - Certificate verification failed
使用 `mbedtls_strerror()` 可以获取详细错误信息。
## 功能测试
### 使用 MQTTX 客户端测试
[MQTTX](https://mqttx.app/) 是一个优秀的跨平台 MQTT 客户端工具,可用于测试设备的发布和订阅功能。
#### 安装 MQTTX CLI
```bash
# Linux
curl -LO https://www.emqx.com/en/downloads/MQTTX/v1.12.1/mqttx-cli-linux-x64
sudo install ./mqttx-cli-linux-x64 /usr/local/bin/mqttx
```
#### 测试场景 1: 订阅设备发布的消息
在电脑上运行以下命令,订阅设备发布的主题:
```bash
mqttx sub -h broker.emqx.io -p 8883 -t "arcs/test/pub" -l mqtts
```
**预期结果:**
```
✔ Connected
✔ Subscribed to arcs/test/pub
topic: arcs/test/pub, qos: 0, size: 24B
Hello from ARCS via SSL!
```
设备会定期发布消息到 `arcs/test/pub` 主题,MQTTX 客户端会接收并显示这些消息。
#### 测试场景 2: 向设备发布消息
在电脑上运行以下命令,向设备订阅的主题发送消息:
```bash
mqttx pub -h broker.emqx.io -p 8883 -t "arcs/test/sub" -m "Hello ARCS Girl" -l mqtts
```
**预期结果:**
MQTTX 客户端输出:
```
✔ Connected
✔ Message published
```
设备串口日志输出:
```
I/mqtt-ssl-test [1034:43:49.966 1 main] Received PUBLISH: topic=arcs/test/sub, payload=Hello ARCS Girl
```
#### MQTTX 参数说明
| 参数 | 说明 | 示例值 |
|-----|------|--------|
| `-h` | MQTT broker 地址 | `broker.emqx.io` |
| `-p` | 端口号 | `8883` (SSL端口) |
| `-t` | 主题名称 | `arcs/test/pub` |
| `-m` | 消息内容 | `"Hello ARCS Girl"` |
| `-l` | 协议类型 | `mqtts` (MQTT over SSL/TLS) |
| `-q` | QoS 级别 (可选) | `0`, `1`, `2` |
| `-u` | 用户名 (可选) | `username` |
| `-P` | 密码 (可选) | `password` |
#### 完整测试流程
1. **启动设备**: 编译并运行 SSL MQTT 示例程序
2. **订阅测试**: 在电脑上运行 MQTTX 订阅命令,验证能收到设备消息
3. **发布测试**: 在电脑上运行 MQTTX 发布命令,验证设备能收到消息
4. **双向通信**: 同时开启订阅和发布,观察双向消息传输
### 使用 Mosquitto 客户端测试
如果你已经安装了 Mosquitto 客户端工具:
```bash
# 订阅设备发布的消息
mosquitto_sub -h broker.emqx.io -p 8883 -t "arcs/test/pub" --capath /etc/ssl/certs/
# 向设备发布消息
mosquitto_pub -h broker.emqx.io -p 8883 -t "arcs/test/sub" -m "Hello from Mosquitto" --capath /etc/ssl/certs/
```
### 测试 Checklist
- [ ] WiFi 连接成功并获取 IP 地址
- [ ] SSL/TLS 握手成功完成
- [ ] MQTT 连接建立成功
- [ ] 主题订阅成功
- [ ] 设备能够成功发布消息
- [ ] 外部客户端能够接收到设备发布的消息
- [ ] 设备能够接收外部客户端发布的消息
- [ ] 消息内容正确无误
- [ ] 程序稳定运行,无异常断线
## 测试建议
1. **先测试 TCP 版本**: 确保基本的 MQTT 功能正常
2. **检查内存配置**: SSL/TLS 需要更多内存(已配置 32KB mbedTLS堆)
3. **验证网络连接**: 确保可以访问目标 broker 的 8883 端口
4. **使用公共 broker 测试**: 如 `broker.emqx.io`, `test.mosquitto.org`
5. **使用专业工具**: 推荐使用 MQTTX 或 Mosquitto 客户端进行测试
## 公共 MQTT Broker
| Broker | 地址 | SSL端口 | 证书验证 |
|--------|------|---------|---------|
| EMQX | broker.emqx.io | 8883 | 可选 |
| Eclipse | mqtt.eclipseprojects.io | 8883 | 需要 |
| HiveMQ | broker.hivemq.com | 8883 | 可选 |
| Mosquitto | test.mosquitto.org | 8883 | 需要 |
## 故障排查
### SSL 握手失败
1. 检查 broker 是否支持 TLS 1.2
2. 检查系统时间是否正确(影响证书验证)
3. 增加 mbedTLS 堆大小
### 内存不足
1. 增加 `CONFIG_MBEDTLS_HEAP_SIZE`
2. 增加 `CONFIG_PSRAM_HEAP_SIZE`
3. 减少 `NETWORK_BUFFER_SIZE`
### 连接超时
1. 检查防火墙是否阻止 8883 端口
2. 增加超时时间
3. 检查 DNS 解析是否正常
## 相关文档
- [coreMQTT 库文档](https://www.freertos.org/mqtt/index.html)
- [mbedTLS 文档](https://mbed-tls.readthedocs.io/)
- [MQTT 规范](https://mqtt.org/mqtt-specification/)
## 许可证
Copyright (c) 2025, LISTENAI
SPDX-License-Identifier: Apache-2.0

View File

@@ -0,0 +1,177 @@
#!/bin/bash
set -e
usage() {
echo "使用方式: $0 [选项]"
echo "选项:"
echo " -S, --Source <path> 指定项目源码路径 (默认为当前脚本所在目录)"
echo " -t, --target <target> 指定构建目标 (如 menuconfig)"
echo " -C, --Clean 清理构建目录"
echo " -B, --build 构建输出目录"
echo " -h, --help 显示此帮助信息"
echo " -r, --release 以 Release 模式构建 (移除 DEBUG_PATH 信息)"
echo " -w, --warnings-as-errors 将警告视为错误"
echo " -D<var>=<value> 传递 CMake 变量 (可多次使用)"
echo ""
echo "示例:"
echo " $0 -S samples/helloworld -DBOARD=arcs_mini 指定板型构建"
echo " $0 -S samples/helloworld -DBOARD=arcs_evb 使用 EVB 板型"
echo " $0 -S samples/helloworld -t menuconfig -DBOARD=arcs_mini 运行 menuconfig"
echo " $0 -C -S samples/helloworld -DBOARD=arcs_mini 清理并重新构建"
echo " $0 -S samples/helloworld -DBOARD=my_board -DBOARD_SEARCH_PATH=/path/to/boards 使用自定义板型"
exit 1
}
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_PATH="$SCRIPT_DIR"
TARGET=""
CLEAN=false
OUTPUT="build"
WARNINGS_AS_ERRORS=false
RELEASE=false
ARCS_BASE_DIR_NAME="arcs-sdk"
ARCS_DEV_TOOLS_DIR_NAME="listenai-dev-tools"
ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME="gcc"
ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME="listenai-tools"
find_arcs_base() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_BASE_DIR_NAME"
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found ARCS_BASE: $current_dir/$dir_name"
export ARCS_BASE="$current_dir/$dir_name"
return 0
fi
current_dir=$(dirname "$current_dir")
done
echo "ARCS_BASE not found, Please add ARCS_BASE environment variable or set ARCS_BASE_DIR_NAME to the correct directory."
echo "Current Target ARCS_BASE directory name: $ARCS_BASE_DIR_NAME."
exit 1
}
find_dev_tools() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_DEV_TOOLS_DIR_NAME"
echo "trying to find $dir_name in parent directories..."
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found $dir_name: $current_dir/$dir_name"
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME" ]; then
echo "Found LISTENAI_TOOLS_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
export LISTENAI_TOOLS_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
fi
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME" ]; then
echo "Found NUCLEI_TOOLCHAIN_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
export NUCLEI_TOOLCHAIN_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
fi
return 0
fi
current_dir=$(dirname "$current_dir")
done
}
while [[ $# -gt 0 ]]; do
case $1 in
-S|--Source)
PROJECT_PATH="$2"
shift 2
;;
-B|--build)
OUTPUT="$2"
shift 2
;;
-t|--target)
TARGET="$2"
shift 2
;;
-C|--Clean)
CLEAN=true
shift 1
;;
-w|--warnings-as-errors)
WARNINGS_AS_ERRORS=true
shift 1
;;
-h|--help)
usage
;;
-r|--release)
RELEASE=true
shift 1
;;
-D*)
CMAKE_VARS+=("$1")
shift 1
;;
*)
echo "未知参数: $1"
usage
;;
esac
done
echo "Source: $PROJECT_PATH"
echo "Target: $TARGET"
echo "Clean : $CLEAN"
if [ -z "$LISTENAI_TOOLS_PATH" ] || [ -z "$NUCLEI_TOOLCHAIN_PATH" ]; then
find_dev_tools
fi
if [ -z "${LISTENAI_TOOLS_PATH}" ]; then
export LISTENAI_TOOLS_PATH="请添加 LISTENAI_TOOLS_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 LISTENAI_TOOLS_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
if [ -z "${NUCLEI_TOOLCHAIN_PATH}" ]; then
export NUCLEI_TOOLCHAIN_PATH="请添加 NUCLEI_TOOLCHAIN_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 NUCLEI_TOOLCHAIN_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
############### 下面代码不用修改 ##################
# 构建工具的位置
CMAKE_PROGRAM="$LISTENAI_TOOLS_PATH/cmake/bin/cmake"
NINJA_PROGRAM="$LISTENAI_TOOLS_PATH/ninja/ninja"
# 配置环境变量 ARCS_BASE
if [ -z "$ARCS_BASE" ]; then
find_arcs_base
fi
if [ "$CLEAN" = true ]; then
rm -rf $OUTPUT
fi
# Initialize CMAKE_VARS array if it doesn't exist
declare -a CMAKE_VARS
# Add warnings-as-errors flag if enabled
if [ "$WARNINGS_AS_ERRORS" = true ]; then
CMAKE_VARS+=("-DCMAKE_C_FLAGS=-Werror")
CMAKE_VARS+=("-DCMAKE_CXX_FLAGS=-Werror")
echo "Treating warnings as errors"
fi
# Add release flags if enabled
if [ "$RELEASE" = true ]; then
CMAKE_VARS+=("-DENABLE_DEBUG_PATH=OFF")
echo "Release mode enabled (-DENABLE_DEBUG_PATH=OFF)"
fi
$CMAKE_PROGRAM -B "$OUTPUT" -G Ninja -S "$PROJECT_PATH" \
-DCMAKE_MAKE_PROGRAM="$NINJA_PROGRAM" \
"${CMAKE_VARS[@]}"
if [ -z "$TARGET" ]; then
$CMAKE_PROGRAM --build "$OUTPUT" -j4
else
$CMAKE_PROGRAM --build "$OUTPUT" --target "$TARGET"
fi

View File

@@ -0,0 +1,64 @@
CONFIG_HEAP_SIZE=0xC000
CONFIG_PSRAM_HEAP_SIZE=0x5b0000
# DISK驱动使能
CONFIG_DISK_DRIVER=y
# 文件系统配置
CONFIG_FILE_SYSTEM=y
CONFIG_FATFS_FILESYSTEM=y
CONFIG_LSFS=y
CONFIG_LSFS_FAT=y
CONFIG_LSFS_REGISTER_VFS=y
CONFIG_LVFS_POSIX_API_ALIAS=y
CONFIG_LVFS=y
CONFIG_LVFS_POSIX_API=y
CONFIG_FS_ENV_MEMORY_MANAGEMENT_USE_PSRAM=y
# 网络配置
CONFIG_LISA_WIFI=y
CONFIG_WIFI_LWIP_SAME_CORE=y
# MAC Manager
CONFIG_MAC_MANAGER=y
CONFIG_MAC_MANAGER_MAC_BY_CHIP_ID=y
# WIFI MANAGER
CONFIG_WIFI_MANAGER=y
# LISA Shell
CONFIG_LISA_SHELL=y
CONFIG_SYSLOG_PRINTF_REDIRECT=y
# LISA Porting
CONFIG_LISA_PORTING=y
# SD Card drivers
CONFIG_DISK_DRIVER_SDMMC=y
CONFIG_LISA_SDMMC_DEVICE=y
CONFIG_CJSON=y
CONFIG_LISA_OS=y
CONFIG_LISA_KV=y
CONFIG_LISA_NETWORK=y
# coreMQTT
CONFIG_SDK_MODULE_COREMQTT=y
# mbedTLS (SSL/TLS支持)
CONFIG_MBEDTLS=y
CONFIG_MBEDTLS_BUILTIN=y
CONFIG_MBEDTLS_ENABLE_HEAP=y
CONFIG_MBEDTLS_HEAP_SIZE=0x8000
# mbedTLS功能配置
CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
CONFIG_MBEDTLS_KEY_EXCHANGE_RSA_ENABLED=y
CONFIG_MBEDTLS_SSL_CLI_C=y
CONFIG_MBEDTLS_SSL_TLS_C=y
CONFIG_MBEDTLS_X509_CRT_PARSE_C=y
CONFIG_MBEDTLS_PEM_PARSE_C=y
CONFIG_BACK_TRACE=y

View File

@@ -0,0 +1,3 @@
tests:
samples.mqtt.ssl:
build_only: true

View File

@@ -0,0 +1,10 @@
listenai_library_named(app)
listenai_library_sources(
main.c
fs/user_fs.c
)
listenai_include_directories(
./
./fs
)

View File

@@ -0,0 +1,86 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lsfs.h"
#include "disk/disk_access.h"
#include "lisa_sdmmc.h"
#include "log_print.h"
#if CONFIG_LVFS
#include "lvfs.h"
#endif
#define DISK_DEVICE "SD:"
#define DISK_MOUNT_POINT "/" DISK_DEVICE
static struct lsfs_mount_t flash_lsfs_mnt = {
.type = LSFS_FATFS,
.mnt_point = DISK_MOUNT_POINT,
.fs_data = NULL,
};
static int fs_mount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_mount(mp);
if (ret != 0)
{
CLOG("No file system, try reformatting, mount ret: %d", ret);
ret = lsfs_mkfs(mp->type, &mp->mnt_point[1], NULL, 0);
if (ret == 0)
{
CLOG("mkfs success, retry mount");
ret = lsfs_mount(mp);
}
else
{
CLOG("mkfs failed, ret: %d", ret);
}
}
if (ret != 0)
{
CLOG("Failed to mount filesystem: %d", ret);
return ret;
}
CLOG("%s mounted successfully", mp->mnt_point);
return 0;
}
static void user_fs_change_to_root_folder(void)
{
int ret;
ret = lvfs_chdrive(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change drive to %s, error: %d\n", DISK_MOUNT_POINT, ret);
}
ret = lvfs_chdir(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change directory to /SD:/, error: %d\n", ret);
} else {
CLOG("Changed to directory /SD:/\n");
}
}
int user_fs_init(void){
lisa_sdmmc_probe(lisa_device_get("sdmmc0"));
disk_init(NULL);
#if CONFIG_LVFS
lvfs_init();
#endif
lsfs_init();
if(0 != fs_mount(&flash_lsfs_mnt)){
return -1;
}
user_fs_change_to_root_folder();
return 0;
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef USER_FS_H
#define USER_FS_H
int user_fs_init(void);
#endif /* USER_FS_H */

View File

@@ -0,0 +1,482 @@
#include "lisa_thread.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "stdbool.h"
#include "FreeRTOS.h"
#include "task.h"
#define TAG "mqtt-ssl-test"
#include "lisa_log.h"
#include "lisa_kv.h"
#include "mac_manager.h"
#include "mac_manager_ops.h"
#include "wifi_manager/wifi_manager.h"
#include "user_fs.h"
#include "lisa_wifi.h"
#include "ls_wifi_type.h"
#include "net_al.h"
#include "net_def.h"
#include "core_mqtt.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
/* mbedTLS 头文件 */
#include "mbedtls/net_sockets.h"
#include "mbedtls/ssl.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/error.h"
#include "mbedtls/debug.h"
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
#define MQTT_BROKER_HOST "broker.emqx.io"
#define MQTT_BROKER_PORT 8883 // SSL端口
#define MQTT_CLIENT_ID "arcs_mqtt_ssl_client"
#define TARGET_WIFI_SSID "Xiaomi_listenai_2.4G"
#define TARGET_WIFI_PWD "a12345678"
#define MQTT_TOPIC_PUB "arcs/test/pub"
#define MQTT_TOPIC_SUB "arcs/test/sub"
#define WIFI_MGR_AUTO_CONNECT_INTERVAL_MS 2000
#define WIFI_MGR_SEARCH_AP_BUFFER_SIZE 10
#define NETWORK_BUFFER_SIZE 2048
static mac_manager_t *m_mac_manager;
static wifi_mgr_sta_config_t list[WIFI_MGR_SEARCH_AP_BUFFER_SIZE] = { 0 };
static volatile bool g_wifi_connected = false;
static volatile bool g_get_ip_success = false;
/* SSL/TLS 上下文 */
struct NetworkContext {
mbedtls_net_context server_fd;
mbedtls_ssl_context ssl;
mbedtls_ssl_config conf;
mbedtls_entropy_context entropy;
mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_x509_crt cacert;
};
/* MQTT 缓冲区 */
static uint8_t g_network_buffer[NETWORK_BUFFER_SIZE];
static int8_t custom_get_wifi_mac(uint8_t mac_addr[6])
{
int8_t ret = 0;
if (!mac_addr)
return -1;
ret = mac_manager_get(m_mac_manager, mac_addr, 6);
LOGI("custom_get_wifi_mac: %d, %02X:%02X:%02X:%02X:%02X:%02X", ret,
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
return ret;
}
static void dhcp_status_callback(int vif_idx, bool success, uint32_t ip_addr,
uint32_t netmask, uint32_t gateway, void *arg)
{
if (success) {
LOGI("DHCP Success: IP=%d.%d.%d.%d",
ip_addr & 0xff, (ip_addr >> 8) & 0xff,
(ip_addr >> 16) & 0xff, (ip_addr >> 24) & 0xff);
g_get_ip_success = true;
} else {
LOGI("DHCP Failed on VIF-%d", vif_idx);
}
}
static void wifi_mgr_connection_status_cb(wifi_mgr_connection_info_t *connection_info, void *arg)
{
net_if_t *net_if;
LOGI("WiFi connection status: %d", connection_info->status);
switch (connection_info->status) {
case WIFI_MGR_STA_CONNECTED:
LOGI("WiFi connected to AP");
g_wifi_connected = true;
net_if = net_if_get(WIFI_VIF_STA_IDX);
net_if_up(net_if);
if (!net_if->static_ip) {
ls_dhcpc_start(WIFI_VIF_STA_IDX);
}
break;
case WIFI_MGR_STA_DISCONNECTED:
LOGI("WiFi disconnected from AP");
g_wifi_connected = false;
ls_dhcpc_stop(WIFI_VIF_STA_IDX);
net_if_down(net_if_get(WIFI_VIF_STA_IDX));
break;
default:
break;
}
}
static void user_mac_manager_init(void)
{
mac_manager_config_t config = {
.random_mac_if_mac_invalid = false,
};
m_mac_manager = mac_manager_init(mac_manager_ops_get()->mem_ops,
mac_manager_ops_get()->content_ops, &config);
if (m_mac_manager == NULL) {
LOGI("mac_manager_init failed\n");
}
}
static void user_wifi_manager_init(void)
{
wifi_mgr_sta_config_t cfg = {
.ssid = TARGET_WIFI_SSID,
.pwd = TARGET_WIFI_PWD,
};
wifi_mgr_autoconn_config_t autoconn_cfg = {
.interval_ms = WIFI_MGR_AUTO_CONNECT_INTERVAL_MS,
};
wifi_mgr_init(wifi_mgr_ops_get());
wifi_mgr_sta_enable();
wifi_mgr_sta_add_connection_cb(wifi_mgr_connection_status_cb, NULL);
int count = wifi_mgr_storage_search_ap(list, WIFI_MGR_SEARCH_AP_BUFFER_SIZE, SEARCH_ALL, NULL);
for (int i = 0; i < count; i++) {
LOGI("Deleting saved AP: ssid=%s", list[i].ssid);
wifi_mgr_storage_delete_ap(&list[i]);
}
wifi_mgr_storage_save_ap(&cfg);
wifi_mgr_auto_connect_start(&autoconn_cfg);
LOGI("WiFi auto-connect started");
}
static int wait_for_wifi_connection(void)
{
int timeout = 30;
LOGI("Waiting for WiFi connection and IP address...");
while (timeout > 0) {
if (g_wifi_connected && g_get_ip_success) {
LOGI("WiFi connected and IP obtained");
vTaskDelay(pdMS_TO_TICKS(500));
return 0;
}
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
if (timeout % 5 == 0) {
LOGI("Waiting... (WiFi:%d, IP:%d, timeout:%d)", g_wifi_connected, g_get_ip_success, timeout);
}
}
LOGI("WiFi connection timeout");
return -1;
}
/* mbedTLS debug 回调 */
static void my_debug(void *ctx, int level, const char *file, int line, const char *str)
{
((void) level);
LOGI("%s:%04d: %s", file, line, str);
}
/* Transport 接口实现 (SSL版本) */
static int32_t transport_recv(NetworkContext_t *pNetworkContext, void *pBuffer, size_t bytesToRecv)
{
int ret = mbedtls_ssl_read(&pNetworkContext->ssl, pBuffer, bytesToRecv);
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
return 0; /* 非阻塞,需要重试 */
}
if (ret < 0) {
LOGE("mbedtls_ssl_read failed: -0x%04x", -ret);
return -1;
}
return ret;
}
static int32_t transport_send(NetworkContext_t *pNetworkContext, const void *pBuffer, size_t bytesToSend)
{
int ret = mbedtls_ssl_write(&pNetworkContext->ssl, pBuffer, bytesToSend);
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
return 0; /* 非阻塞,需要重试 */
}
if (ret < 0) {
LOGE("mbedtls_ssl_write failed: -0x%04x", -ret);
return -1;
}
return ret;
}
static uint32_t get_time_ms(void)
{
return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
/* MQTT 事件回调 */
static void mqtt_event_callback(MQTTContext_t *pContext, MQTTPacketInfo_t *pPacketInfo,
MQTTDeserializedInfo_t *pDeserializedInfo)
{
if (pPacketInfo->type == MQTT_PACKET_TYPE_PUBLISH) {
MQTTPublishInfo_t *pPublishInfo = pDeserializedInfo->pPublishInfo;
LOGI("Received PUBLISH: topic=%.*s, payload=%.*s",
(int)pPublishInfo->topicNameLength, pPublishInfo->pTopicName,
(int)pPublishInfo->payloadLength, (char *)pPublishInfo->pPayload);
} else if (pPacketInfo->type == MQTT_PACKET_TYPE_SUBACK) {
LOGI("Received SUBACK");
} else if (pPacketInfo->type == MQTT_PACKET_TYPE_PUBACK) {
LOGI("Received PUBACK");
}
}
static int connect_to_broker_ssl(struct NetworkContext *pNetworkContext)
{
int ret;
char port_str[8];
LOGI("Initializing SSL/TLS connection to %s:%d...", MQTT_BROKER_HOST, MQTT_BROKER_PORT);
/* 1. 初始化 mbedTLS 结构 */
mbedtls_net_init(&pNetworkContext->server_fd);
mbedtls_ssl_init(&pNetworkContext->ssl);
mbedtls_ssl_config_init(&pNetworkContext->conf);
mbedtls_x509_crt_init(&pNetworkContext->cacert);
mbedtls_ctr_drbg_init(&pNetworkContext->ctr_drbg);
mbedtls_entropy_init(&pNetworkContext->entropy);
/* 2. 初始化随机数生成器 */
const char *pers = "mqtt_ssl_client";
ret = mbedtls_ctr_drbg_seed(&pNetworkContext->ctr_drbg, mbedtls_entropy_func,
&pNetworkContext->entropy, (const unsigned char *)pers, strlen(pers));
if (ret != 0) {
LOGE("mbedtls_ctr_drbg_seed failed: -0x%04x", -ret);
return -1;
}
/* 3. 连接到服务器 */
snprintf(port_str, sizeof(port_str), "%d", MQTT_BROKER_PORT);
ret = mbedtls_net_connect(&pNetworkContext->server_fd, MQTT_BROKER_HOST,
port_str, MBEDTLS_NET_PROTO_TCP);
if (ret != 0) {
LOGE("mbedtls_net_connect failed: -0x%04x", -ret);
return -1;
}
LOGI("TCP connection established");
/* 4. 设置非阻塞模式 */
ret = mbedtls_net_set_nonblock(&pNetworkContext->server_fd);
if (ret != 0) {
LOGE("mbedtls_net_set_nonblock failed: -0x%04x", -ret);
return -1;
}
/* 5. 设置 SSL/TLS 配置 */
ret = mbedtls_ssl_config_defaults(&pNetworkContext->conf,
MBEDTLS_SSL_IS_CLIENT,
MBEDTLS_SSL_TRANSPORT_STREAM,
MBEDTLS_SSL_PRESET_DEFAULT);
if (ret != 0) {
LOGE("mbedtls_ssl_config_defaults failed: -0x%04x", -ret);
return -1;
}
/* 配置为不验证证书 (用于测试,生产环境应验证证书) */
mbedtls_ssl_conf_authmode(&pNetworkContext->conf, MBEDTLS_SSL_VERIFY_NONE);
mbedtls_ssl_conf_rng(&pNetworkContext->conf, mbedtls_ctr_drbg_random,
&pNetworkContext->ctr_drbg);
/* 启用调试输出 (可选) */
mbedtls_ssl_conf_dbg(&pNetworkContext->conf, my_debug, NULL);
/* 6. 设置 SSL 上下文 */
ret = mbedtls_ssl_setup(&pNetworkContext->ssl, &pNetworkContext->conf);
if (ret != 0) {
LOGE("mbedtls_ssl_setup failed: -0x%04x", -ret);
return -1;
}
ret = mbedtls_ssl_set_hostname(&pNetworkContext->ssl, MQTT_BROKER_HOST);
if (ret != 0) {
LOGE("mbedtls_ssl_set_hostname failed: -0x%04x", -ret);
return -1;
}
mbedtls_ssl_set_bio(&pNetworkContext->ssl, &pNetworkContext->server_fd,
mbedtls_net_send, mbedtls_net_recv, NULL);
/* 7. 执行 SSL/TLS 握手 */
LOGI("Performing SSL/TLS handshake...");
while ((ret = mbedtls_ssl_handshake(&pNetworkContext->ssl)) != 0) {
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
LOGE("mbedtls_ssl_handshake failed: -0x%04x", -ret);
return -1;
}
vTaskDelay(pdMS_TO_TICKS(10));
}
LOGI("SSL/TLS handshake completed successfully");
LOGI("Connected to MQTT broker %s:%d via SSL/TLS", MQTT_BROKER_HOST, MQTT_BROKER_PORT);
return 0;
}
static void cleanup_ssl(struct NetworkContext *pNetworkContext)
{
mbedtls_ssl_close_notify(&pNetworkContext->ssl);
mbedtls_net_free(&pNetworkContext->server_fd);
mbedtls_x509_crt_free(&pNetworkContext->cacert);
mbedtls_ssl_free(&pNetworkContext->ssl);
mbedtls_ssl_config_free(&pNetworkContext->conf);
mbedtls_ctr_drbg_free(&pNetworkContext->ctr_drbg);
mbedtls_entropy_free(&pNetworkContext->entropy);
}
int main(int argc, char **argv)
{
MQTTContext_t mqttContext;
MQTTConnectInfo_t connectInfo;
MQTTSubscribeInfo_t subscribeInfo;
MQTTPublishInfo_t publishInfo;
MQTTFixedBuffer_t networkBuffer;
TransportInterface_t transport;
struct NetworkContext networkContext;
MQTTStatus_t mqttStatus;
bool sessionPresent;
LOGI("MQTT SSL/TLS Test Starting...");
/* 初始化 */
user_mac_manager_init();
net_dhcp_register_status_callback(dhcp_status_callback, NULL);
lisa_wifi_ops_t ops = { .custom_mac = custom_get_wifi_mac };
lisa_wifi_init(&ops);
user_fs_init();
lisa_kv_init();
user_wifi_manager_init();
if (wait_for_wifi_connection() != 0) {
LOGE("Failed to connect to WiFi");
return -1;
}
LOGI("\n========================================");
LOGI("MQTT SSL/TLS 功能测试");
LOGI("========================================\n");
/* 连接到 MQTT Broker (SSL) */
if (connect_to_broker_ssl(&networkContext) != 0) {
return -1;
}
/* 设置 Transport 接口 */
transport.recv = transport_recv;
transport.send = transport_send;
transport.pNetworkContext = &networkContext;
transport.writev = NULL;
/* 设置网络缓冲区 */
networkBuffer.pBuffer = g_network_buffer;
networkBuffer.size = NETWORK_BUFFER_SIZE;
/* 初始化 MQTT Context */
mqttStatus = MQTT_Init(&mqttContext, &transport, get_time_ms, mqtt_event_callback, &networkBuffer);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Init failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
/* 建立 MQTT 连接 */
memset(&connectInfo, 0, sizeof(connectInfo));
connectInfo.cleanSession = true;
connectInfo.pClientIdentifier = MQTT_CLIENT_ID;
connectInfo.clientIdentifierLength = strlen(MQTT_CLIENT_ID);
connectInfo.keepAliveSeconds = 60;
LOGI("=== 测试 1: MQTT CONNECT ===");
mqttStatus = MQTT_Connect(&mqttContext, &connectInfo, NULL, 5000, &sessionPresent);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Connect failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("MQTT Connected successfully");
vTaskDelay(pdMS_TO_TICKS(500));
/* 订阅主题 */
LOGI("\n=== 测试 2: MQTT SUBSCRIBE ===");
memset(&subscribeInfo, 0, sizeof(subscribeInfo));
subscribeInfo.qos = MQTTQoS0;
subscribeInfo.pTopicFilter = MQTT_TOPIC_SUB;
subscribeInfo.topicFilterLength = strlen(MQTT_TOPIC_SUB);
mqttStatus = MQTT_Subscribe(&mqttContext, &subscribeInfo, 1, MQTT_GetPacketId(&mqttContext));
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Subscribe failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("Subscribe request sent for topic: %s", MQTT_TOPIC_SUB);
/* 处理 SUBACK */
mqttStatus = MQTT_ProcessLoop(&mqttContext);
if (mqttStatus != MQTTSuccess && mqttStatus != MQTTNeedMoreBytes) {
LOGE("MQTT_ProcessLoop failed: %s", MQTT_Status_strerror(mqttStatus));
}
vTaskDelay(pdMS_TO_TICKS(500));
/* 发布消息 */
LOGI("\n=== 测试 3: MQTT PUBLISH ===");
memset(&publishInfo, 0, sizeof(publishInfo));
publishInfo.qos = MQTTQoS0;
publishInfo.pTopicName = MQTT_TOPIC_PUB;
publishInfo.topicNameLength = strlen(MQTT_TOPIC_PUB);
publishInfo.pPayload = "Hello from ARCS via SSL!";
publishInfo.payloadLength = strlen("Hello from ARCS via SSL!");
mqttStatus = MQTT_Publish(&mqttContext, &publishInfo, MQTT_GetPacketId(&mqttContext));
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Publish failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("Published message to topic: %s", MQTT_TOPIC_PUB);
vTaskDelay(pdMS_TO_TICKS(1000));
/* 处理接收的消息 */
LOGI("\n=== 测试 4: 接收消息 ===");
while (1) {
mqttStatus = MQTT_ProcessLoop(&mqttContext);
if (mqttStatus != MQTTSuccess && mqttStatus != MQTTNeedMoreBytes) {
LOGE("MQTT_ProcessLoop failed: %s", MQTT_Status_strerror(mqttStatus));
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
/* 断开连接 */
LOGI("\n=== 测试 5: MQTT DISCONNECT ===");
mqttStatus = MQTT_Disconnect(&mqttContext);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Disconnect failed: %s", MQTT_Status_strerror(mqttStatus));
} else {
LOGI("MQTT Disconnected successfully");
}
LOGI("\n========================================");
LOGI("所有 MQTT SSL/TLS 测试完成");
LOGI("========================================\n");
cleanup:
cleanup_ssl(&networkContext);
return 0;
}

View File

@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(listenai-cmake REQUIRED HINTS $ENV{ARCS_BASE})
project(arcs)
listenai_include_directories(
./
)
add_subdirectory(src)
listenai_add_executable(${PROJECT_NAME})

View File

@@ -0,0 +1 @@
osource "$ARCS_BASE/Kconfig"

View File

@@ -0,0 +1,93 @@
# MQTT 客户端示例
基于 coreMQTT 库的 MQTT 客户端示例。
## 功能
- 连接到 MQTT Broker
- 订阅主题
- 发布消息
- 接收消息
## 配置
使用前请修改 `src/main.c` 中的以下配置:
```c
#define MQTT_BROKER_HOST "broker.emqx.io" // MQTT Broker 地址
#define MQTT_BROKER_PORT 1883 // MQTT Broker 端口
#define MQTT_CLIENT_ID "arcs_mqtt_client"
#define TARGET_WIFI_SSID "your_wifi_ssid" // WiFi SSID
#define TARGET_WIFI_PWD "your_wifi_pwd" // WiFi 密码
#define MQTT_TOPIC_PUB "arcs/test/pub" // 发布主题
#define MQTT_TOPIC_SUB "arcs/test/sub" // 订阅主题
```
## 编译
```{eval-rst}
.. include:: /sample_build.rst
```
## 运行
烧录固件后,设备会:
1. 连接 WiFi
2. 连接 MQTT Broker
3. 订阅 `arcs/test/sub` 主题
4. 向 `arcs/test/pub` 主题发布消息
5. 等待接收消息
6. 断开连接
## 测试方法
### 使用公共 MQTT Broker
推荐使用以下公共 MQTT Broker 进行测试:
- broker.emqx.io:1883 (推荐)
- test.mosquitto.org:1883
### 使用 MQTTX 工具测试
[MQTTX](https://mqttx.app/) 是一个跨平台的 MQTT 客户端工具,可以方便地进行测试。
#### 1. 安装 MQTTX CLI
```bash
curl -LO https://www.emqx.com/en/downloads/MQTTX/v1.12.1/mqttx-cli-linux-x64
sudo install ./mqttx-cli-linux-x64 /usr/local/bin/mqttx
```
#### 2. 订阅设备发布的主题
在电脑上运行以下命令,订阅设备将要发布消息的主题:
```bash
mqttx sub -h broker.emqx.io -p 1883 -t "arcs/test/pub"
```
#### 3. 运行设备固件
烧录并运行固件后MQTTX 会收到设备发布的消息:
```
topic: arcs/test/pub, qos: 0, size: 16B
Hello from ARCS!
```
#### 4. 向设备发送消息
在另一个终端运行以下命令,向设备订阅的主题发送消息:
```bash
mqttx pub -h broker.emqx.io -p 1883 -t "arcs/test/sub" -m "Hello ARCS Girl"
```
设备端会打印接收到的消息:
```
Received PUBLISH: topic=arcs/test/sub, payload=Hello ARCS Girl
```

View File

@@ -0,0 +1,177 @@
#!/bin/bash
set -e
usage() {
echo "使用方式: $0 [选项]"
echo "选项:"
echo " -S, --Source <path> 指定项目源码路径 (默认为当前脚本所在目录)"
echo " -t, --target <target> 指定构建目标 (如 menuconfig)"
echo " -C, --Clean 清理构建目录"
echo " -B, --build 构建输出目录"
echo " -h, --help 显示此帮助信息"
echo " -r, --release 以 Release 模式构建 (移除 DEBUG_PATH 信息)"
echo " -w, --warnings-as-errors 将警告视为错误"
echo " -D<var>=<value> 传递 CMake 变量 (可多次使用)"
echo ""
echo "示例:"
echo " $0 -S samples/helloworld -DBOARD=arcs_mini 指定板型构建"
echo " $0 -S samples/helloworld -DBOARD=arcs_evb 使用 EVB 板型"
echo " $0 -S samples/helloworld -t menuconfig -DBOARD=arcs_mini 运行 menuconfig"
echo " $0 -C -S samples/helloworld -DBOARD=arcs_mini 清理并重新构建"
echo " $0 -S samples/helloworld -DBOARD=my_board -DBOARD_SEARCH_PATH=/path/to/boards 使用自定义板型"
exit 1
}
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_PATH="$SCRIPT_DIR"
TARGET=""
CLEAN=false
OUTPUT="build"
WARNINGS_AS_ERRORS=false
RELEASE=false
ARCS_BASE_DIR_NAME="arcs-sdk"
ARCS_DEV_TOOLS_DIR_NAME="listenai-dev-tools"
ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME="gcc"
ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME="listenai-tools"
find_arcs_base() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_BASE_DIR_NAME"
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found ARCS_BASE: $current_dir/$dir_name"
export ARCS_BASE="$current_dir/$dir_name"
return 0
fi
current_dir=$(dirname "$current_dir")
done
echo "ARCS_BASE not found, Please add ARCS_BASE environment variable or set ARCS_BASE_DIR_NAME to the correct directory."
echo "Current Target ARCS_BASE directory name: $ARCS_BASE_DIR_NAME."
exit 1
}
find_dev_tools() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_DEV_TOOLS_DIR_NAME"
echo "trying to find $dir_name in parent directories..."
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found $dir_name: $current_dir/$dir_name"
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME" ]; then
echo "Found LISTENAI_TOOLS_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
export LISTENAI_TOOLS_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
fi
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME" ]; then
echo "Found NUCLEI_TOOLCHAIN_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
export NUCLEI_TOOLCHAIN_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
fi
return 0
fi
current_dir=$(dirname "$current_dir")
done
}
while [[ $# -gt 0 ]]; do
case $1 in
-S|--Source)
PROJECT_PATH="$2"
shift 2
;;
-B|--build)
OUTPUT="$2"
shift 2
;;
-t|--target)
TARGET="$2"
shift 2
;;
-C|--Clean)
CLEAN=true
shift 1
;;
-w|--warnings-as-errors)
WARNINGS_AS_ERRORS=true
shift 1
;;
-h|--help)
usage
;;
-r|--release)
RELEASE=true
shift 1
;;
-D*)
CMAKE_VARS+=("$1")
shift 1
;;
*)
echo "未知参数: $1"
usage
;;
esac
done
echo "Source: $PROJECT_PATH"
echo "Target: $TARGET"
echo "Clean : $CLEAN"
if [ -z "$LISTENAI_TOOLS_PATH" ] || [ -z "$NUCLEI_TOOLCHAIN_PATH" ]; then
find_dev_tools
fi
if [ -z "${LISTENAI_TOOLS_PATH}" ]; then
export LISTENAI_TOOLS_PATH="请添加 LISTENAI_TOOLS_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 LISTENAI_TOOLS_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
if [ -z "${NUCLEI_TOOLCHAIN_PATH}" ]; then
export NUCLEI_TOOLCHAIN_PATH="请添加 NUCLEI_TOOLCHAIN_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 NUCLEI_TOOLCHAIN_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
############### 下面代码不用修改 ##################
# 构建工具的位置
CMAKE_PROGRAM="$LISTENAI_TOOLS_PATH/cmake/bin/cmake"
NINJA_PROGRAM="$LISTENAI_TOOLS_PATH/ninja/ninja"
# 配置环境变量 ARCS_BASE
if [ -z "$ARCS_BASE" ]; then
find_arcs_base
fi
if [ "$CLEAN" = true ]; then
rm -rf $OUTPUT
fi
# Initialize CMAKE_VARS array if it doesn't exist
declare -a CMAKE_VARS
# Add warnings-as-errors flag if enabled
if [ "$WARNINGS_AS_ERRORS" = true ]; then
CMAKE_VARS+=("-DCMAKE_C_FLAGS=-Werror")
CMAKE_VARS+=("-DCMAKE_CXX_FLAGS=-Werror")
echo "Treating warnings as errors"
fi
# Add release flags if enabled
if [ "$RELEASE" = true ]; then
CMAKE_VARS+=("-DENABLE_DEBUG_PATH=OFF")
echo "Release mode enabled (-DENABLE_DEBUG_PATH=OFF)"
fi
$CMAKE_PROGRAM -B "$OUTPUT" -G Ninja -S "$PROJECT_PATH" \
-DCMAKE_MAKE_PROGRAM="$NINJA_PROGRAM" \
"${CMAKE_VARS[@]}"
if [ -z "$TARGET" ]; then
$CMAKE_PROGRAM --build "$OUTPUT" -j4
else
$CMAKE_PROGRAM --build "$OUTPUT" --target "$TARGET"
fi

View File

@@ -0,0 +1,51 @@
CONFIG_HEAP_SIZE=0xC000
CONFIG_PSRAM_HEAP_SIZE=0x5b0000
# DISK驱动使能
CONFIG_DISK_DRIVER=y
# 文件系统配置
CONFIG_FILE_SYSTEM=y
CONFIG_FATFS_FILESYSTEM=y
CONFIG_LSFS=y
CONFIG_LSFS_FAT=y
CONFIG_LSFS_REGISTER_VFS=y
CONFIG_LVFS_POSIX_API_ALIAS=y
CONFIG_LVFS=y
CONFIG_LVFS_POSIX_API=y
CONFIG_FS_ENV_MEMORY_MANAGEMENT_USE_PSRAM=y
# 网络配置
CONFIG_LISA_WIFI=y
CONFIG_WIFI_LWIP_SAME_CORE=y
# MAC Manager
CONFIG_MAC_MANAGER=y
CONFIG_MAC_MANAGER_MAC_BY_CHIP_ID=y
# WIFI MANAGER
CONFIG_WIFI_MANAGER=y
# LISA Shell
CONFIG_LISA_SHELL=y
CONFIG_SYSLOG_PRINTF_REDIRECT=y
# LISA Porting
CONFIG_LISA_PORTING=y
# SD Card drivers
CONFIG_DISK_DRIVER_SDMMC=y
CONFIG_LISA_SDMMC_DEVICE=y
CONFIG_CJSON=y
CONFIG_LISA_OS=y
CONFIG_LISA_KV=y
CONFIG_LISA_NETWORK=y
# coreMQTT
CONFIG_SDK_MODULE_COREMQTT=y
CONFIG_BACK_TRACE=y

View File

@@ -0,0 +1,3 @@
tests:
samples.mqtt:
build_only: true

View File

@@ -0,0 +1,10 @@
listenai_library_named(app)
listenai_library_sources(
main.c
fs/user_fs.c
)
listenai_include_directories(
./
./fs
)

View File

@@ -0,0 +1,86 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lsfs.h"
#include "disk/disk_access.h"
#include "lisa_sdmmc.h"
#include "log_print.h"
#if CONFIG_LVFS
#include "lvfs.h"
#endif
#define DISK_DEVICE "SD:"
#define DISK_MOUNT_POINT "/" DISK_DEVICE
static struct lsfs_mount_t flash_lsfs_mnt = {
.type = LSFS_FATFS,
.mnt_point = DISK_MOUNT_POINT,
.fs_data = NULL,
};
static int fs_mount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_mount(mp);
if (ret != 0)
{
CLOG("No file system, try reformatting, mount ret: %d", ret);
ret = lsfs_mkfs(mp->type, &mp->mnt_point[1], NULL, 0);
if (ret == 0)
{
CLOG("mkfs success, retry mount");
ret = lsfs_mount(mp);
}
else
{
CLOG("mkfs failed, ret: %d", ret);
}
}
if (ret != 0)
{
CLOG("Failed to mount filesystem: %d", ret);
return ret;
}
CLOG("%s mounted successfully", mp->mnt_point);
return 0;
}
static void user_fs_change_to_root_folder(void)
{
int ret;
ret = lvfs_chdrive(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change drive to %s, error: %d\n", DISK_MOUNT_POINT, ret);
}
ret = lvfs_chdir(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change directory to /SD:/, error: %d\n", ret);
} else {
CLOG("Changed to directory /SD:/\n");
}
}
int user_fs_init(void){
lisa_sdmmc_probe(lisa_device_get("sdmmc0"));
disk_init(NULL);
#if CONFIG_LVFS
lvfs_init();
#endif
lsfs_init();
if(0 != fs_mount(&flash_lsfs_mnt)){
return -1;
}
user_fs_change_to_root_folder();
return 0;
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef USER_FS_H
#define USER_FS_H
int user_fs_init(void);
#endif /* USER_FS_H */

View File

@@ -0,0 +1,388 @@
#include "lisa_thread.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "stdbool.h"
#include "FreeRTOS.h"
#include "task.h"
#define TAG "mqtt-test"
#include "lisa_log.h"
#include "lisa_kv.h"
#include "mac_manager.h"
#include "mac_manager_ops.h"
#include "wifi_manager/wifi_manager.h"
#include "user_fs.h"
#include "lisa_wifi.h"
#include "ls_wifi_type.h"
#include "net_al.h"
#include "net_def.h"
#include "core_mqtt.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
#define MQTT_BROKER_HOST "broker.emqx.io"
#define MQTT_BROKER_PORT 1883
#define MQTT_CLIENT_ID "arcs_mqtt_client"
#define TARGET_WIFI_SSID "Xiaomi_listenai_2.4G"
#define TARGET_WIFI_PWD "a12345678"
#define MQTT_TOPIC_PUB "arcs/test/pub"
#define MQTT_TOPIC_SUB "arcs/test/sub"
#define WIFI_MGR_AUTO_CONNECT_INTERVAL_MS 2000
#define WIFI_MGR_SEARCH_AP_BUFFER_SIZE 10
#define NETWORK_BUFFER_SIZE 1024
static mac_manager_t *m_mac_manager;
static wifi_mgr_sta_config_t list[WIFI_MGR_SEARCH_AP_BUFFER_SIZE] = { 0 };
static volatile bool g_wifi_connected = false;
static volatile bool g_get_ip_success = false;
/* 网络上下文 */
struct NetworkContext {
int socket;
};
/* MQTT 缓冲区 */
static uint8_t g_network_buffer[NETWORK_BUFFER_SIZE];
static int8_t custom_get_wifi_mac(uint8_t mac_addr[6])
{
int8_t ret = 0;
if (!mac_addr)
return -1;
ret = mac_manager_get(m_mac_manager, mac_addr, 6);
LOGI("custom_get_wifi_mac: %d, %02X:%02X:%02X:%02X:%02X:%02X", ret,
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
return ret;
}
static void dhcp_status_callback(int vif_idx, bool success, uint32_t ip_addr,
uint32_t netmask, uint32_t gateway, void *arg)
{
if (success) {
LOGI("DHCP Success: IP=%d.%d.%d.%d",
ip_addr & 0xff, (ip_addr >> 8) & 0xff,
(ip_addr >> 16) & 0xff, (ip_addr >> 24) & 0xff);
g_get_ip_success = true;
} else {
LOGI("DHCP Failed on VIF-%d", vif_idx);
}
}
static void wifi_mgr_connection_status_cb(wifi_mgr_connection_info_t *connection_info, void *arg)
{
net_if_t *net_if;
LOGI("WiFi connection status: %d", connection_info->status);
switch (connection_info->status) {
case WIFI_MGR_STA_CONNECTED:
LOGI("WiFi connected to AP");
g_wifi_connected = true;
net_if = net_if_get(WIFI_VIF_STA_IDX);
net_if_up(net_if);
if (!net_if->static_ip) {
ls_dhcpc_start(WIFI_VIF_STA_IDX);
}
break;
case WIFI_MGR_STA_DISCONNECTED:
LOGI("WiFi disconnected from AP");
g_wifi_connected = false;
ls_dhcpc_stop(WIFI_VIF_STA_IDX);
net_if_down(net_if_get(WIFI_VIF_STA_IDX));
break;
default:
break;
}
}
static void user_mac_manager_init(void)
{
mac_manager_config_t config = {
.random_mac_if_mac_invalid = false,
};
m_mac_manager = mac_manager_init(mac_manager_ops_get()->mem_ops,
mac_manager_ops_get()->content_ops, &config);
if (m_mac_manager == NULL) {
LOGI("mac_manager_init failed\n");
}
}
static void user_wifi_manager_init(void)
{
wifi_mgr_sta_config_t cfg = {
.ssid = TARGET_WIFI_SSID,
.pwd = TARGET_WIFI_PWD,
};
wifi_mgr_autoconn_config_t autoconn_cfg = {
.interval_ms = WIFI_MGR_AUTO_CONNECT_INTERVAL_MS,
};
wifi_mgr_init(wifi_mgr_ops_get());
wifi_mgr_sta_enable();
wifi_mgr_sta_add_connection_cb(wifi_mgr_connection_status_cb, NULL);
int count = wifi_mgr_storage_search_ap(list, WIFI_MGR_SEARCH_AP_BUFFER_SIZE, SEARCH_ALL, NULL);
for (int i = 0; i < count; i++) {
LOGI("Deleting saved AP: ssid=%s", list[i].ssid);
wifi_mgr_storage_delete_ap(&list[i]);
}
wifi_mgr_storage_save_ap(&cfg);
wifi_mgr_auto_connect_start(&autoconn_cfg);
LOGI("WiFi auto-connect started");
}
static int wait_for_wifi_connection(void)
{
int timeout = 30;
LOGI("Waiting for WiFi connection and IP address...");
while (timeout > 0) {
if (g_wifi_connected && g_get_ip_success) {
LOGI("WiFi connected and IP obtained");
vTaskDelay(pdMS_TO_TICKS(500));
return 0;
}
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
if (timeout % 5 == 0) {
LOGI("Waiting... (WiFi:%d, IP:%d, timeout:%d)", g_wifi_connected, g_get_ip_success, timeout);
}
}
LOGI("WiFi connection timeout");
return -1;
}
/* Transport 接口实现 */
static int32_t transport_recv(NetworkContext_t *pNetworkContext, void *pBuffer, size_t bytesToRecv)
{
int ret = recv(pNetworkContext->socket, pBuffer, bytesToRecv, 0);
if (ret < 0) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return 0;
}
return -1;
}
return ret;
}
static int32_t transport_send(NetworkContext_t *pNetworkContext, const void *pBuffer, size_t bytesToSend)
{
int ret = send(pNetworkContext->socket, pBuffer, bytesToSend, 0);
if (ret < 0) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return 0;
}
return -1;
}
return ret;
}
static uint32_t get_time_ms(void)
{
return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
/* MQTT 事件回调 */
static void mqtt_event_callback(MQTTContext_t *pContext, MQTTPacketInfo_t *pPacketInfo,
MQTTDeserializedInfo_t *pDeserializedInfo)
{
if (pPacketInfo->type == MQTT_PACKET_TYPE_PUBLISH) {
MQTTPublishInfo_t *pPublishInfo = pDeserializedInfo->pPublishInfo;
LOGI("Received PUBLISH: topic=%.*s, payload=%.*s",
(int)pPublishInfo->topicNameLength, pPublishInfo->pTopicName,
(int)pPublishInfo->payloadLength, (char *)pPublishInfo->pPayload);
} else if (pPacketInfo->type == MQTT_PACKET_TYPE_SUBACK) {
LOGI("Received SUBACK");
} else if (pPacketInfo->type == MQTT_PACKET_TYPE_PUBACK) {
LOGI("Received PUBACK");
}
}
static int connect_to_broker(struct NetworkContext *pNetworkContext)
{
struct sockaddr_in server_addr;
struct hostent *host;
pNetworkContext->socket = socket(AF_INET, SOCK_STREAM, 0);
if (pNetworkContext->socket < 0) {
LOGE("Failed to create socket");
return -1;
}
host = gethostbyname(MQTT_BROKER_HOST);
if (host == NULL) {
LOGE("Failed to resolve hostname");
close(pNetworkContext->socket);
return -1;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(MQTT_BROKER_PORT);
memcpy(&server_addr.sin_addr, host->h_addr, host->h_length);
if (connect(pNetworkContext->socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
LOGE("Failed to connect to broker");
close(pNetworkContext->socket);
return -1;
}
/* 设置非阻塞模式 */
int flags = fcntl(pNetworkContext->socket, F_GETFL, 0);
fcntl(pNetworkContext->socket, F_SETFL, flags | O_NONBLOCK);
LOGI("Connected to MQTT broker %s:%d", MQTT_BROKER_HOST, MQTT_BROKER_PORT);
return 0;
}
int main(int argc, char **argv)
{
MQTTContext_t mqttContext;
MQTTConnectInfo_t connectInfo;
MQTTSubscribeInfo_t subscribeInfo;
MQTTPublishInfo_t publishInfo;
MQTTFixedBuffer_t networkBuffer;
TransportInterface_t transport;
struct NetworkContext networkContext;
MQTTStatus_t mqttStatus;
bool sessionPresent;
LOGI("MQTT Test Starting...");
/* 初始化 */
user_mac_manager_init();
net_dhcp_register_status_callback(dhcp_status_callback, NULL);
lisa_wifi_ops_t ops = { .custom_mac = custom_get_wifi_mac };
lisa_wifi_init(&ops);
user_fs_init();
lisa_kv_init();
user_wifi_manager_init();
if (wait_for_wifi_connection() != 0) {
LOGE("Failed to connect to WiFi");
return -1;
}
LOGI("\n========================================");
LOGI("MQTT 功能测试");
LOGI("========================================\n");
/* 连接到 MQTT Broker */
if (connect_to_broker(&networkContext) != 0) {
return -1;
}
/* 设置 Transport 接口 */
transport.recv = transport_recv;
transport.send = transport_send;
transport.pNetworkContext = &networkContext;
transport.writev = NULL;
/* 设置网络缓冲区 */
networkBuffer.pBuffer = g_network_buffer;
networkBuffer.size = NETWORK_BUFFER_SIZE;
/* 初始化 MQTT Context */
mqttStatus = MQTT_Init(&mqttContext, &transport, get_time_ms, mqtt_event_callback, &networkBuffer);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Init failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
/* 建立 MQTT 连接 */
memset(&connectInfo, 0, sizeof(connectInfo));
connectInfo.cleanSession = true;
connectInfo.pClientIdentifier = MQTT_CLIENT_ID;
connectInfo.clientIdentifierLength = strlen(MQTT_CLIENT_ID);
connectInfo.keepAliveSeconds = 60;
LOGI("=== 测试 1: MQTT CONNECT ===");
mqttStatus = MQTT_Connect(&mqttContext, &connectInfo, NULL, 5000, &sessionPresent);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Connect failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("MQTT Connected successfully");
vTaskDelay(pdMS_TO_TICKS(500));
/* 订阅主题 */
LOGI("\n=== 测试 2: MQTT SUBSCRIBE ===");
memset(&subscribeInfo, 0, sizeof(subscribeInfo));
subscribeInfo.qos = MQTTQoS0;
subscribeInfo.pTopicFilter = MQTT_TOPIC_SUB;
subscribeInfo.topicFilterLength = strlen(MQTT_TOPIC_SUB);
mqttStatus = MQTT_Subscribe(&mqttContext, &subscribeInfo, 1, MQTT_GetPacketId(&mqttContext));
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Subscribe failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("Subscribe request sent for topic: %s", MQTT_TOPIC_SUB);
/* 处理 SUBACK */
mqttStatus = MQTT_ProcessLoop(&mqttContext);
if (mqttStatus != MQTTSuccess && mqttStatus != MQTTNeedMoreBytes) {
LOGE("MQTT_ProcessLoop failed: %s", MQTT_Status_strerror(mqttStatus));
}
vTaskDelay(pdMS_TO_TICKS(500));
/* 发布消息 */
LOGI("\n=== 测试 3: MQTT PUBLISH ===");
memset(&publishInfo, 0, sizeof(publishInfo));
publishInfo.qos = MQTTQoS0;
publishInfo.pTopicName = MQTT_TOPIC_PUB;
publishInfo.topicNameLength = strlen(MQTT_TOPIC_PUB);
publishInfo.pPayload = "Hello from ARCS!";
publishInfo.payloadLength = strlen("Hello from ARCS!");
mqttStatus = MQTT_Publish(&mqttContext, &publishInfo, MQTT_GetPacketId(&mqttContext));
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Publish failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("Published message to topic: %s", MQTT_TOPIC_PUB);
vTaskDelay(pdMS_TO_TICKS(1000));
/* 处理接收的消息 */
LOGI("\n=== 测试 4: 接收消息 ===");
// for (int i = 0; i < 5; i++) {
while (1) {
mqttStatus = MQTT_ProcessLoop(&mqttContext);
if (mqttStatus != MQTTSuccess && mqttStatus != MQTTNeedMoreBytes) {
LOGE("MQTT_ProcessLoop failed: %s", MQTT_Status_strerror(mqttStatus));
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
/* 断开连接 */
LOGI("\n=== 测试 5: MQTT DISCONNECT ===");
mqttStatus = MQTT_Disconnect(&mqttContext);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Disconnect failed: %s", MQTT_Status_strerror(mqttStatus));
} else {
LOGI("MQTT Disconnected successfully");
}
LOGI("\n========================================");
LOGI("所有 MQTT 测试完成");
LOGI("========================================\n");
cleanup:
close(networkContext.socket);
return 0;
}

View File

@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(listenai-cmake REQUIRED HINTS $ENV{ARCS_BASE})
project(arcs)
listenai_include_directories(
./
)
add_subdirectory(src)
listenai_add_executable(${PROJECT_NAME})

View File

@@ -0,0 +1 @@
osource "$ARCS_BASE/Kconfig"

View File

@@ -0,0 +1,318 @@
# MQTT over WebSocket 示例
本示例演示如何在 ARCS 平台上使用 coreMQTT 库通过 WebSocket 连接到 MQTT broker。
## 功能特性
- 使用 lisa_websocket 组件实现 WebSocket 连接
- 支持 MQTT over WebSocket (端口 8083)
- WiFi 自动连接
- MQTT 发布/订阅功能
- 事件驱动的 WebSocket 通信
- 非阻塞 I/O 操作
## 硬件要求
- ARCS 开发板
- SD 卡(用于文件系统)
- WiFi 网络环境
## 配置说明
在使用前,请修改 [src/main.c](src/main.c) 中的以下配置:
```c
// MQTT Broker 配置
#define MQTT_BROKER_HOST "broker.emqx.io" // MQTT broker 地址
#define MQTT_BROKER_PORT "8083" // WebSocket 端口
#define MQTT_BROKER_PATH "/mqtt" // MQTT over WebSocket 路径
#define MQTT_CLIENT_ID "arcs_mqtt_ws_client"
// WiFi 配置
#define TARGET_WIFI_SSID "your_wifi_ssid" // 修改为你的WiFi名称
#define TARGET_WIFI_PWD "your_wifi_pwd" // 修改为你的WiFi密码
// MQTT 主题
#define MQTT_TOPIC_PUB "arcs/test/pub" // 发布主题
#define MQTT_TOPIC_SUB "arcs/test/sub" // 订阅主题
```
## WebSocket 配置
### 连接参数
```c
lisa_ws_request_t ws_req = {
.scheme = "ws", // WebSocket 协议
.host = "broker.emqx.io", // Broker 地址
.port = "8083", // WebSocket 端口
.path = "/mqtt", // MQTT 路径
.timeout = 15000, // 连接超时(毫秒)
.on_event = on_ws_event, // 事件回调
.on_data = on_ws_data // 数据回调
};
```
### MQTT 子协议
MQTT over WebSocket 需要在 WebSocket 握手时声明 `mqtt` 子协议。`lisa_websocket` 组件会自动检测路径中包含 `/mqtt` 并设置正确的子协议头:
```
\r\nSec-WebSocket-Protocol: mqtt\r\n
```
## 编译
```{eval-rst}
.. include:: /sample_build.rst
```
## 运行效果
程序启动后,将依次执行以下步骤:
1. **WiFi 连接**: 自动连接到配置的 WiFi 网络
2. **WebSocket 连接**: 建立到 MQTT broker 的 WebSocket 连接
3. **MQTT 连接**: 通过 WebSocket 通道建立 MQTT 连接
4. **订阅主题**: 订阅指定的 MQTT 主题
5. **发布消息**: 发布测试消息
6. **接收消息**: 持续监听和处理接收的消息
### 预期日志输出
```
[mqtt-ws-test] MQTT over WebSocket Test Starting...
[mqtt-ws-test] WiFi connected and IP obtained
========================================
MQTT WebSocket 功能测试
========================================
[lisa-ws] websocket thread waiting for connect sem
[lisa-ws] Got connect signal
[mqtt-ws-test] Waiting for WebSocket connection... (10/100)
[mqtt-ws-test] Connected to MQTT broker via WebSocket
=== 测试 1: MQTT CONNECT ===
[mqtt-ws-test] MQTT Connected successfully
=== 测试 2: MQTT SUBSCRIBE ===
[mqtt-ws-test] Subscribe request sent for topic: arcs/test/sub
=== 测试 3: MQTT PUBLISH ===
[mqtt-ws-test] Published message to topic: arcs/test/pub
=== 测试 4: 接收消息 ===
[mqtt-ws-test] Received PUBLISH: topic=arcs/test/sub, payload=...
```
## 主要差异(相比其他传输方式)
| 特性 | TCP 版本 | SSL 版本 | WebSocket 版本 |
|-----|---------|---------|---------------|
| 端口 | 1883 | 8883 | 8083 |
| 加密 | 无 | TLS 1.2+ | 无 |
| 传输层 | TCP | TCP + TLS | TCP + WebSocket |
| 库依赖 | lwIP | mbedTLS | lisa_websocket + nopoll |
| 连接建立 | TCP 三次握手 | TCP + SSL握手 | TCP + WebSocket握手 |
| 数据传输 | socket send/recv | mbedtls_ssl_write/read | lisa_ws_send/on_data回调 |
| 子协议 | 无 | 无 | mqtt |
| 适用场景 | 标准MQTT | 加密MQTT | 穿越HTTP代理 |
## WebSocket 配置 (prj.conf)
```ini
# WebSocket 支持
CONFIG_LISA_WEBSOCKET=y
CONFIG_MODULE_NOPOLL=y
# coreMQTT
CONFIG_SDK_MODULE_COREMQTT=y
```
## 调试技巧
### WebSocket 连接状态
通过事件回调监控 WebSocket 连接状态:
```c
static void on_ws_event(lisa_ws_event_t *event)
{
switch (event->what) {
case LISA_WS_ON_CONNECTING:
LOGI("WebSocket connecting...");
break;
case LISA_WS_ON_CONNECTED:
LOGI("WebSocket connected");
break;
case LISA_WS_ON_DISCONNECTED:
LOGI("WebSocket disconnected");
break;
}
}
```
### 常见问题
1. **连接超时**:
- 检查 broker 是否支持 WebSocket
- 确认端口号正确 (通常是 8083)
- 确认路径正确 (通常是 `/mqtt`)
2. **握手失败**:
- 检查是否正确设置了 MQTT 子协议
- 查看 WebSocket 握手日志
3. **数据传输异常**:
- 检查 `recv_buffer` 大小是否足够
- 确认事件回调正确设置
## 功能测试
### 使用 MQTTX 客户端测试
[MQTTX](https://mqttx.app/) 是一个优秀的跨平台 MQTT 客户端工具,可用于测试设备的发布和订阅功能。
#### 安装 MQTTX CLI
```bash
# Linux
curl -LO https://www.emqx.com/en/downloads/MQTTX/v1.12.1/mqttx-cli-linux-x64
sudo install ./mqttx-cli-linux-x64 /usr/local/bin/mqttx
```
#### 测试场景 1: 订阅设备发布的消息
在电脑上运行以下命令,订阅设备发布的主题:
```bash
mqttx sub -h broker.emqx.io -p 8083 -t "arcs/test/pub" -l ws --path "/mqtt"
```
**预期结果:**
```
✔ Connected
✔ Subscribed to arcs/test/pub
topic: arcs/test/pub, qos: 0, size: 28B
Hello from ARCS via WebSocket!
```
设备会定期发布消息到 `arcs/test/pub` 主题,MQTTX 客户端会接收并显示这些消息。
#### 测试场景 2: 向设备发布消息
在电脑上运行以下命令,向设备订阅的主题发送消息:
```bash
mqttx pub -h broker.emqx.io -p 8083 -t "arcs/test/sub" -m "Hello ARCS WebSocket" -l ws --path "/mqtt"
```
**预期结果:**
MQTTX 客户端输出:
```
✔ Connected
✔ Message published
```
设备串口日志输出:
```
I/mqtt-ws-test [1034:43:49.966 1 main] Received PUBLISH: topic=arcs/test/sub, payload=Hello ARCS WebSocket
```
#### MQTTX 参数说明
| 参数 | 说明 | 示例值 |
|-----|------|--------|
| `-h` | MQTT broker 地址 | `broker.emqx.io` |
| `-p` | 端口号 | `8083` (WebSocket端口) |
| `-t` | 主题名称 | `arcs/test/pub` |
| `-m` | 消息内容 | `"Hello ARCS WebSocket"` |
| `--protocol` | 协议类型 | `ws` (WebSocket) |
| `--path` | WebSocket 路径 | `/mqtt` |
| `-q` | QoS 级别 (可选) | `0`, `1`, `2` |
| `-u` | 用户名 (可选) | `username` |
| `-P` | 密码 (可选) | `password` |
#### 完整测试流程
1. **启动设备**: 编译并运行 WebSocket MQTT 示例程序
2. **订阅测试**: 在电脑上运行 MQTTX 订阅命令,验证能收到设备消息
3. **发布测试**: 在电脑上运行 MQTTX 发布命令,验证设备能收到消息
4. **双向通信**: 同时开启订阅和发布,观察双向消息传输
### 测试 Checklist
- [ ] WiFi 连接成功并获取 IP 地址
- [ ] WebSocket 握手成功完成
- [ ] MQTT 子协议正确声明
- [ ] MQTT 连接建立成功
- [ ] 主题订阅成功
- [ ] 设备能够成功发布消息
- [ ] 外部客户端能够接收到设备发布的消息
- [ ] 设备能够接收外部客户端发布的消息
- [ ] 消息内容正确无误
- [ ] 程序稳定运行,无异常断线
## 测试建议
1. **先测试 TCP 版本**: 确保基本的 MQTT 功能正常
2. **检查内存配置**: WebSocket 需要额外的缓冲区
3. **验证网络连接**: 确保可以访问目标 broker 的 8083 端口
4. **使用公共 broker 测试**: 如 `broker.emqx.io`
5. **使用专业工具**: 推荐使用 MQTTX 或浏览器开发者工具进行测试
## 公共 MQTT Broker (WebSocket)
| Broker | 地址 | WS端口 | 路径 | 认证 |
|--------|------|--------|------|------|
| EMQX | broker.emqx.io | 8083 | /mqtt | 可选 |
| HiveMQ | broker.hivemq.com | 8000 | /mqtt | 可选 |
## 故障排查
### WebSocket 连接超时
1. 检查 broker 是否支持 WebSocket (通常端口 8083)
2. 确认路径正确 (通常是 `/mqtt`)
3. 检查 MQTT 子协议是否正确声明
4. 增加 `timeout` 参数值
### MQTT 握手失败
1. 检查 WebSocket 连接是否已建立 (`ws_connected` 标志)
2. 确认在 WebSocket 连接成功后才发起 MQTT 连接
3. 检查 MQTT client ID 是否重复
### 数据接收异常
1. 检查 `recv_buffer` 大小 (默认 2048 字节)
2. 确认 `on_data` 回调正确实现
3. 检查数据类型 (MQTT 使用二进制帧)
### 内存不足
1. 增加 `CONFIG_PSRAM_HEAP_SIZE`
2. 减少 `NETWORK_BUFFER_SIZE`
3. 检查 WebSocket 队列大小配置
## WebSocket vs WSS
本示例使用非加密的 WebSocket (ws://)。如果需要加密连接,请使用 [wss](../wss/) 示例,它提供:
- TLS 加密传输
- 端口 8084
- 完整的证书验证支持
## 相关文档
- [coreMQTT 库文档](https://www.freertos.org/mqtt/index.html)
- [WebSocket 协议规范](https://datatracker.ietf.org/doc/html/rfc6455)
- [MQTT over WebSocket 规范](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718127)
- [nopoll 库文档](http://www.aspl.es/nopoll/)
## 许可证
Copyright (c) 2025, LISTENAI
SPDX-License-Identifier: Apache-2.0

View File

@@ -0,0 +1,177 @@
#!/bin/bash
set -e
usage() {
echo "使用方式: $0 [选项]"
echo "选项:"
echo " -S, --Source <path> 指定项目源码路径 (默认为当前脚本所在目录)"
echo " -t, --target <target> 指定构建目标 (如 menuconfig)"
echo " -C, --Clean 清理构建目录"
echo " -B, --build 构建输出目录"
echo " -h, --help 显示此帮助信息"
echo " -r, --release 以 Release 模式构建 (移除 DEBUG_PATH 信息)"
echo " -w, --warnings-as-errors 将警告视为错误"
echo " -D<var>=<value> 传递 CMake 变量 (可多次使用)"
echo ""
echo "示例:"
echo " $0 -S samples/helloworld -DBOARD=arcs_mini 指定板型构建"
echo " $0 -S samples/helloworld -DBOARD=arcs_evb 使用 EVB 板型"
echo " $0 -S samples/helloworld -t menuconfig -DBOARD=arcs_mini 运行 menuconfig"
echo " $0 -C -S samples/helloworld -DBOARD=arcs_mini 清理并重新构建"
echo " $0 -S samples/helloworld -DBOARD=my_board -DBOARD_SEARCH_PATH=/path/to/boards 使用自定义板型"
exit 1
}
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_PATH="$SCRIPT_DIR"
TARGET=""
CLEAN=false
OUTPUT="build"
WARNINGS_AS_ERRORS=false
RELEASE=false
ARCS_BASE_DIR_NAME="arcs-sdk"
ARCS_DEV_TOOLS_DIR_NAME="listenai-dev-tools"
ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME="gcc"
ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME="listenai-tools"
find_arcs_base() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_BASE_DIR_NAME"
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found ARCS_BASE: $current_dir/$dir_name"
export ARCS_BASE="$current_dir/$dir_name"
return 0
fi
current_dir=$(dirname "$current_dir")
done
echo "ARCS_BASE not found, Please add ARCS_BASE environment variable or set ARCS_BASE_DIR_NAME to the correct directory."
echo "Current Target ARCS_BASE directory name: $ARCS_BASE_DIR_NAME."
exit 1
}
find_dev_tools() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_DEV_TOOLS_DIR_NAME"
echo "trying to find $dir_name in parent directories..."
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found $dir_name: $current_dir/$dir_name"
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME" ]; then
echo "Found LISTENAI_TOOLS_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
export LISTENAI_TOOLS_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
fi
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME" ]; then
echo "Found NUCLEI_TOOLCHAIN_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
export NUCLEI_TOOLCHAIN_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
fi
return 0
fi
current_dir=$(dirname "$current_dir")
done
}
while [[ $# -gt 0 ]]; do
case $1 in
-S|--Source)
PROJECT_PATH="$2"
shift 2
;;
-B|--build)
OUTPUT="$2"
shift 2
;;
-t|--target)
TARGET="$2"
shift 2
;;
-C|--Clean)
CLEAN=true
shift 1
;;
-w|--warnings-as-errors)
WARNINGS_AS_ERRORS=true
shift 1
;;
-h|--help)
usage
;;
-r|--release)
RELEASE=true
shift 1
;;
-D*)
CMAKE_VARS+=("$1")
shift 1
;;
*)
echo "未知参数: $1"
usage
;;
esac
done
echo "Source: $PROJECT_PATH"
echo "Target: $TARGET"
echo "Clean : $CLEAN"
if [ -z "$LISTENAI_TOOLS_PATH" ] || [ -z "$NUCLEI_TOOLCHAIN_PATH" ]; then
find_dev_tools
fi
if [ -z "${LISTENAI_TOOLS_PATH}" ]; then
export LISTENAI_TOOLS_PATH="请添加 LISTENAI_TOOLS_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 LISTENAI_TOOLS_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
if [ -z "${NUCLEI_TOOLCHAIN_PATH}" ]; then
export NUCLEI_TOOLCHAIN_PATH="请添加 NUCLEI_TOOLCHAIN_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 NUCLEI_TOOLCHAIN_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
############### 下面代码不用修改 ##################
# 构建工具的位置
CMAKE_PROGRAM="$LISTENAI_TOOLS_PATH/cmake/bin/cmake"
NINJA_PROGRAM="$LISTENAI_TOOLS_PATH/ninja/ninja"
# 配置环境变量 ARCS_BASE
if [ -z "$ARCS_BASE" ]; then
find_arcs_base
fi
if [ "$CLEAN" = true ]; then
rm -rf $OUTPUT
fi
# Initialize CMAKE_VARS array if it doesn't exist
declare -a CMAKE_VARS
# Add warnings-as-errors flag if enabled
if [ "$WARNINGS_AS_ERRORS" = true ]; then
CMAKE_VARS+=("-DCMAKE_C_FLAGS=-Werror")
CMAKE_VARS+=("-DCMAKE_CXX_FLAGS=-Werror")
echo "Treating warnings as errors"
fi
# Add release flags if enabled
if [ "$RELEASE" = true ]; then
CMAKE_VARS+=("-DENABLE_DEBUG_PATH=OFF")
echo "Release mode enabled (-DENABLE_DEBUG_PATH=OFF)"
fi
$CMAKE_PROGRAM -B "$OUTPUT" -G Ninja -S "$PROJECT_PATH" \
-DCMAKE_MAKE_PROGRAM="$NINJA_PROGRAM" \
"${CMAKE_VARS[@]}"
if [ -z "$TARGET" ]; then
$CMAKE_PROGRAM --build "$OUTPUT" -j4
else
$CMAKE_PROGRAM --build "$OUTPUT" --target "$TARGET"
fi

View File

@@ -0,0 +1,54 @@
CONFIG_HEAP_SIZE=0xC000
CONFIG_PSRAM_HEAP_SIZE=0x5b0000
# DISK驱动使能
CONFIG_DISK_DRIVER=y
# 文件系统配置
CONFIG_FILE_SYSTEM=y
CONFIG_FATFS_FILESYSTEM=y
CONFIG_LSFS=y
CONFIG_LSFS_FAT=y
CONFIG_LSFS_REGISTER_VFS=y
CONFIG_LVFS_POSIX_API_ALIAS=y
CONFIG_LVFS=y
CONFIG_LVFS_POSIX_API=y
CONFIG_FS_ENV_MEMORY_MANAGEMENT_USE_PSRAM=y
# 网络配置
CONFIG_LISA_WIFI=y
CONFIG_WIFI_LWIP_SAME_CORE=y
# MAC Manager
CONFIG_MAC_MANAGER=y
CONFIG_MAC_MANAGER_MAC_BY_CHIP_ID=y
# WIFI MANAGER
CONFIG_WIFI_MANAGER=y
# LISA Shell
CONFIG_LISA_SHELL=y
CONFIG_SYSLOG_PRINTF_REDIRECT=y
# LISA Porting
CONFIG_LISA_PORTING=y
# SD Card drivers
CONFIG_DISK_DRIVER_SDMMC=y
CONFIG_LISA_SDMMC_DEVICE=y
CONFIG_CJSON=y
CONFIG_LISA_OS=y
CONFIG_LISA_KV=y
CONFIG_LISA_NETWORK=y
# coreMQTT
CONFIG_SDK_MODULE_COREMQTT=y
# WebSocket支持 (MQTT over WebSocket)
CONFIG_LISA_WEBSOCKET=y
CONFIG_MODULE_NOPOLL=y
CONFIG_BACK_TRACE=y

View File

@@ -0,0 +1,3 @@
tests:
samples.mqtt.ws:
build_only: true

View File

@@ -0,0 +1,10 @@
listenai_library_named(app)
listenai_library_sources(
main.c
fs/user_fs.c
)
listenai_include_directories(
./
./fs
)

View File

@@ -0,0 +1,86 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lsfs.h"
#include "disk/disk_access.h"
#include "lisa_sdmmc.h"
#include "log_print.h"
#if CONFIG_LVFS
#include "lvfs.h"
#endif
#define DISK_DEVICE "SD:"
#define DISK_MOUNT_POINT "/" DISK_DEVICE
static struct lsfs_mount_t flash_lsfs_mnt = {
.type = LSFS_FATFS,
.mnt_point = DISK_MOUNT_POINT,
.fs_data = NULL,
};
static int fs_mount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_mount(mp);
if (ret != 0)
{
CLOG("No file system, try reformatting, mount ret: %d", ret);
ret = lsfs_mkfs(mp->type, &mp->mnt_point[1], NULL, 0);
if (ret == 0)
{
CLOG("mkfs success, retry mount");
ret = lsfs_mount(mp);
}
else
{
CLOG("mkfs failed, ret: %d", ret);
}
}
if (ret != 0)
{
CLOG("Failed to mount filesystem: %d", ret);
return ret;
}
CLOG("%s mounted successfully", mp->mnt_point);
return 0;
}
static void user_fs_change_to_root_folder(void)
{
int ret;
ret = lvfs_chdrive(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change drive to %s, error: %d\n", DISK_MOUNT_POINT, ret);
}
ret = lvfs_chdir(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change directory to /SD:/, error: %d\n", ret);
} else {
CLOG("Changed to directory /SD:/\n");
}
}
int user_fs_init(void){
lisa_sdmmc_probe(lisa_device_get("sdmmc0"));
disk_init(NULL);
#if CONFIG_LVFS
lvfs_init();
#endif
lsfs_init();
if(0 != fs_mount(&flash_lsfs_mnt)){
return -1;
}
user_fs_change_to_root_folder();
return 0;
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef USER_FS_H
#define USER_FS_H
int user_fs_init(void);
#endif /* USER_FS_H */

View File

@@ -0,0 +1,501 @@
#include "lisa_thread.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "stdbool.h"
#include "FreeRTOS.h"
#include "task.h"
#define TAG "mqtt-ws-test"
#include "lisa_log.h"
#include "lisa_kv.h"
#include "mac_manager.h"
#include "mac_manager_ops.h"
#include "wifi_manager/wifi_manager.h"
#include "user_fs.h"
#include "lisa_wifi.h"
#include "ls_wifi_type.h"
#include "net_al.h"
#include "net_def.h"
#include "core_mqtt.h"
#include "lisa_websocket.h"
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
#define MQTT_BROKER_HOST "broker.emqx.io"
#define MQTT_BROKER_PORT "8083" // WebSocket端口
#define MQTT_BROKER_PATH "/mqtt" // MQTT over WebSocket路径
#define MQTT_CLIENT_ID "arcs_mqtt_ws_client"
#define TARGET_WIFI_SSID "lschat" //"Xiaomi_listenai_2.4G"
#define TARGET_WIFI_PWD "12345678"
#define MQTT_TOPIC_PUB "arcs/test/pub"
#define MQTT_TOPIC_SUB "arcs/test/sub"
#define WIFI_MGR_AUTO_CONNECT_INTERVAL_MS 2000
#define WIFI_MGR_SEARCH_AP_BUFFER_SIZE 10
#define NETWORK_BUFFER_SIZE 2048
#define WS_RECV_BUFFER_SIZE 4096
static mac_manager_t *m_mac_manager;
static wifi_mgr_sta_config_t list[WIFI_MGR_SEARCH_AP_BUFFER_SIZE] = { 0 };
static volatile bool g_wifi_connected = false;
static volatile bool g_get_ip_success = false;
static volatile bool g_ws_connected = false;
/* WebSocket + MQTT 上下文 */
struct NetworkContext {
lisa_ws_t *ws_handle;
uint8_t *recv_buffer;
size_t recv_len;
size_t recv_offset;
bool ws_connected;
};
/* MQTT 缓冲区 */
static uint8_t g_network_buffer[NETWORK_BUFFER_SIZE];
static uint8_t g_ws_recv_buffer[WS_RECV_BUFFER_SIZE];
static int8_t custom_get_wifi_mac(uint8_t mac_addr[6])
{
int8_t ret = 0;
if (!mac_addr)
return -1;
ret = mac_manager_get(m_mac_manager, mac_addr, 6);
LOGI("custom_get_wifi_mac: %d, %02X:%02X:%02X:%02X:%02X:%02X", ret,
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
return ret;
}
static void dhcp_status_callback(int vif_idx, bool success, uint32_t ip_addr,
uint32_t netmask, uint32_t gateway, void *arg)
{
if (success) {
LOGI("DHCP Success: IP=%d.%d.%d.%d",
ip_addr & 0xff, (ip_addr >> 8) & 0xff,
(ip_addr >> 16) & 0xff, (ip_addr >> 24) & 0xff);
g_get_ip_success = true;
} else {
LOGI("DHCP Failed on VIF-%d", vif_idx);
}
}
static void wifi_mgr_connection_status_cb(wifi_mgr_connection_info_t *connection_info, void *arg)
{
net_if_t *net_if;
LOGI("WiFi connection status: %d", connection_info->status);
switch (connection_info->status) {
case WIFI_MGR_STA_CONNECTED:
LOGI("WiFi connected to AP");
g_wifi_connected = true;
net_if = net_if_get(WIFI_VIF_STA_IDX);
net_if_up(net_if);
if (!net_if->static_ip) {
ls_dhcpc_start(WIFI_VIF_STA_IDX);
}
break;
case WIFI_MGR_STA_DISCONNECTED:
LOGI("WiFi disconnected from AP");
g_wifi_connected = false;
ls_dhcpc_stop(WIFI_VIF_STA_IDX);
net_if_down(net_if_get(WIFI_VIF_STA_IDX));
break;
default:
break;
}
}
static void user_mac_manager_init(void)
{
mac_manager_config_t config = {
.random_mac_if_mac_invalid = false,
};
m_mac_manager = mac_manager_init(mac_manager_ops_get()->mem_ops,
mac_manager_ops_get()->content_ops, &config);
if (m_mac_manager == NULL) {
LOGI("mac_manager_init failed\n");
}
}
static void user_wifi_manager_init(void)
{
wifi_mgr_sta_config_t cfg = {
.ssid = TARGET_WIFI_SSID,
.pwd = TARGET_WIFI_PWD,
};
wifi_mgr_autoconn_config_t autoconn_cfg = {
.interval_ms = WIFI_MGR_AUTO_CONNECT_INTERVAL_MS,
};
wifi_mgr_init(wifi_mgr_ops_get());
wifi_mgr_sta_enable();
wifi_mgr_sta_add_connection_cb(wifi_mgr_connection_status_cb, NULL);
int count = wifi_mgr_storage_search_ap(list, WIFI_MGR_SEARCH_AP_BUFFER_SIZE, SEARCH_ALL, NULL);
for (int i = 0; i < count; i++) {
LOGI("Deleting saved AP: ssid=%s", list[i].ssid);
wifi_mgr_storage_delete_ap(&list[i]);
}
wifi_mgr_storage_save_ap(&cfg);
wifi_mgr_auto_connect_start(&autoconn_cfg);
LOGI("WiFi auto-connect started");
}
static int wait_for_wifi_connection(void)
{
int timeout = 30;
LOGI("Waiting for WiFi connection and IP address...");
while (timeout > 0) {
if (g_wifi_connected && g_get_ip_success) {
LOGI("WiFi connected and IP obtained");
vTaskDelay(pdMS_TO_TICKS(500));
return 0;
}
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
if (timeout % 5 == 0) {
LOGI("Waiting... (WiFi:%d, IP:%d, timeout:%d)", g_wifi_connected, g_get_ip_success, timeout);
}
}
LOGI("WiFi connection timeout");
return -1;
}
/* WebSocket 事件回调 */
static void on_ws_event(lisa_ws_event_t *event)
{
struct NetworkContext *ctx = (struct NetworkContext *)event->user;
switch (event->what) {
case LISA_WS_ON_CONNECTED:
LOGI("WebSocket connected");
ctx->ws_connected = true;
g_ws_connected = true;
break;
case LISA_WS_ON_DISCONNECTED:
LOGI("WebSocket disconnected");
ctx->ws_connected = false;
g_ws_connected = false;
break;
case LISA_WS_ON_ERROR:
LOGE("WebSocket error");
break;
case LISA_WS_ON_CONNECTING:
LOGI("WebSocket connecting...");
break;
default:
break;
}
}
/* WebSocket 数据接收回调 */
static void on_ws_data(lisa_ws_data_t *data)
{
struct NetworkContext *ctx = (struct NetworkContext *)data->user;
if (data->type == LISA_WS_BIN && data->len > 0) {
/* 将接收到的数据存储到缓冲区 */
size_t available = WS_RECV_BUFFER_SIZE - ctx->recv_len;
size_t copy_len = (data->len > available) ? available : data->len;
if (copy_len > 0) {
memcpy(ctx->recv_buffer + ctx->recv_len, data->buf, copy_len);
ctx->recv_len += copy_len;
LOGI("WS received %u bytes, buffer now has %u bytes", copy_len, ctx->recv_len);
} else {
LOGW("WS recv buffer full, dropping %u bytes", data->len);
}
}
}
/* Transport 接口实现 (WebSocket版本) */
static int32_t transport_recv(NetworkContext_t *pNetworkContext, void *pBuffer, size_t bytesToRecv)
{
if (!pNetworkContext->ws_connected) {
return -1;
}
/* 从缓冲区读取数据 */
size_t available = pNetworkContext->recv_len - pNetworkContext->recv_offset;
if (available == 0) {
/* 没有数据可读,返回0表示需要重试 */
vTaskDelay(pdMS_TO_TICKS(10));
return 0;
}
size_t to_read = (bytesToRecv > available) ? available : bytesToRecv;
memcpy(pBuffer, pNetworkContext->recv_buffer + pNetworkContext->recv_offset, to_read);
pNetworkContext->recv_offset += to_read;
/* 如果缓冲区读完了,重置指针 */
if (pNetworkContext->recv_offset >= pNetworkContext->recv_len) {
pNetworkContext->recv_len = 0;
pNetworkContext->recv_offset = 0;
}
return (int32_t)to_read;
}
static int32_t transport_send(NetworkContext_t *pNetworkContext, const void *pBuffer, size_t bytesToSend)
{
if (!pNetworkContext->ws_connected) {
return -1;
}
lisa_ws_err_e ret = lisa_ws_send_binary(pNetworkContext->ws_handle, pBuffer, bytesToSend);
if (ret == LISA_WS_OK) {
return (int32_t)bytesToSend;
} else if (ret == LISA_WS_DISCONNECT) {
LOGE("WebSocket disconnected during send");
return -1;
} else {
LOGE("WebSocket send error: %d", ret);
return 0; /* 暂时错误,可以重试 */
}
}
static uint32_t get_time_ms(void)
{
return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
/* MQTT 事件回调 */
static void mqtt_event_callback(MQTTContext_t *pContext, MQTTPacketInfo_t *pPacketInfo,
MQTTDeserializedInfo_t *pDeserializedInfo)
{
if (pPacketInfo->type == MQTT_PACKET_TYPE_PUBLISH) {
MQTTPublishInfo_t *pPublishInfo = pDeserializedInfo->pPublishInfo;
LOGI("Received PUBLISH: topic=%.*s, payload=%.*s",
(int)pPublishInfo->topicNameLength, pPublishInfo->pTopicName,
(int)pPublishInfo->payloadLength, (char *)pPublishInfo->pPayload);
} else if (pPacketInfo->type == MQTT_PACKET_TYPE_SUBACK) {
LOGI("Received SUBACK");
} else if (pPacketInfo->type == MQTT_PACKET_TYPE_PUBACK) {
LOGI("Received PUBACK");
}
}
static int connect_to_broker_ws(struct NetworkContext *pNetworkContext)
{
lisa_ws_request_t ws_req = {0};
LOGI("Initializing WebSocket connection to ws://%s:%s%s...",
MQTT_BROKER_HOST, MQTT_BROKER_PORT, MQTT_BROKER_PATH);
/* 初始化接收缓冲区 */
pNetworkContext->recv_buffer = g_ws_recv_buffer;
pNetworkContext->recv_len = 0;
pNetworkContext->recv_offset = 0;
pNetworkContext->ws_connected = false;
/* 配置 WebSocket 请求 */
ws_req.scheme = (uint8_t *)"ws";
ws_req.host = (uint8_t *)MQTT_BROKER_HOST;
ws_req.port = (uint8_t *)MQTT_BROKER_PORT;
ws_req.path = (uint8_t *)MQTT_BROKER_PATH;
ws_req.timeout = 15000; /* 增加超时时间到15秒 */
ws_req.user = pNetworkContext;
ws_req.on_event = on_ws_event;
ws_req.on_data = on_ws_data;
ws_req.extra_header = "\r\nSec-WebSocket-Protocol: mqtt\r\n";
/* 创建 WebSocket 实例 */
pNetworkContext->ws_handle = lisa_ws_init(&ws_req);
if (pNetworkContext->ws_handle == NULL) {
LOGE("Failed to initialize WebSocket");
return -1;
}
/* 连接到 WebSocket 服务器 */
lisa_ws_err_e ret = lisa_ws_connect(pNetworkContext->ws_handle);
if (ret != LISA_WS_OK) {
LOGE("Failed to connect WebSocket: %d", ret);
lisa_ws_cleanup(pNetworkContext->ws_handle);
return -1;
}
/* 等待连接建立 - 使用事件状态而非超时计数 */
int check_count = 0;
int max_checks = 100; /* 10秒超时 (100 * 100ms) */
while (check_count < max_checks) {
if (pNetworkContext->ws_connected) {
break;
}
vTaskDelay(pdMS_TO_TICKS(100));
check_count++;
if (check_count % 10 == 0) {
LOGI("Waiting for WebSocket connection... (%d/%d)", check_count, max_checks);
}
}
if (!pNetworkContext->ws_connected) {
LOGE("WebSocket connection timeout after %d checks", check_count);
lisa_ws_cleanup(pNetworkContext->ws_handle);
return -1;
}
LOGI("Connected to MQTT broker via WebSocket");
return 0;
}
static void cleanup_ws(struct NetworkContext *pNetworkContext)
{
if (pNetworkContext->ws_handle) {
lisa_ws_disconnect(pNetworkContext->ws_handle);
lisa_ws_cleanup(pNetworkContext->ws_handle);
pNetworkContext->ws_handle = NULL;
}
}
int main(int argc, char **argv)
{
MQTTContext_t mqttContext;
MQTTConnectInfo_t connectInfo;
MQTTSubscribeInfo_t subscribeInfo;
MQTTPublishInfo_t publishInfo;
MQTTFixedBuffer_t networkBuffer;
TransportInterface_t transport;
struct NetworkContext networkContext = {0};
MQTTStatus_t mqttStatus;
bool sessionPresent;
LOGI("MQTT over WebSocket Test Starting...");
/* 初始化 */
user_mac_manager_init();
net_dhcp_register_status_callback(dhcp_status_callback, NULL);
lisa_wifi_ops_t ops = { .custom_mac = custom_get_wifi_mac };
lisa_wifi_init(&ops);
user_fs_init();
lisa_kv_init();
user_wifi_manager_init();
if (wait_for_wifi_connection() != 0) {
LOGE("Failed to connect to WiFi");
return -1;
}
LOGI("\n========================================");
LOGI("MQTT over WebSocket 功能测试");
LOGI("========================================\n");
/* 连接到 MQTT Broker (WebSocket) */
if (connect_to_broker_ws(&networkContext) != 0) {
return -1;
}
/* 设置 Transport 接口 */
transport.recv = transport_recv;
transport.send = transport_send;
transport.pNetworkContext = &networkContext;
transport.writev = NULL;
/* 设置网络缓冲区 */
networkBuffer.pBuffer = g_network_buffer;
networkBuffer.size = NETWORK_BUFFER_SIZE;
/* 初始化 MQTT Context */
mqttStatus = MQTT_Init(&mqttContext, &transport, get_time_ms, mqtt_event_callback, &networkBuffer);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Init failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
/* 建立 MQTT 连接 */
memset(&connectInfo, 0, sizeof(connectInfo));
connectInfo.cleanSession = true;
connectInfo.pClientIdentifier = MQTT_CLIENT_ID;
connectInfo.clientIdentifierLength = strlen(MQTT_CLIENT_ID);
connectInfo.keepAliveSeconds = 60;
LOGI("=== 测试 1: MQTT CONNECT ===");
mqttStatus = MQTT_Connect(&mqttContext, &connectInfo, NULL, 5000, &sessionPresent);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Connect failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("MQTT Connected successfully");
vTaskDelay(pdMS_TO_TICKS(500));
/* 订阅主题 */
LOGI("\n=== 测试 2: MQTT SUBSCRIBE ===");
memset(&subscribeInfo, 0, sizeof(subscribeInfo));
subscribeInfo.qos = MQTTQoS0;
subscribeInfo.pTopicFilter = MQTT_TOPIC_SUB;
subscribeInfo.topicFilterLength = strlen(MQTT_TOPIC_SUB);
mqttStatus = MQTT_Subscribe(&mqttContext, &subscribeInfo, 1, MQTT_GetPacketId(&mqttContext));
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Subscribe failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("Subscribe request sent for topic: %s", MQTT_TOPIC_SUB);
/* 处理 SUBACK */
mqttStatus = MQTT_ProcessLoop(&mqttContext);
if (mqttStatus != MQTTSuccess && mqttStatus != MQTTNeedMoreBytes) {
LOGE("MQTT_ProcessLoop failed: %s", MQTT_Status_strerror(mqttStatus));
}
vTaskDelay(pdMS_TO_TICKS(500));
/* 发布消息 */
LOGI("\n=== 测试 3: MQTT PUBLISH ===");
memset(&publishInfo, 0, sizeof(publishInfo));
publishInfo.qos = MQTTQoS0;
publishInfo.pTopicName = MQTT_TOPIC_PUB;
publishInfo.topicNameLength = strlen(MQTT_TOPIC_PUB);
publishInfo.pPayload = "Hello from ARCS via WebSocket!";
publishInfo.payloadLength = strlen("Hello from ARCS via WebSocket!");
mqttStatus = MQTT_Publish(&mqttContext, &publishInfo, MQTT_GetPacketId(&mqttContext));
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Publish failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("Published message to topic: %s", MQTT_TOPIC_PUB);
vTaskDelay(pdMS_TO_TICKS(1000));
/* 处理接收的消息 */
LOGI("\n=== 测试 4: 接收消息 ===");
while (1) {
mqttStatus = MQTT_ProcessLoop(&mqttContext);
if (mqttStatus != MQTTSuccess && mqttStatus != MQTTNeedMoreBytes) {
LOGE("MQTT_ProcessLoop failed: %s", MQTT_Status_strerror(mqttStatus));
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
/* 断开连接 */
LOGI("\n=== 测试 5: MQTT DISCONNECT ===");
mqttStatus = MQTT_Disconnect(&mqttContext);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Disconnect failed: %s", MQTT_Status_strerror(mqttStatus));
} else {
LOGI("MQTT Disconnected successfully");
}
LOGI("\n========================================");
LOGI("所有 MQTT WebSocket 测试完成");
LOGI("========================================\n");
cleanup:
cleanup_ws(&networkContext);
return 0;
}

View File

@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(listenai-cmake REQUIRED HINTS $ENV{ARCS_BASE})
project(arcs)
listenai_include_directories(
./
)
add_subdirectory(src)
listenai_add_executable(${PROJECT_NAME})

View File

@@ -0,0 +1 @@
osource "$ARCS_BASE/Kconfig"

View File

@@ -0,0 +1,384 @@
# MQTT over WebSocket Secure (WSS) 示例
本示例演示如何在 ARCS 平台上使用 coreMQTT 库通过 WebSocket Secure 连接到 MQTT broker。
## 功能特性
- 使用 lisa_websocket 组件实现 WebSocket Secure 连接
- 支持 MQTT over WSS (端口 8084)
- TLS/SSL 加密传输
- WiFi 自动连接
- MQTT 发布/订阅功能
- 事件驱动的 WebSocket 通信
- 非阻塞 I/O 操作
## 硬件要求
- ARCS 开发板
- SD 卡(用于文件系统)
- WiFi 网络环境
## 配置说明
在使用前,请修改 [src/main.c](src/main.c) 中的以下配置:
```c
// MQTT Broker 配置
#define MQTT_BROKER_HOST "broker.emqx.io" // MQTT broker 地址
#define MQTT_BROKER_PORT "8084" // WebSocket Secure 端口
#define MQTT_BROKER_PATH "/mqtt" // MQTT over WSS 路径
#define MQTT_CLIENT_ID "arcs_mqtt_wss_client"
// WiFi 配置
#define TARGET_WIFI_SSID "your_wifi_ssid" // 修改为你的WiFi名称
#define TARGET_WIFI_PWD "your_wifi_pwd" // 修改为你的WiFi密码
// MQTT 主题
#define MQTT_TOPIC_PUB "arcs/test/pub" // 发布主题
#define MQTT_TOPIC_SUB "arcs/test/sub" // 订阅主题
```
## WebSocket Secure 配置
### 连接参数
```c
lisa_ws_request_t ws_req = {
.scheme = "wss", // WebSocket Secure 协议
.host = "broker.emqx.io", // Broker 地址
.port = "8084", // WSS 端口
.path = "/mqtt", // MQTT 路径
.timeout = 15000, // 连接超时(毫秒)
.on_event = on_ws_event, // 事件回调
.on_data = on_ws_data // 数据回调
};
```
### MQTT 子协议
MQTT over WebSocket 需要在 WebSocket 握手时声明 `mqtt` 子协议。`lisa_websocket` 组件会自动检测路径中包含 `/mqtt` 并设置正确的子协议头:
```
Sec-WebSocket-Protocol: mqtt
```
### SSL/TLS 配置
当前配置为**不验证服务器证书**模式(适用于测试):
```c
nopoll_conn_opts_ssl_peer_verify(nopoll_opts, nopoll_false);
```
生产环境建议启用证书验证。
## 编译
```{eval-rst}
.. include:: /sample_build.rst
```
## 运行效果
程序启动后,将依次执行以下步骤:
1. **WiFi 连接**: 自动连接到配置的 WiFi 网络
2. **WebSocket Secure 连接**: 建立到 MQTT broker 的加密 WebSocket 连接
3. **MQTT 连接**: 通过 WSS 通道建立 MQTT 连接
4. **订阅主题**: 订阅指定的 MQTT 主题
5. **发布消息**: 发布测试消息
6. **接收消息**: 持续监听和处理接收的消息
### 预期日志输出
```
[mqtt-wss-test] MQTT over WebSocket Secure Test Starting...
[mqtt-wss-test] WiFi connected and IP obtained
========================================
MQTT WebSocket Secure 功能测试
========================================
[lisa-ws] websocket thread waiting for connect sem
[lisa-ws] Got connect signal
[mqtt-wss-test] Waiting for WebSocket Secure connection... (10/100)
[mqtt-wss-test] Connected to MQTT broker via WebSocket Secure
=== 测试 1: MQTT CONNECT ===
[mqtt-wss-test] MQTT Connected successfully
=== 测试 2: MQTT SUBSCRIBE ===
[mqtt-wss-test] Subscribe request sent for topic: arcs/test/sub
=== 测试 3: MQTT PUBLISH ===
[mqtt-wss-test] Published message to topic: arcs/test/pub
=== 测试 4: 接收消息 ===
[mqtt-wss-test] Received PUBLISH: topic=arcs/test/sub, payload=...
```
## 主要差异(相比其他传输方式)
| 特性 | TCP | SSL | WebSocket | WSS |
|-----|-----|-----|-----------|-----|
| 端口 | 1883 | 8883 | 8083 | 8084 |
| 加密 | 无 | TLS 1.2+ | 无 | TLS 1.2+ |
| 传输层 | TCP | TCP + TLS | TCP + WS | TCP + WS + TLS |
| 库依赖 | lwIP | mbedTLS | nopoll | nopoll + mbedTLS |
| 连接建立 | TCP 握手 | TCP + SSL握手 | TCP + WS握手 | TCP + WS + SSL握手 |
| 数据传输 | socket | mbedtls_ssl | lisa_ws | lisa_ws (加密) |
| 子协议 | 无 | 无 | mqtt | mqtt |
| 适用场景 | 标准MQTT | 加密MQTT | 穿越代理 | 加密+穿越代理 |
## 配置文件 (prj.conf)
```ini
# WebSocket Secure 支持
CONFIG_LISA_WEBSOCKET=y
CONFIG_MODULE_NOPOLL=y
# mbedTLS (SSL/TLS支持 - WSS需要)
CONFIG_MBEDTLS=y
CONFIG_MBEDTLS_BUILTIN=y
CONFIG_MBEDTLS_ENABLE_HEAP=y
CONFIG_MBEDTLS_HEAP_SIZE=0x8000
# mbedTLS 功能配置
CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
CONFIG_MBEDTLS_KEY_EXCHANGE_RSA_ENABLED=y
CONFIG_MBEDTLS_SSL_CLI_C=y
CONFIG_MBEDTLS_SSL_TLS_C=y
CONFIG_MBEDTLS_X509_CRT_PARSE_C=y
CONFIG_MBEDTLS_PEM_PARSE_C=y
# coreMQTT
CONFIG_SDK_MODULE_COREMQTT=y
```
## 调试技巧
### WebSocket 连接状态
通过事件回调监控 WebSocket 连接状态:
```c
static void on_ws_event(lisa_ws_event_t *event)
{
switch (event->what) {
case LISA_WS_ON_CONNECTING:
LOGI("WebSocket connecting...");
break;
case LISA_WS_ON_CONNECTED:
LOGI("WebSocket connected");
break;
case LISA_WS_ON_DISCONNECTED:
LOGI("WebSocket disconnected");
break;
}
}
```
### 常见问题
1. **SSL 握手失败**:
- 检查 mbedTLS 堆大小 (CONFIG_MBEDTLS_HEAP_SIZE)
- 确认系统时间正确 (影响证书验证)
- 检查是否禁用了证书验证
2. **连接超时**:
- 检查 broker 是否支持 WSS
- 确认端口号正确 (通常是 8084)
- 确认路径正确 (通常是 `/mqtt`)
3. **数据传输异常**:
- 检查 `recv_buffer` 大小是否足够
- 确认事件回调正确设置
- 检查 SSL 加密是否正常
## 功能测试
### 使用 MQTTX 客户端测试
[MQTTX](https://mqttx.app/) 是一个优秀的跨平台 MQTT 客户端工具,可用于测试设备的发布和订阅功能。
#### 安装 MQTTX CLI
```bash
# Linux
curl -LO https://www.emqx.com/en/downloads/MQTTX/v1.12.1/mqttx-cli-linux-x64
sudo install ./mqttx-cli-linux-x64 /usr/local/bin/mqttx
```
#### 测试场景 1: 订阅设备发布的消息
在电脑上运行以下命令,订阅设备发布的主题:
```bash
mqttx sub -h broker.emqx.io -p 8084 -t "arcs/test/pub" -l wss --path "/mqtt"
```
**预期结果:**
```
✔ Connected
✔ Subscribed to arcs/test/pub
topic: arcs/test/pub, qos: 0, size: 38B
Hello from ARCS via WebSocket Secure!
```
设备会定期发布消息到 `arcs/test/pub` 主题,MQTTX 客户端会接收并显示这些消息。
#### 测试场景 2: 向设备发布消息
在电脑上运行以下命令,向设备订阅的主题发送消息:
```bash
mqttx pub -h broker.emqx.io -p 8084 -t "arcs/test/sub" -m "Hello ARCS WSS" -l wss --path "/mqtt"
```
**预期结果:**
MQTTX 客户端输出:
```
✔ Connected
✔ Message published
```
设备串口日志输出:
```
I/mqtt-wss-test [1034:43:49.966 1 main] Received PUBLISH: topic=arcs/test/sub, payload=Hello ARCS WSS
```
#### MQTTX 参数说明
| 参数 | 说明 | 示例值 |
|-----|------|--------|
| `-h` | MQTT broker 地址 | `broker.emqx.io` |
| `-p` | 端口号 | `8084` (WSS端口) |
| `-t` | 主题名称 | `arcs/test/pub` |
| `-m` | 消息内容 | `"Hello ARCS WSS"` |
| `--protocol` | 协议类型 | `wss` (WebSocket Secure) |
| `--path` | WebSocket 路径 | `/mqtt` |
| `-q` | QoS 级别 (可选) | `0`, `1`, `2` |
| `-u` | 用户名 (可选) | `username` |
| `-P` | 密码 (可选) | `password` |
#### 完整测试流程
1. **启动设备**: 编译并运行 WSS MQTT 示例程序
2. **订阅测试**: 在电脑上运行 MQTTX 订阅命令,验证能收到设备消息
3. **发布测试**: 在电脑上运行 MQTTX 发布命令,验证设备能收到消息
4. **双向通信**: 同时开启订阅和发布,观察双向消息传输
### 使用浏览器 WebSocket 测试
WebSocket Secure 也可以在浏览器中测试:
```javascript
// 在浏览器控制台运行
const ws = new WebSocket('wss://broker.emqx.io:8084/mqtt', 'mqtt');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
console.log('WSS connected');
// 这里可以发送 MQTT 协议数据包
};
ws.onmessage = (event) => {
console.log('Received:', event.data);
};
```
### 测试 Checklist
- [ ] WiFi 连接成功并获取 IP 地址
- [ ] SSL/TLS 握手成功完成
- [ ] WebSocket 握手成功完成
- [ ] MQTT 子协议正确声明
- [ ] MQTT 连接建立成功
- [ ] 主题订阅成功
- [ ] 设备能够成功发布消息
- [ ] 外部客户端能够接收到设备发布的消息
- [ ] 设备能够接收外部客户端发布的消息
- [ ] 消息内容正确无误
- [ ] 加密连接稳定,无异常断线
## 测试建议
1. **先测试 WS 版本**: 确保 WebSocket 基本功能正常
2. **检查内存配置**: WSS 需要更多内存 (WebSocket + SSL)
3. **验证网络连接**: 确保可以访问目标 broker 的 8084 端口
4. **使用公共 broker 测试**: 如 `broker.emqx.io`
5. **使用专业工具**: 推荐使用 MQTTX 或浏览器开发者工具进行测试
## 公共 MQTT Broker (WebSocket Secure)
| Broker | 地址 | WSS端口 | 路径 | 认证 |
|--------|------|---------|------|------|
| EMQX | broker.emqx.io | 8084 | /mqtt | 可选 |
| HiveMQ | broker.hivemq.com | 8884 | /mqtt | 可选 |
## 故障排查
### SSL/TLS 握手失败
1. 检查 mbedTLS 堆大小 (CONFIG_MBEDTLS_HEAP_SIZE)
2. 确认系统时间正确
3. 检查是否正确禁用证书验证 (测试模式)
4. 查看 SSL 错误码
### WebSocket 连接超时
1. 检查 broker 是否支持 WSS (通常端口 8084)
2. 确认路径正确 (通常是 `/mqtt`)
3. 检查 MQTT 子协议是否正确声明
4. 增加 `timeout` 参数值
### MQTT 握手失败
1. 检查 WebSocket 连接是否已建立 (`ws_connected` 标志)
2. 确认在 WebSocket 连接成功后才发起 MQTT 连接
3. 检查 MQTT client ID 是否重复
### 数据接收异常
1. 检查 `recv_buffer` 大小 (默认 2048 字节)
2. 确认 `on_data` 回调正确实现
3. 检查数据类型 (MQTT 使用二进制帧)
### 内存不足
1. 增加 `CONFIG_MBEDTLS_HEAP_SIZE` (SSL 需要)
2. 增加 `CONFIG_PSRAM_HEAP_SIZE`
3. 减少 `NETWORK_BUFFER_SIZE`
4. 检查 WebSocket 队列大小配置
## WSS 的优势
相比其他传输方式,WSS 提供:
1. **双重安全**: WebSocket 协议 + TLS 加密
2. **防火墙友好**: 使用 443 或 8084 端口,易于穿越企业防火墙
3. **代理支持**: 可通过 HTTP 代理建立连接
4. **浏览器兼容**: 可直接在 Web 应用中使用
5. **完整加密**: 所有数据都经过 TLS 加密传输
## 相关示例对比
| 示例 | 传输方式 | 加密 | 端口 | 适用场景 |
|------|---------|------|------|---------|
| [tcp](../tcp/) | TCP | 无 | 1883 | 内网/测试 |
| [ssl](../ssl/) | TCP + TLS | 是 | 8883 | 加密通信 |
| [ws](../ws/) | WebSocket | 无 | 8083 | 穿越代理 |
| **wss** | WebSocket + TLS | 是 | 8084 | **生产环境推荐** |
## 相关文档
- [coreMQTT 库文档](https://www.freertos.org/mqtt/index.html)
- [WebSocket 协议规范](https://datatracker.ietf.org/doc/html/rfc6455)
- [MQTT over WebSocket 规范](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718127)
- [nopoll 库文档](http://www.aspl.es/nopoll/)
- [mbedTLS 文档](https://mbed-tls.readthedocs.io/)
## 许可证
Copyright (c) 2025, LISTENAI
SPDX-License-Identifier: Apache-2.0

View File

@@ -0,0 +1,177 @@
#!/bin/bash
set -e
usage() {
echo "使用方式: $0 [选项]"
echo "选项:"
echo " -S, --Source <path> 指定项目源码路径 (默认为当前脚本所在目录)"
echo " -t, --target <target> 指定构建目标 (如 menuconfig)"
echo " -C, --Clean 清理构建目录"
echo " -B, --build 构建输出目录"
echo " -h, --help 显示此帮助信息"
echo " -r, --release 以 Release 模式构建 (移除 DEBUG_PATH 信息)"
echo " -w, --warnings-as-errors 将警告视为错误"
echo " -D<var>=<value> 传递 CMake 变量 (可多次使用)"
echo ""
echo "示例:"
echo " $0 -S samples/helloworld -DBOARD=arcs_mini 指定板型构建"
echo " $0 -S samples/helloworld -DBOARD=arcs_evb 使用 EVB 板型"
echo " $0 -S samples/helloworld -t menuconfig -DBOARD=arcs_mini 运行 menuconfig"
echo " $0 -C -S samples/helloworld -DBOARD=arcs_mini 清理并重新构建"
echo " $0 -S samples/helloworld -DBOARD=my_board -DBOARD_SEARCH_PATH=/path/to/boards 使用自定义板型"
exit 1
}
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_PATH="$SCRIPT_DIR"
TARGET=""
CLEAN=false
OUTPUT="build"
WARNINGS_AS_ERRORS=false
RELEASE=false
ARCS_BASE_DIR_NAME="arcs-sdk"
ARCS_DEV_TOOLS_DIR_NAME="listenai-dev-tools"
ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME="gcc"
ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME="listenai-tools"
find_arcs_base() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_BASE_DIR_NAME"
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found ARCS_BASE: $current_dir/$dir_name"
export ARCS_BASE="$current_dir/$dir_name"
return 0
fi
current_dir=$(dirname "$current_dir")
done
echo "ARCS_BASE not found, Please add ARCS_BASE environment variable or set ARCS_BASE_DIR_NAME to the correct directory."
echo "Current Target ARCS_BASE directory name: $ARCS_BASE_DIR_NAME."
exit 1
}
find_dev_tools() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_DEV_TOOLS_DIR_NAME"
echo "trying to find $dir_name in parent directories..."
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found $dir_name: $current_dir/$dir_name"
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME" ]; then
echo "Found LISTENAI_TOOLS_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
export LISTENAI_TOOLS_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
fi
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME" ]; then
echo "Found NUCLEI_TOOLCHAIN_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
export NUCLEI_TOOLCHAIN_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
fi
return 0
fi
current_dir=$(dirname "$current_dir")
done
}
while [[ $# -gt 0 ]]; do
case $1 in
-S|--Source)
PROJECT_PATH="$2"
shift 2
;;
-B|--build)
OUTPUT="$2"
shift 2
;;
-t|--target)
TARGET="$2"
shift 2
;;
-C|--Clean)
CLEAN=true
shift 1
;;
-w|--warnings-as-errors)
WARNINGS_AS_ERRORS=true
shift 1
;;
-h|--help)
usage
;;
-r|--release)
RELEASE=true
shift 1
;;
-D*)
CMAKE_VARS+=("$1")
shift 1
;;
*)
echo "未知参数: $1"
usage
;;
esac
done
echo "Source: $PROJECT_PATH"
echo "Target: $TARGET"
echo "Clean : $CLEAN"
if [ -z "$LISTENAI_TOOLS_PATH" ] || [ -z "$NUCLEI_TOOLCHAIN_PATH" ]; then
find_dev_tools
fi
if [ -z "${LISTENAI_TOOLS_PATH}" ]; then
export LISTENAI_TOOLS_PATH="请添加 LISTENAI_TOOLS_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 LISTENAI_TOOLS_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
if [ -z "${NUCLEI_TOOLCHAIN_PATH}" ]; then
export NUCLEI_TOOLCHAIN_PATH="请添加 NUCLEI_TOOLCHAIN_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 NUCLEI_TOOLCHAIN_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
############### 下面代码不用修改 ##################
# 构建工具的位置
CMAKE_PROGRAM="$LISTENAI_TOOLS_PATH/cmake/bin/cmake"
NINJA_PROGRAM="$LISTENAI_TOOLS_PATH/ninja/ninja"
# 配置环境变量 ARCS_BASE
if [ -z "$ARCS_BASE" ]; then
find_arcs_base
fi
if [ "$CLEAN" = true ]; then
rm -rf $OUTPUT
fi
# Initialize CMAKE_VARS array if it doesn't exist
declare -a CMAKE_VARS
# Add warnings-as-errors flag if enabled
if [ "$WARNINGS_AS_ERRORS" = true ]; then
CMAKE_VARS+=("-DCMAKE_C_FLAGS=-Werror")
CMAKE_VARS+=("-DCMAKE_CXX_FLAGS=-Werror")
echo "Treating warnings as errors"
fi
# Add release flags if enabled
if [ "$RELEASE" = true ]; then
CMAKE_VARS+=("-DENABLE_DEBUG_PATH=OFF")
echo "Release mode enabled (-DENABLE_DEBUG_PATH=OFF)"
fi
$CMAKE_PROGRAM -B "$OUTPUT" -G Ninja -S "$PROJECT_PATH" \
-DCMAKE_MAKE_PROGRAM="$NINJA_PROGRAM" \
"${CMAKE_VARS[@]}"
if [ -z "$TARGET" ]; then
$CMAKE_PROGRAM --build "$OUTPUT" -j4
else
$CMAKE_PROGRAM --build "$OUTPUT" --target "$TARGET"
fi

View File

@@ -0,0 +1,68 @@
CONFIG_HEAP_SIZE=0xC000
CONFIG_PSRAM_HEAP_SIZE=0x5b0000
# DISK驱动使能
CONFIG_DISK_DRIVER=y
# 文件系统配置
CONFIG_FILE_SYSTEM=y
CONFIG_FATFS_FILESYSTEM=y
CONFIG_LSFS=y
CONFIG_LSFS_FAT=y
CONFIG_LSFS_REGISTER_VFS=y
CONFIG_LVFS_POSIX_API_ALIAS=y
CONFIG_LVFS=y
CONFIG_LVFS_POSIX_API=y
CONFIG_FS_ENV_MEMORY_MANAGEMENT_USE_PSRAM=y
# 网络配置
CONFIG_LISA_WIFI=y
CONFIG_WIFI_LWIP_SAME_CORE=y
# MAC Manager
CONFIG_MAC_MANAGER=y
CONFIG_MAC_MANAGER_MAC_BY_CHIP_ID=y
# WIFI MANAGER
CONFIG_WIFI_MANAGER=y
# LISA Shell
CONFIG_LISA_SHELL=y
CONFIG_SYSLOG_PRINTF_REDIRECT=y
# LISA Porting
CONFIG_LISA_PORTING=y
# SD Card drivers
CONFIG_DISK_DRIVER_SDMMC=y
CONFIG_LISA_SDMMC_DEVICE=y
CONFIG_CJSON=y
CONFIG_LISA_OS=y
CONFIG_LISA_KV=y
CONFIG_LISA_NETWORK=y
# coreMQTT
CONFIG_SDK_MODULE_COREMQTT=y
# WebSocket Secure支持 (MQTT over WSS)
CONFIG_LISA_WEBSOCKET=y
CONFIG_MODULE_NOPOLL=y
# mbedTLS (SSL/TLS支持 - WSS需要)
CONFIG_MBEDTLS=y
CONFIG_MBEDTLS_BUILTIN=y
CONFIG_MBEDTLS_ENABLE_HEAP=y
CONFIG_MBEDTLS_HEAP_SIZE=0x8000
# mbedTLS功能配置
CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
CONFIG_MBEDTLS_KEY_EXCHANGE_RSA_ENABLED=y
CONFIG_MBEDTLS_SSL_CLI_C=y
CONFIG_MBEDTLS_SSL_TLS_C=y
CONFIG_MBEDTLS_X509_CRT_PARSE_C=y
CONFIG_MBEDTLS_PEM_PARSE_C=y
CONFIG_BACK_TRACE=y

View File

@@ -0,0 +1,3 @@
tests:
samples.mqtt.wss:
build_only: true

View File

@@ -0,0 +1,10 @@
listenai_library_named(app)
listenai_library_sources(
main.c
fs/user_fs.c
)
listenai_include_directories(
./
./fs
)

View File

@@ -0,0 +1,86 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lsfs.h"
#include "disk/disk_access.h"
#include "lisa_sdmmc.h"
#include "log_print.h"
#if CONFIG_LVFS
#include "lvfs.h"
#endif
#define DISK_DEVICE "SD:"
#define DISK_MOUNT_POINT "/" DISK_DEVICE
static struct lsfs_mount_t flash_lsfs_mnt = {
.type = LSFS_FATFS,
.mnt_point = DISK_MOUNT_POINT,
.fs_data = NULL,
};
static int fs_mount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_mount(mp);
if (ret != 0)
{
CLOG("No file system, try reformatting, mount ret: %d", ret);
ret = lsfs_mkfs(mp->type, &mp->mnt_point[1], NULL, 0);
if (ret == 0)
{
CLOG("mkfs success, retry mount");
ret = lsfs_mount(mp);
}
else
{
CLOG("mkfs failed, ret: %d", ret);
}
}
if (ret != 0)
{
CLOG("Failed to mount filesystem: %d", ret);
return ret;
}
CLOG("%s mounted successfully", mp->mnt_point);
return 0;
}
static void user_fs_change_to_root_folder(void)
{
int ret;
ret = lvfs_chdrive(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change drive to %s, error: %d\n", DISK_MOUNT_POINT, ret);
}
ret = lvfs_chdir(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change directory to /SD:/, error: %d\n", ret);
} else {
CLOG("Changed to directory /SD:/\n");
}
}
int user_fs_init(void){
lisa_sdmmc_probe(lisa_device_get("sdmmc0"));
disk_init(NULL);
#if CONFIG_LVFS
lvfs_init();
#endif
lsfs_init();
if(0 != fs_mount(&flash_lsfs_mnt)){
return -1;
}
user_fs_change_to_root_folder();
return 0;
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef USER_FS_H
#define USER_FS_H
int user_fs_init(void);
#endif /* USER_FS_H */

View File

@@ -0,0 +1,502 @@
#include "lisa_thread.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "stdbool.h"
#include "FreeRTOS.h"
#include "task.h"
#define TAG "mqtt-wss-test"
#include "lisa_log.h"
#include "lisa_kv.h"
#include "mac_manager.h"
#include "mac_manager_ops.h"
#include "wifi_manager/wifi_manager.h"
#include "user_fs.h"
#include "lisa_wifi.h"
#include "ls_wifi_type.h"
#include "net_al.h"
#include "net_def.h"
#include "core_mqtt.h"
#include "lisa_websocket.h"
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
#define MQTT_BROKER_HOST "broker.emqx.io"
#define MQTT_BROKER_PORT "8084" // WebSocket Secure 端口
#define MQTT_BROKER_PATH "/mqtt" // MQTT over WSS 路径
#define MQTT_CLIENT_ID "arcs_mqtt_wss_client"
#define TARGET_WIFI_SSID "Xiaomi_listenai_2.4G"
#define TARGET_WIFI_PWD "a12345678"
#define MQTT_TOPIC_PUB "arcs/test/pub"
#define MQTT_TOPIC_SUB "arcs/test/sub"
#define WIFI_MGR_AUTO_CONNECT_INTERVAL_MS 2000
#define WIFI_MGR_SEARCH_AP_BUFFER_SIZE 10
#define NETWORK_BUFFER_SIZE 2048
#define WS_RECV_BUFFER_SIZE 4096
static mac_manager_t *m_mac_manager;
static wifi_mgr_sta_config_t list[WIFI_MGR_SEARCH_AP_BUFFER_SIZE] = { 0 };
static volatile bool g_wifi_connected = false;
static volatile bool g_get_ip_success = false;
static volatile bool g_ws_connected = false;
/* WebSocket + MQTT 上下文 */
struct NetworkContext {
lisa_ws_t *ws_handle;
uint8_t *recv_buffer;
size_t recv_len;
size_t recv_offset;
bool ws_connected;
};
/* MQTT 缓冲区 */
static uint8_t g_network_buffer[NETWORK_BUFFER_SIZE];
static uint8_t g_ws_recv_buffer[WS_RECV_BUFFER_SIZE];
static int8_t custom_get_wifi_mac(uint8_t mac_addr[6])
{
int8_t ret = 0;
if (!mac_addr)
return -1;
ret = mac_manager_get(m_mac_manager, mac_addr, 6);
LOGI("custom_get_wifi_mac: %d, %02X:%02X:%02X:%02X:%02X:%02X", ret,
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
return ret;
}
static void dhcp_status_callback(int vif_idx, bool success, uint32_t ip_addr,
uint32_t netmask, uint32_t gateway, void *arg)
{
if (success) {
LOGI("DHCP Success: IP=%d.%d.%d.%d",
ip_addr & 0xff, (ip_addr >> 8) & 0xff,
(ip_addr >> 16) & 0xff, (ip_addr >> 24) & 0xff);
g_get_ip_success = true;
} else {
LOGI("DHCP Failed on VIF-%d", vif_idx);
}
}
static void wifi_mgr_connection_status_cb(wifi_mgr_connection_info_t *connection_info, void *arg)
{
net_if_t *net_if;
LOGI("WiFi connection status: %d", connection_info->status);
switch (connection_info->status) {
case WIFI_MGR_STA_CONNECTED:
LOGI("WiFi connected to AP");
g_wifi_connected = true;
net_if = net_if_get(WIFI_VIF_STA_IDX);
net_if_up(net_if);
if (!net_if->static_ip) {
ls_dhcpc_start(WIFI_VIF_STA_IDX);
}
break;
case WIFI_MGR_STA_DISCONNECTED:
LOGI("WiFi disconnected from AP");
g_wifi_connected = false;
ls_dhcpc_stop(WIFI_VIF_STA_IDX);
net_if_down(net_if_get(WIFI_VIF_STA_IDX));
break;
default:
break;
}
}
static void user_mac_manager_init(void)
{
mac_manager_config_t config = {
.random_mac_if_mac_invalid = false,
};
m_mac_manager = mac_manager_init(mac_manager_ops_get()->mem_ops,
mac_manager_ops_get()->content_ops, &config);
if (m_mac_manager == NULL) {
LOGI("mac_manager_init failed\n");
}
}
static void user_wifi_manager_init(void)
{
wifi_mgr_sta_config_t cfg = {
.ssid = TARGET_WIFI_SSID,
.pwd = TARGET_WIFI_PWD,
};
wifi_mgr_autoconn_config_t autoconn_cfg = {
.interval_ms = WIFI_MGR_AUTO_CONNECT_INTERVAL_MS,
};
wifi_mgr_init(wifi_mgr_ops_get());
wifi_mgr_sta_enable();
wifi_mgr_sta_add_connection_cb(wifi_mgr_connection_status_cb, NULL);
int count = wifi_mgr_storage_search_ap(list, WIFI_MGR_SEARCH_AP_BUFFER_SIZE, SEARCH_ALL, NULL);
for (int i = 0; i < count; i++) {
LOGI("Deleting saved AP: ssid=%s", list[i].ssid);
wifi_mgr_storage_delete_ap(&list[i]);
}
wifi_mgr_storage_save_ap(&cfg);
wifi_mgr_auto_connect_start(&autoconn_cfg);
LOGI("WiFi auto-connect started");
}
static int wait_for_wifi_connection(void)
{
int timeout = 30;
LOGI("Waiting for WiFi connection and IP address...");
while (timeout > 0) {
if (g_wifi_connected && g_get_ip_success) {
LOGI("WiFi connected and IP obtained");
vTaskDelay(pdMS_TO_TICKS(500));
return 0;
}
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
if (timeout % 5 == 0) {
LOGI("Waiting... (WiFi:%d, IP:%d, timeout:%d)", g_wifi_connected, g_get_ip_success, timeout);
}
}
LOGI("WiFi connection timeout");
return -1;
}
/* WebSocket 事件回调 */
static void on_ws_event(lisa_ws_event_t *event)
{
struct NetworkContext *ctx = (struct NetworkContext *)event->user;
switch (event->what) {
case LISA_WS_ON_CONNECTED:
LOGI("WebSocket Secure connected");
ctx->ws_connected = true;
g_ws_connected = true;
break;
case LISA_WS_ON_DISCONNECTED:
LOGI("WebSocket Secure disconnected");
ctx->ws_connected = false;
g_ws_connected = false;
break;
case LISA_WS_ON_ERROR:
LOGE("WebSocket Secure error");
break;
case LISA_WS_ON_CONNECTING:
LOGI("WebSocket Secure connecting...");
break;
default:
break;
}
}
/* WebSocket 数据接收回调 */
static void on_ws_data(lisa_ws_data_t *data)
{
struct NetworkContext *ctx = (struct NetworkContext *)data->user;
if (data->type == LISA_WS_BIN && data->len > 0) {
/* 将接收到的数据存储到缓冲区 */
size_t available = WS_RECV_BUFFER_SIZE - ctx->recv_len;
size_t copy_len = (data->len > available) ? available : data->len;
if (copy_len > 0) {
memcpy(ctx->recv_buffer + ctx->recv_len, data->buf, copy_len);
ctx->recv_len += copy_len;
LOGI("WSS received %u bytes, buffer now has %u bytes", copy_len, ctx->recv_len);
} else {
LOGW("WSS recv buffer full, dropping %u bytes", data->len);
}
}
}
/* Transport 接口实现 (WebSocket Secure版本) */
static int32_t transport_recv(NetworkContext_t *pNetworkContext, void *pBuffer, size_t bytesToRecv)
{
if (!pNetworkContext->ws_connected) {
return -1;
}
/* 从缓冲区读取数据 */
size_t available = pNetworkContext->recv_len - pNetworkContext->recv_offset;
if (available == 0) {
/* 没有数据可读,返回0表示需要重试 */
vTaskDelay(pdMS_TO_TICKS(10));
return 0;
}
size_t to_read = (bytesToRecv > available) ? available : bytesToRecv;
memcpy(pBuffer, pNetworkContext->recv_buffer + pNetworkContext->recv_offset, to_read);
pNetworkContext->recv_offset += to_read;
/* 如果缓冲区读完了,重置指针 */
if (pNetworkContext->recv_offset >= pNetworkContext->recv_len) {
pNetworkContext->recv_len = 0;
pNetworkContext->recv_offset = 0;
}
return (int32_t)to_read;
}
static int32_t transport_send(NetworkContext_t *pNetworkContext, const void *pBuffer, size_t bytesToSend)
{
if (!pNetworkContext->ws_connected) {
return -1;
}
lisa_ws_err_e ret = lisa_ws_send_binary(pNetworkContext->ws_handle, pBuffer, bytesToSend);
if (ret == LISA_WS_OK) {
return (int32_t)bytesToSend;
} else if (ret == LISA_WS_DISCONNECT) {
LOGE("WebSocket Secure disconnected during send");
return -1;
} else {
LOGE("WebSocket Secure send error: %d", ret);
return 0; /* 暂时错误,可以重试 */
}
}
static uint32_t get_time_ms(void)
{
return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
/* MQTT 事件回调 */
static void mqtt_event_callback(MQTTContext_t *pContext, MQTTPacketInfo_t *pPacketInfo,
MQTTDeserializedInfo_t *pDeserializedInfo)
{
if (pPacketInfo->type == MQTT_PACKET_TYPE_PUBLISH) {
MQTTPublishInfo_t *pPublishInfo = pDeserializedInfo->pPublishInfo;
LOGI("Received PUBLISH: topic=%.*s, payload=%.*s",
(int)pPublishInfo->topicNameLength, pPublishInfo->pTopicName,
(int)pPublishInfo->payloadLength, (char *)pPublishInfo->pPayload);
} else if (pPacketInfo->type == MQTT_PACKET_TYPE_SUBACK) {
LOGI("Received SUBACK");
} else if (pPacketInfo->type == MQTT_PACKET_TYPE_PUBACK) {
LOGI("Received PUBACK");
}
}
static int connect_to_broker_wss(struct NetworkContext *pNetworkContext)
{
lisa_ws_request_t ws_req = {0};
LOGI("Initializing WebSocket Secure connection to wss://%s:%s%s...",
MQTT_BROKER_HOST, MQTT_BROKER_PORT, MQTT_BROKER_PATH);
/* 初始化接收缓冲区 */
pNetworkContext->recv_buffer = g_ws_recv_buffer;
pNetworkContext->recv_len = 0;
pNetworkContext->recv_offset = 0;
pNetworkContext->ws_connected = false;
/* 配置 WebSocket Secure 请求 */
ws_req.scheme = (uint8_t *)"wss"; /* 使用WSS协议(WebSocket Secure) */
ws_req.host = (uint8_t *)MQTT_BROKER_HOST;
ws_req.port = (uint8_t *)MQTT_BROKER_PORT;
ws_req.path = (uint8_t *)MQTT_BROKER_PATH;
ws_req.timeout = 15000; /* 增加超时时间到15秒 */
ws_req.user = pNetworkContext;
ws_req.on_event = on_ws_event;
ws_req.on_data = on_ws_data;
/* MQTT子协议已由lisa_websocket自动设置(基于path="/mqtt") */
ws_req.extra_header = "\r\nSec-WebSocket-Protocol: mqtt\r\n";
/* 创建 WebSocket 实例 */
pNetworkContext->ws_handle = lisa_ws_init(&ws_req);
if (pNetworkContext->ws_handle == NULL) {
LOGE("Failed to initialize WebSocket Secure");
return -1;
}
/* 连接到 WebSocket Secure 服务器 */
lisa_ws_err_e ret = lisa_ws_connect(pNetworkContext->ws_handle);
if (ret != LISA_WS_OK) {
LOGE("Failed to connect WebSocket Secure: %d", ret);
lisa_ws_cleanup(pNetworkContext->ws_handle);
return -1;
}
/* 等待连接建立 - 使用事件状态而非超时计数 */
int check_count = 0;
int max_checks = 100; /* 10秒超时 (100 * 100ms) */
while (check_count < max_checks) {
if (pNetworkContext->ws_connected) {
break;
}
vTaskDelay(pdMS_TO_TICKS(100));
check_count++;
if (check_count % 10 == 0) {
LOGI("Waiting for WebSocket Secure connection... (%d/%d)", check_count, max_checks);
}
}
if (!pNetworkContext->ws_connected) {
LOGE("WebSocket Secure connection timeout after %d checks", check_count);
lisa_ws_cleanup(pNetworkContext->ws_handle);
return -1;
}
LOGI("Connected to MQTT broker via WebSocket Secure");
return 0;
}
static void cleanup_wss(struct NetworkContext *pNetworkContext)
{
if (pNetworkContext->ws_handle) {
lisa_ws_disconnect(pNetworkContext->ws_handle);
lisa_ws_cleanup(pNetworkContext->ws_handle);
pNetworkContext->ws_handle = NULL;
}
}
int main(int argc, char **argv)
{
MQTTContext_t mqttContext;
MQTTConnectInfo_t connectInfo;
MQTTSubscribeInfo_t subscribeInfo;
MQTTPublishInfo_t publishInfo;
MQTTFixedBuffer_t networkBuffer;
TransportInterface_t transport;
struct NetworkContext networkContext = {0};
MQTTStatus_t mqttStatus;
bool sessionPresent;
LOGI("MQTT over WebSocket Secure Test Starting...");
/* 初始化 */
user_mac_manager_init();
net_dhcp_register_status_callback(dhcp_status_callback, NULL);
lisa_wifi_ops_t ops = { .custom_mac = custom_get_wifi_mac };
lisa_wifi_init(&ops);
user_fs_init();
lisa_kv_init();
user_wifi_manager_init();
if (wait_for_wifi_connection() != 0) {
LOGE("Failed to connect to WiFi");
return -1;
}
LOGI("\n========================================");
LOGI("MQTT over WebSocket Secure 功能测试");
LOGI("========================================\n");
/* 连接到 MQTT Broker (WebSocket Secure) */
if (connect_to_broker_wss(&networkContext) != 0) {
return -1;
}
/* 设置 Transport 接口 */
transport.recv = transport_recv;
transport.send = transport_send;
transport.pNetworkContext = &networkContext;
transport.writev = NULL;
/* 设置网络缓冲区 */
networkBuffer.pBuffer = g_network_buffer;
networkBuffer.size = NETWORK_BUFFER_SIZE;
/* 初始化 MQTT Context */
mqttStatus = MQTT_Init(&mqttContext, &transport, get_time_ms, mqtt_event_callback, &networkBuffer);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Init failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
/* 建立 MQTT 连接 */
memset(&connectInfo, 0, sizeof(connectInfo));
connectInfo.cleanSession = true;
connectInfo.pClientIdentifier = MQTT_CLIENT_ID;
connectInfo.clientIdentifierLength = strlen(MQTT_CLIENT_ID);
connectInfo.keepAliveSeconds = 60;
LOGI("=== 测试 1: MQTT CONNECT ===");
mqttStatus = MQTT_Connect(&mqttContext, &connectInfo, NULL, 5000, &sessionPresent);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Connect failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("MQTT Connected successfully");
vTaskDelay(pdMS_TO_TICKS(500));
/* 订阅主题 */
LOGI("\n=== 测试 2: MQTT SUBSCRIBE ===");
memset(&subscribeInfo, 0, sizeof(subscribeInfo));
subscribeInfo.qos = MQTTQoS0;
subscribeInfo.pTopicFilter = MQTT_TOPIC_SUB;
subscribeInfo.topicFilterLength = strlen(MQTT_TOPIC_SUB);
mqttStatus = MQTT_Subscribe(&mqttContext, &subscribeInfo, 1, MQTT_GetPacketId(&mqttContext));
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Subscribe failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("Subscribe request sent for topic: %s", MQTT_TOPIC_SUB);
/* 处理 SUBACK */
mqttStatus = MQTT_ProcessLoop(&mqttContext);
if (mqttStatus != MQTTSuccess && mqttStatus != MQTTNeedMoreBytes) {
LOGE("MQTT_ProcessLoop failed: %s", MQTT_Status_strerror(mqttStatus));
}
vTaskDelay(pdMS_TO_TICKS(500));
/* 发布消息 */
LOGI("\n=== 测试 3: MQTT PUBLISH ===");
memset(&publishInfo, 0, sizeof(publishInfo));
publishInfo.qos = MQTTQoS0;
publishInfo.pTopicName = MQTT_TOPIC_PUB;
publishInfo.topicNameLength = strlen(MQTT_TOPIC_PUB);
publishInfo.pPayload = "Hello from ARCS via WebSocket Secure!";
publishInfo.payloadLength = strlen("Hello from ARCS via WebSocket Secure!");
mqttStatus = MQTT_Publish(&mqttContext, &publishInfo, MQTT_GetPacketId(&mqttContext));
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Publish failed: %s", MQTT_Status_strerror(mqttStatus));
goto cleanup;
}
LOGI("Published message to topic: %s", MQTT_TOPIC_PUB);
vTaskDelay(pdMS_TO_TICKS(1000));
/* 处理接收的消息 */
LOGI("\n=== 测试 4: 接收消息 ===");
while (1) {
mqttStatus = MQTT_ProcessLoop(&mqttContext);
if (mqttStatus != MQTTSuccess && mqttStatus != MQTTNeedMoreBytes) {
LOGE("MQTT_ProcessLoop failed: %s", MQTT_Status_strerror(mqttStatus));
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
/* 断开连接 */
LOGI("\n=== 测试 5: MQTT DISCONNECT ===");
mqttStatus = MQTT_Disconnect(&mqttContext);
if (mqttStatus != MQTTSuccess) {
LOGE("MQTT_Disconnect failed: %s", MQTT_Status_strerror(mqttStatus));
} else {
LOGI("MQTT Disconnected successfully");
}
LOGI("\n========================================");
LOGI("所有 MQTT WebSocket Secure 测试完成");
LOGI("========================================\n");
cleanup:
cleanup_wss(&networkContext);
return 0;
}

View File

@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(listenai-cmake REQUIRED HINTS $ENV{ARCS_BASE})
project(arcs)
listenai_include_directories(
./
)
add_subdirectory(src)
listenai_add_executable(${PROJECT_NAME})
listenai_link_libraries(mbedtls)

View File

@@ -0,0 +1 @@
osource "$ARCS_BASE/Kconfig"

View File

@@ -0,0 +1,433 @@
# WebSocket 客户端示例
## 功能说明
此示例演示如何使用 LISA WebSocket 客户端库进行实时双向通信,包括:
- WebSocket 连接建立
- 文本消息发送与接收
- WebSocket 事件处理(连接/断开)
- 实时双向通信演示
示例通过 WiFi 连接到 WebSocket 服务器,建立 WebSocket 连接后循环发送测试消息并接收服务器响应,展示了 WebSocket 客户端的基本用法。
## 硬件连接
本示例使用芯片内部 WiFi 外设,无需额外接线。
**串口输出:**
- 串口 TX: PA3
- 波特率921600
## 测试环境准备
### 1. 安装 Python 依赖
本测试服务器需要安装 `websocket-server` 库。
**Python 版本要求:** Python 3.6 或更高版本
**安装依赖:**
```bash
cd samples/network/websocket
pip3 install -r requirements.txt
```
### 2. 启动 WebSocket 测试服务器
在与设备**同一局域网**的 Linux/Mac 主机上运行测试服务器:
```bash
cd samples/network/websocket
python3 websocket-server.py
```
服务器启动后将显示:
```
WebSocket 服务器启动在 ws://0.0.0.0:9001/
等待客户端连接...
```
### 3. 配置 WiFi 和服务器地址
**重要:** 本示例强制要求在编译前配置 WiFi 和服务器信息,否则会编译失败。
`src/main.c` 中找到配置区域(第 22-31 行),**取消注释**并填写实际配置:
**修改前(默认状态):**
```c
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
// #define WS_TEST_SCHEME // WebSocket 协议,通常为 "ws" 或 "wss"
// #define WS_TEST_HOST // WebSocket 服务器 IP例如"192.168.1.100"
// #define WS_TEST_PORT // WebSocket 服务器端口,例如:"9001"
// #define WS_TEST_PATH // WebSocket 路径,例如:"/"
// #define TARGET_WIFI_SSID // 修改为目标 WiFi 的 SSID
// #define TARGET_WIFI_PWD // 修改为目标 WiFi 的密码
```
**修改后(配置示例):**
```c
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
#define WS_TEST_SCHEME "ws" // WebSocket 协议
#define WS_TEST_HOST "192.168.1.100" // WebSocket 服务器 IP
#define WS_TEST_PORT "9001" // WebSocket 服务器端口
#define WS_TEST_PATH "/" // WebSocket 路径
#define TARGET_WIFI_SSID "MyHomeWiFi" // 你的 WiFi 名称
#define TARGET_WIFI_PWD "mypassword123" // 你的 WiFi 密码
```
**操作步骤:**
1. 移除 `WS_TEST_SCHEME` 前的 `//` 注释符,填写为 "ws" 或 "wss"
2. 移除 `WS_TEST_HOST` 前的 `//` 注释符,填写实际服务器 IP
3. 移除 `WS_TEST_PORT` 前的 `//` 注释符,填写实际端口(通常为 "9001"
4. 移除 `WS_TEST_PATH` 前的 `//` 注释符,填写 WebSocket 路径(通常为 "/"
5. 移除 `TARGET_WIFI_SSID` 前的 `//` 注释符,填写实际 WiFi 名称
6. 移除 `TARGET_WIFI_PWD` 前的 `//` 注释符,填写实际 WiFi 密码
**获取服务器 IP 地址:**
在运行 `websocket-server.py` 的主机上执行:
```bash
# Linux/Mac
ifconfig | grep "inet "
# 或者
ip addr show | grep "inet "
```
查找类似 `192.168.x.x` 的局域网地址。
## 编译运行
### 编译
```{eval-rst}
.. include:: /sample_build.rst
```
### 烧录
```{eval-rst}
.. include:: /sample_flash.rst
```
## 预期输出
系统启动后,终端将输出以下内容:
### 1. WiFi 连接日志
```
[INFO] WebSocket Test Starting...
[INFO] WiFi auto-connect started
[INFO] custom_get_wifi_mac: 0, XX:XX:XX:XX:XX:XX
[INFO] WiFi connection status: 1
[INFO] WiFi connected to AP
[INFO] WiFi event: module=1, id=4
[INFO] Got IP address
[INFO] WiFi connected and IP obtained, starting WebSocket test...
```
### 2. WebSocket 连接和数据交互
```
========================================
WebSocket 功能测试
========================================
[INFO] Connecting to WebSocket server: ws://192.168.1.100:9001/
[INFO] lisa ws connect
WebSocket 连接成功,开始发送测试数据...
[INFO] 发送消息 1/10: lisa_test
[INFO] ws recv data: Echo: lisa_test
[INFO] 发送消息 2/10: lisa_test
[INFO] ws recv data: Echo: lisa_test
[INFO] 发送消息 3/10: lisa_test
[INFO] ws recv data: Echo: lisa_test
...
[INFO] 发送消息 10/10: lisa_test
[INFO] ws recv data: Echo: lisa_test
========================================
WebSocket 测试完成
发送消息数: 10
========================================
```
### 3. 服务器端日志
服务器端将显示接收到的消息:
```
WebSocket 服务器启动在 ws://0.0.0.0:9001/
等待客户端连接...
客户端已连接
接收到消息: lisa_test
回显消息: Echo: lisa_test
接收到消息: lisa_test
回显消息: Echo: lisa_test
...
```
## 核心 API
| API | 说明 |
|-----|------|
| `lisa_ws_init()` | 初始化 WebSocket 客户端 |
| `lisa_ws_connect()` | 连接到 WebSocket 服务器 |
| `lisa_ws_send_text()` | 发送文本消息 |
| `lisa_ws_disconnect()` | 断开 WebSocket 连接 |
| `lisa_ws_cleanup()` | 清理并释放 WebSocket 客户端资源 |
| `wifi_mgr_init()` | 初始化 WiFi 管理器 |
| `wifi_mgr_auto_connect_start()` | 启动自动连接 |
| `ls_event_register_cb()` | 注册 WiFi 事件回调 |
## 关键代码
### 1. WebSocket 连接初始化
使用 LISA WebSocket 库建立连接:
```c
// 配置 WebSocket 请求参数
lisa_ws_request_t lisa_ws_req;
lisa_ws_req.scheme = WS_TEST_SCHEME; // "ws" 或 "wss"
lisa_ws_req.host = WS_TEST_HOST; // 服务器 IP
lisa_ws_req.port = WS_TEST_PORT; // 服务器端口
lisa_ws_req.path = WS_TEST_PATH; // 路径
lisa_ws_req.timeout = WS_SEND_RETRY_DELAY;
lisa_ws_req.on_data = get_ws_data_cb;
lisa_ws_req.on_event = get_ws_event_cb;
lisa_ws_req.user = NULL;
lisa_ws_req.extra_header = NULL;
// 初始化 WebSocket 客户端
lisa_ws_t* lisa_ws_ins = lisa_ws_init(&lisa_ws_req);
if (lisa_ws_ins == NULL) {
LOGE("lisa ws init failed");
return -1;
}
// 连接到服务器
if (lisa_ws_connect(lisa_ws_ins) != 0) {
LOGE("lisa evs websocket connect error");
goto ERR;
}
```
### 2. WebSocket 事件回调
处理 WebSocket 连接状态变化:
```c
static void get_ws_event_cb(lisa_ws_event_t *event)
{
g_event = event->what;
if (event->what == LISA_WS_ON_CONNECTED) {
LOGI("lisa ws connect");
} else if (event->what == LISA_WS_ON_DISCONNECTED) {
LOGI("lisa ws disconnect");
} else {
LOGI("event %d", event->what);
}
}
```
### 3. WebSocket 数据接收回调
处理接收到的 WebSocket 消息:
```c
static void get_ws_data_cb(lisa_ws_data_t *data)
{
LOGI("ws recv data: %s", data->buf);
}
```
### 4. WebSocket 消息发送
发送文本消息到服务器:
```c
// 等待 WebSocket 连接成功
int check_conn_count = 0;
while (g_event == LISA_WS_ON_DISCONNECTED) {
lisa_thread_mdelay(WS_SEND_RETRY_SLEEP_TIME);
check_conn_count++;
if (check_conn_count >= (WS_SEND_RETRY_DELAY / WS_SEND_RETRY_SLEEP_TIME)) {
LOGE("lisa ws connect timeout");
goto ERR;
}
}
// 连接成功后发送消息
int ws_test_count = 0;
while (1) {
LOGI("发送消息 %d/%d: %s", ws_test_count + 1, WS_TEST_MAX_COUNT, WS_TEST_CONTENT);
lisa_ws_send_text(lisa_ws_ins, WS_TEST_CONTENT);
lisa_thread_mdelay(1000);
ws_test_count++;
if (ws_test_count >= WS_TEST_MAX_COUNT) {
break;
}
}
```
### 5. WiFi 连接和 IP 获取
等待 WiFi 连接成功并获取 IP 地址:
```c
// 注册 ls_event 回调监听 GOT_IP 事件
ls_event_init();
ls_event_register_cb(EVENT_WIFI, EVENT_WIFI_GOT_IP, app_wifi_event_cb, NULL);
// WiFi 事件回调
static int app_wifi_event_cb(void *arg, event_module_t event_module,
int event_id, void *event_data)
{
switch (event_id) {
case EVENT_WIFI_GOT_IP:
LOGI("Got IP address");
g_get_ip_success = true;
break;
}
return 0;
}
// 等待 WiFi 连接和 IP 获取
static int wait_for_wifi_connection(void)
{
while (timeout > 0) {
if (g_wifi_connected && g_get_ip_success) {
LOGI("WiFi connected and IP obtained, starting WebSocket test...");
return 0;
}
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
}
return -1;
}
```
### 6. 资源清理
正确清理 WebSocket 资源:
```c
ERR:
if (lisa_ws_ins != NULL) {
lisa_ws_disconnect(lisa_ws_ins);
lisa_ws_cleanup(lisa_ws_ins);
lisa_ws_ins = NULL;
}
return ret;
```
## 配置说明
### WebSocket 请求参数
`lisa_ws_request_t` 结构体包含以下主要字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `scheme` | `const char *` | WebSocket 协议("ws" 或 "wss" |
| `host` | `const char *` | 服务器主机地址 |
| `port` | `const char *` | 服务器端口号 |
| `path` | `const char *` | WebSocket 路径 |
| `timeout` | `int` | 连接超时时间(毫秒) |
| `on_data` | `lisa_ws_data_cb_t` | 数据接收回调函数 |
| `on_event` | `lisa_ws_event_cb_t` | 事件通知回调函数 |
| `extra_header` | `const char *` | 额外的 HTTP 头部(可选) |
### WebSocket 事件类型
| 事件 | 说明 |
|------|------|
| `LISA_WS_ON_CONNECTED` | WebSocket 连接已建立 |
| `LISA_WS_ON_DISCONNECTED` | WebSocket 连接已断开 |
### prj.conf 关键配置
```kconfig
# LWIP 网络栈
CONFIG_LWIP=y
CONFIG_LWIP_OPTION_LWIP_DNS=y
CONFIG_LWIP_OPTION_SO_REUSE=y
# WiFi 管理器
CONFIG_WIFI_MANAGER=y
# WebSocket 客户端
CONFIG_MODULE_NOPOLL=y
CONFIG_LISA_WEBSOCKET=y
# LISA 组件
CONFIG_LISA_OS=y
CONFIG_LISA_KV=y
CONFIG_LISA_NETWORK=y
# 堆内存配置
CONFIG_HEAP_SIZE=0x3C00 # 主堆: 15KB
CONFIG_PSRAM_HEAP_SIZE=0x5b0000 # PSRAM 堆: 5.6MB
```
## WebSocket 协议说明
### ws vs wss
- **ws://** - 未加密的 WebSocket 连接(类似 HTTP
- **wss://** - 加密的 WebSocket 连接(类似 HTTPS需要 TLS/SSL
本示例默认使用 `ws://` 协议。如需使用 `wss://`,需要:
1. 修改 `WS_TEST_SCHEME` 为 "wss"
2. 确保服务器支持 TLS/SSL
3. 可能需要配置证书(根据服务器要求)
## 注意事项
1. **编译前配置(重要)**:首次编译前**必须**取消注释并修改 `src/main.c` 中的 WiFi 和服务器配置,否则会触发编译错误:
```
main.c:35:2: error: #error "请修改 WS_TEST_SCHEME 为 WebSocket 协议例如ws 或 wss"
main.c:39:2: error: #error "请修改 WS_TEST_HOST 为实际的 WebSocket 服务器 IP例如192.168.1.100"
main.c:43:2: error: #error "请修改 WS_TEST_PORT 为实际的 WebSocket 服务器端口例如9001"
main.c:47:2: error: #error "请修改 WS_TEST_PATH 为 WebSocket 路径(例如:/"
main.c:51:2: error: #error "请修改 TARGET_WIFI_SSID 为实际的 WiFi SSID"
main.c:55:2: error: #error "请修改 TARGET_WIFI_PWD 为实际的 WiFi 密码"
```
这是为了防止使用默认配置导致连接失败。配置方法请参考上方的"配置 WiFi 和服务器地址"章节。
2. **网络环境**:确保设备与 WebSocket 服务器在同一局域网内,避免跨网段访问
3. **端口配置**WebSocket 服务器默认使用 9001 端口,需填写为字符串格式:"9001"
4. **路径配置**WebSocket 路径通常为 "/",根据实际服务器配置填写
5. **WiFi 配置**:将 `TARGET_WIFI_SSID` 和 `TARGET_WIFI_PWD` 修改为实际的 WiFi SSID 和密码
6. **连接超时**WebSocket 连接超时设置为 10 秒,如果网络较慢可适当增加 `timeout` 值
7. **事件处理**`on_event` 和 `on_data` 回调函数在 WebSocket 事件发生时被调用,需要正确处理
8. **资源清理**:使用完毕后必须调用 `lisa_ws_disconnect()` 和 `lisa_ws_cleanup()` 释放资源
9. **Python 版本**:测试服务器需要 Python 3.6 或更高版本,并安装 `websocket-server` 库
10. **消息格式**当前示例仅发送文本消息LISA WebSocket 也支持二进制数据传输
## 相关文档
- [LISA WiFi 组件文档](../../../components/lisa_wifi/README.md) - WiFi 初始化和连接
- [WiFi Manager 文档](../../../modules/wifi_manager/README.md) - WiFi 连接管理
- [LISA WebSocket 组件文档](../../../components/lisa_websocket/README.md) - WebSocket 客户端详细说明

View File

@@ -0,0 +1,177 @@
#!/bin/bash
set -e
usage() {
echo "使用方式: $0 [选项]"
echo "选项:"
echo " -S, --Source <path> 指定项目源码路径 (默认为当前脚本所在目录)"
echo " -t, --target <target> 指定构建目标 (如 menuconfig)"
echo " -C, --Clean 清理构建目录"
echo " -B, --build 构建输出目录"
echo " -h, --help 显示此帮助信息"
echo " -r, --release 以 Release 模式构建 (移除 DEBUG_PATH 信息)"
echo " -w, --warnings-as-errors 将警告视为错误"
echo " -D<var>=<value> 传递 CMake 变量 (可多次使用)"
echo ""
echo "示例:"
echo " $0 -S samples/helloworld -DBOARD=arcs_mini 指定板型构建"
echo " $0 -S samples/helloworld -DBOARD=arcs_evb 使用 EVB 板型"
echo " $0 -S samples/helloworld -t menuconfig -DBOARD=arcs_mini 运行 menuconfig"
echo " $0 -C -S samples/helloworld -DBOARD=arcs_mini 清理并重新构建"
echo " $0 -S samples/helloworld -DBOARD=my_board -DBOARD_SEARCH_PATH=/path/to/boards 使用自定义板型"
exit 1
}
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_PATH="$SCRIPT_DIR"
TARGET=""
CLEAN=false
OUTPUT="build"
WARNINGS_AS_ERRORS=false
RELEASE=false
ARCS_BASE_DIR_NAME="arcs-sdk"
ARCS_DEV_TOOLS_DIR_NAME="listenai-dev-tools"
ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME="gcc"
ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME="listenai-tools"
find_arcs_base() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_BASE_DIR_NAME"
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found ARCS_BASE: $current_dir/$dir_name"
export ARCS_BASE="$current_dir/$dir_name"
return 0
fi
current_dir=$(dirname "$current_dir")
done
echo "ARCS_BASE not found, Please add ARCS_BASE environment variable or set ARCS_BASE_DIR_NAME to the correct directory."
echo "Current Target ARCS_BASE directory name: $ARCS_BASE_DIR_NAME."
exit 1
}
find_dev_tools() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_DEV_TOOLS_DIR_NAME"
echo "trying to find $dir_name in parent directories..."
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found $dir_name: $current_dir/$dir_name"
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME" ]; then
echo "Found LISTENAI_TOOLS_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
export LISTENAI_TOOLS_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
fi
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME" ]; then
echo "Found NUCLEI_TOOLCHAIN_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
export NUCLEI_TOOLCHAIN_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
fi
return 0
fi
current_dir=$(dirname "$current_dir")
done
}
while [[ $# -gt 0 ]]; do
case $1 in
-S|--Source)
PROJECT_PATH="$2"
shift 2
;;
-B|--build)
OUTPUT="$2"
shift 2
;;
-t|--target)
TARGET="$2"
shift 2
;;
-C|--Clean)
CLEAN=true
shift 1
;;
-w|--warnings-as-errors)
WARNINGS_AS_ERRORS=true
shift 1
;;
-h|--help)
usage
;;
-r|--release)
RELEASE=true
shift 1
;;
-D*)
CMAKE_VARS+=("$1")
shift 1
;;
*)
echo "未知参数: $1"
usage
;;
esac
done
echo "Source: $PROJECT_PATH"
echo "Target: $TARGET"
echo "Clean : $CLEAN"
if [ -z "$LISTENAI_TOOLS_PATH" ] || [ -z "$NUCLEI_TOOLCHAIN_PATH" ]; then
find_dev_tools
fi
if [ -z "${LISTENAI_TOOLS_PATH}" ]; then
export LISTENAI_TOOLS_PATH="请添加 LISTENAI_TOOLS_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 LISTENAI_TOOLS_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
if [ -z "${NUCLEI_TOOLCHAIN_PATH}" ]; then
export NUCLEI_TOOLCHAIN_PATH="请添加 NUCLEI_TOOLCHAIN_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 NUCLEI_TOOLCHAIN_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
############### 下面代码不用修改 ##################
# 构建工具的位置
CMAKE_PROGRAM="$LISTENAI_TOOLS_PATH/cmake/bin/cmake"
NINJA_PROGRAM="$LISTENAI_TOOLS_PATH/ninja/ninja"
# 配置环境变量 ARCS_BASE
if [ -z "$ARCS_BASE" ]; then
find_arcs_base
fi
if [ "$CLEAN" = true ]; then
rm -rf $OUTPUT
fi
# Initialize CMAKE_VARS array if it doesn't exist
declare -a CMAKE_VARS
# Add warnings-as-errors flag if enabled
if [ "$WARNINGS_AS_ERRORS" = true ]; then
CMAKE_VARS+=("-DCMAKE_C_FLAGS=-Werror")
CMAKE_VARS+=("-DCMAKE_CXX_FLAGS=-Werror")
echo "Treating warnings as errors"
fi
# Add release flags if enabled
if [ "$RELEASE" = true ]; then
CMAKE_VARS+=("-DENABLE_DEBUG_PATH=OFF")
echo "Release mode enabled (-DENABLE_DEBUG_PATH=OFF)"
fi
$CMAKE_PROGRAM -B "$OUTPUT" -G Ninja -S "$PROJECT_PATH" \
-DCMAKE_MAKE_PROGRAM="$NINJA_PROGRAM" \
"${CMAKE_VARS[@]}"
if [ -z "$TARGET" ]; then
$CMAKE_PROGRAM --build "$OUTPUT" -j4
else
$CMAKE_PROGRAM --build "$OUTPUT" --target "$TARGET"
fi

View File

@@ -0,0 +1,148 @@
# 项目可根据需要在此文件中设置对应的配置
CONFIG_ARCS_HAL_WARNING_DISABLE=y
CONFIG_SDK_MODULE_EASYLOGGER=n
# CONFIG_ARCS_HAL_DEBUG=y
CONFIG_MODULE_FREERTOS=y
CONFIG_MODULE_HEAP=y
CONFIG_LINK_OPTION_NOSYS_SPECS=y
CONFIG_EASYLOGGER_ASYNC_BUF_SIZE=0x1000
# DISK驱动使能
CONFIG_DISK_DRIVER=y
#using mbedtls for hardware decoding(IF CONFIG_WX_SQLITE3_HARD_CODEC)
CONFIG_SDK_MODULE_MBEDTLS=y
CONFIG_CUSTOM_MBEDTLS_CFG_FILE=y
CONFIG_MBEDTLS_CFG_FILE="config-tls-generic.h"
CONFIG_MBEDTLS_HARDWARE_ENTROPY=y
# CONFIG_MBEDTLS_HARDWARE_AES=y
# CONFIG_MBEDTLS_HARDWARE_SHA1=y
# CONFIG_MBEDTLS_HARDWARE_SHA256=y
# CONFIG_MBEDTLS_HARDWARE_SHA512=y
# CONFIG_MBEDTLS_HARDWARE_AES_CCM=y
# CONFIG_MBEDTLS_HARDWARE_AES_GCM=y
# CONFIG_MBEDTLS_HARDWARE_ECC=y
# System support
CONFIG_MBEDTLS_HAVE_ASM=y
CONFIG_MBEDTLS_HAVE_TIME_DATE=y
CONFIG_MBEDTLS_SSL_ALPN=y
# Save RAM at the expense of ROM
CONFIG_MBEDTLS_AES_ROM_TABLES=y
# mbed TLS feature support
CONFIG_MBEDTLS_CIPHER_MODE_CBC_ENABLED=y
CONFIG_MBEDTLS_KEY_EXCHANGE_RSA_ENABLED=y
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED=y
# Supported TLS versions
CONFIG_MBEDTLS_TLS_VERSION_1_0=y
CONFIG_MBEDTLS_TLS_VERSION_1_1=y
CONFIG_MBEDTLS_TLS_VERSION_1_2=y
# mbed TLS modules
CONFIG_MBEDTLS_CIPHER_AES_ENABLED=y
CONFIG_MBEDTLS_CIPHER=y
CONFIG_MBEDTLS_CTR_DRBG_ENABLED=y
CONFIG_MBEDTLS_ENTROPY_ENABLED=y
CONFIG_MBEDTLS_MD=y
CONFIG_MBEDTLS_MAC_MD5_ENABLED=y
CONFIG_MBEDTLS_HASH_MD5_ENABLED=y
CONFIG_MBEDTLS_MAC_SHA1_ENABLED=y
CONFIG_MBEDTLS_HASH_SHA1_ENABLED=y
CONFIG_MBEDTLS_MAC_SHA256_ENABLED=y
CONFIG_MBEDTLS_SHA256_SMALLER=y
CONFIG_MBEDTLS_HASH_SHA256_ENABLED=y
CONFIG_MBEDTLS_DTLS=y
CONFIG_MBEDTLS_SERVER_NAME_INDICATION=y
CONFIG_MBEDTLS_ECDH_C=y
CONFIG_MBEDTLS_ECP_C=y
CONFIG_MBEDTLS_CIPHER_GCM_ENABLED=y
CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y
CONFIG_MBEDTLS_MAC_SHA512_ENABLED=y
CONFIG_MBEDTLS_HASH_SHA512_ENABLED=y
CONFIG_MBEDTLS_PEM_CERTIFICATE_FORMAT=y
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384
CONFIG_MBEDTLS_CTR_DRBG_ENABLED=y
# CONFIG_MBEDTLS_DEBUG=y
CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=n
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y
# CONFIG_ARCS_HAL_DEBUG=y
CONFIG_MODULE_HEAP=y
CONFIG_LOG=y
CONFIG_SYSLOG_PRINTK_REDIRECT=y
CONFIG_SYSLOG_UART_TX_PIN=3
CONFIG_SYSLOG_UART_TX_PIN_FUNC_SEL=2
CONFIG_SYSLOG_UART_PORT=0
CONFIG_SYSLOG_UART_BAUDRATE=921600
CONFIG_LWIP=y
CONFIG_WIFI=y
CONFIG_WIFI_TX_BUF_COPY=y
CONFIG_WIFI_LWIP_SAME_CORE=y
CONFIG_LWIP_TX_BUF_COPY=y
CONFIG_LWIP_OPTION_LWIP_DNS=y
CONFIG_LWIP_OPTION_SO_REUSE=y
CONFIG_FREERTOS_STATIC_ALLOCATION_ENABLE=y
CONFIG_DCACHE_ENABLE=y
CONFIG_MODULE_ZE_TLS=y
CONFIG_MODULE_HTTPCLIENT=y
CONFIG_MODULE_NOPOLL=y
CONFIG_MODULE_CORESNTP=y
CONFIG_LWIP_OPTION_SO_REUSE=y
CONFIG_LWIP_OPTION_LWIP_DNS=y
CONFIG_HEAP_SIZE=0x3C00
CONFIG_PSRAM_HEAP_SIZE=0x5b0000
# 文件系统配置
CONFIG_FILE_SYSTEM=y
CONFIG_FATFS_FILESYSTEM=y
# 抽象文件系统
CONFIG_LSFS=y
CONFIG_LSFS_FAT=y
CONFIG_LSFS_REGISTER_VFS=y
CONFIG_LVFS_POSIX_API_ALIAS=y
CONFIG_LVFS=y
CONFIG_LVFS_POSIX_API=y
CONFIG_FS_ENV_MEMORY_MANAGEMENT_USE_PSRAM=y
# LISA WIFI
CONFIG_LISA_WIFI=y
# MAC Manager
CONFIG_MAC_MANAGER=y
CONFIG_MAC_MANAGER_MAC_BY_CHIP_ID=y
#WIFI MANAGER
CONFIG_WIFI_MANAGER=y
# LISA Shell
CONFIG_LISA_SHELL=y
# LISA Porting
CONFIG_LISA_PORTING=y
# SD Card drivers
CONFIG_DISK_DRIVER_SDMMC=y
CONFIG_LISA_SDMMC_DEVICE=y
CONFIG_CJSON=y
CONFIG_LISA_OS=y
CONFIG_LISA_KV=y
CONFIG_LISA_NETWORK=y
CONFIG_SDK_MODULE_ICO=y
CONFIG_LISA_WEBSOCKET=y

View File

@@ -0,0 +1,2 @@
# WebSocket 测试服务器依赖
websocket-server>=0.6.4

View File

@@ -0,0 +1,3 @@
tests:
samples.websocket:
build_only: true

View File

@@ -0,0 +1,10 @@
listenai_library_named(app)
listenai_library_sources(
main.c
fs/user_fs.c
)
listenai_include_directories(
./
./fs
)

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lsfs.h"
#include "disk/disk_access.h"
#include "lisa_sdmmc.h"
#include "log_print.h"
#if CONFIG_LVFS
#include "lvfs.h"
#endif
// #define FLASHDISK_DEVICE "NAND:"
#define DISK_DEVICE "SD:"
#define DISK_MOUNT_POINT "/" DISK_DEVICE
static struct lsfs_mount_t flash_lsfs_mnt = {
.type = LSFS_FATFS,
.mnt_point = DISK_MOUNT_POINT,
.fs_data = NULL,
};
static int fs_mount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_mount(mp);
if (ret != 0)
{
CLOG("No file system, try reformatting, mount ret: %d", ret);
/* 格式化底层设备,去掉挂载点前面的'/' 作为设备名,例如 "/SD:" -> "SD:" */
ret = lsfs_mkfs(mp->type, &mp->mnt_point[1], NULL, 0);
if (ret == 0)
{
CLOG("mkfs success, retry mount");
ret = lsfs_mount(mp);
}
else
{
CLOG("mkfs failed, ret: %d", ret);
}
}
if (ret != 0)
{
CLOG("Failed to mount filesystem: %d", ret);
return ret;
}
CLOG("%s mounted successfully", mp->mnt_point);
return 0;
}
static int fs_unmount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_unmount(mp);
if (ret != 0)
{
CLOG("%s unmount failed!\n", mp->mnt_point);
}
CLOG("%s unmount success!\n", mp->mnt_point);
}
static int list_dir(const char *path)
{
struct lsfs_dir_t dir;
struct lsfs_dirent entry;
int ret;
int nfile = 0, ndir = 0;
/* 初始化目录对象 */
lsfs_dir_t_init(&dir);
ret = lsfs_opendir(&dir, path);
CLOG("%s opendir: %d", path, ret);
if (ret == 0)
{
while (1)
{
ret = lsfs_readdir(&dir, &entry);
if (ret != 0 || entry.name[0] == 0)
break; /* Error or end of dir */
if (entry.type == LSFS_DIR_ENTRY_DIR)
{
/* Directory */
CLOG(" <DIR> %s", entry.name);
ndir++;
}
else
{
/* File */
CLOG("%10lu %s", entry.size, entry.name);
nfile++;
}
}
lsfs_closedir(&dir);
CLOG("%d dirs, %d files.", ndir, nfile);
}
else
{
CLOG("Failed to open \"%s\". (%u)", path, ret);
}
return ret;
}
static void user_fs_change_to_root_folder(void)
{
int ret;
ret = lvfs_chdrive(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change drive to %s, error: %d\n", DISK_MOUNT_POINT, ret);
}
ret = lvfs_chdir(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change directory to /SD:/, error: %d\n", ret);
} else {
CLOG("Changed to directory /SD:/\n");
}
}
int user_fs_init(void){
lisa_sdmmc_probe(lisa_device_get("sdmmc0"));
disk_init(NULL);
#if CONFIG_LVFS
lvfs_init();
#endif
lsfs_init();
if(0 != fs_mount(&flash_lsfs_mnt)){
return -1;
}
user_fs_change_to_root_folder();
return 0;
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
int user_fs_init(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,321 @@
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "stdbool.h"
#include "assert.h"
#include "FreeRTOS.h"
#include "task.h"
#define TAG "websocket-test"
#include "lisa_log.h"
#include "lisa_kv.h"
#include "mac_manager.h"
#include "mac_manager_ops.h"
#include "wifi_manager/wifi_manager.h"
#include "user_fs.h"
#include "lisa_wifi.h"
#include "ls_wifi_type.h"
#include "net_al.h"
#include "net_def.h"
#include "lisa_thread.h"
#include "lisa_websocket.h"
// ============================================================
// 重要:使用前请修改以下配置!
// ============================================================
#define WS_TEST_SCHEME "ws" // WebSocket 协议,通常为 "ws" 或 "wss"
#define WS_TEST_HOST "your_server_ip" // WebSocket 服务器 IP例如"192.168.1.100"
#define WS_TEST_PORT "9001" // WebSocket 服务器端口,例如:"9001"
#define WS_TEST_PATH "/" // WebSocket 路径,例如:"/"
#define TARGET_WIFI_SSID "your_wifi_ssid" // 修改为目标 WiFi 的 SSID
#define TARGET_WIFI_PWD "your_wifi_pwd" // 修改为目标 WiFi 的密码
// 编译时检查:确保用户已修改默认配置
#ifndef WS_TEST_SCHEME
#error "请修改 WS_TEST_SCHEME 为 WebSocket 协议例如ws 或 wss"
#endif
#ifndef WS_TEST_HOST
#error "请修改 WS_TEST_HOST 为实际的 WebSocket 服务器 IP例如192.168.1.100"
#endif
#ifndef WS_TEST_PORT
#error "请修改 WS_TEST_PORT 为实际的 WebSocket 服务器端口例如9001"
#endif
#ifndef WS_TEST_PATH
#error "请修改 WS_TEST_PATH 为 WebSocket 路径(例如:/"
#endif
#ifndef TARGET_WIFI_SSID
#error "请修改 TARGET_WIFI_SSID 为实际的 WiFi SSID"
#endif
#ifndef TARGET_WIFI_PWD
#error "请修改 TARGET_WIFI_PWD 为实际的 WiFi 密码"
#endif
#define WIFI_MGR_AUTO_CONNECT_INTERVAL_MS 2000
#define WS_SEND_RETRY_DELAY (10000)
#define WS_SEND_RETRY_SLEEP_TIME (100)
#define WS_TEST_CONTENT "lisa_test"
#define WS_TEST_MAX_COUNT (10)
#define WIFI_MGR_SEARCH_AP_BUFFER_SIZE 10
static lisa_ws_event_e g_event = LISA_WS_ON_DISCONNECTED;
static mac_manager_t *m_mac_manager;
static wifi_mgr_sta_config_t list[WIFI_MGR_SEARCH_AP_BUFFER_SIZE] = { 0 };
static int8_t custom_get_wifi_mac(uint8_t mac_addr[6])
{
int8_t ret = 0;
if (!mac_addr) {
return -1;
}
ret = mac_manager_get(m_mac_manager, mac_addr, 6);
LOGI("custom_get_wifi_mac: %d, %02X:%02X:%02X:%02X:%02X:%02X", ret, mac_addr[0], mac_addr[1], mac_addr[2],
mac_addr[3], mac_addr[4], mac_addr[5]);
return ret;
}
static volatile bool g_wifi_connected = false;
static volatile bool g_get_ip_success = false;
static void dhcp_status_callback(int vif_idx, bool success, uint32_t ip_addr, uint32_t netmask, uint32_t gateway, void *arg)
{
if (success) {
LOGI("DHCP Success on VIF-%d: IP=%d.%d.%d.%d, Mask=%d.%d.%d.%d, GW=%d.%d.%d.%d",
vif_idx,
ip_addr & 0xff, (ip_addr >> 8) & 0xff, (ip_addr >> 16) & 0xff, (ip_addr >> 24) & 0xff,
netmask & 0xff, (netmask >> 8) & 0xff, (netmask >> 16) & 0xff, (netmask >> 24) & 0xff,
gateway & 0xff, (gateway >> 8) & 0xff, (gateway >> 16) & 0xff, (gateway >> 24) & 0xff);
g_get_ip_success = true;
} else {
LOGI("DHCP Failed on VIF-%d", vif_idx);
}
}
static void wifi_mgr_connection_status_cb(wifi_mgr_connection_info_t *connection_info, void *arg)
{
net_if_t *net_if;
LOGI("WiFi connection status: %d", connection_info->status);
switch (connection_info->status) {
case WIFI_MGR_STA_CONNECTED:
LOGI("WiFi connected to AP");
g_wifi_connected = true;
net_if = net_if_get(WIFI_VIF_STA_IDX);
net_if_up(net_if);
if (!net_if->static_ip) {
ls_dhcpc_start(WIFI_VIF_STA_IDX);
}
break;
case WIFI_MGR_STA_DISCONNECTED:
LOGI("WiFi disconnected from AP");
g_wifi_connected = false;
ls_dhcpc_stop(WIFI_VIF_STA_IDX);
net_if_down(net_if_get(WIFI_VIF_STA_IDX));
break;
default:
break;
}
}
static void user_mac_manager_init(void)
{
mac_manager_config_t config = {
.random_mac_if_mac_invalid = false,
};
m_mac_manager = mac_manager_init(mac_manager_ops_get()->mem_ops, mac_manager_ops_get()->content_ops, &config);
if (m_mac_manager == NULL) {
LOGI("mac_manager_init failed\n");
assert(0);
}
}
static void user_wifi_manager_init(void)
{
wifi_mgr_sta_config_t cfg = {
.ssid = TARGET_WIFI_SSID,
.pwd = TARGET_WIFI_PWD,
};
wifi_mgr_autoconn_config_t autoconn_cfg = {
.interval_ms = WIFI_MGR_AUTO_CONNECT_INTERVAL_MS,
};
wifi_mgr_init(wifi_mgr_ops_get());
wifi_mgr_sta_enable();
// 注册 WiFi 连接状态回调
wifi_mgr_sta_add_connection_cb(wifi_mgr_connection_status_cb, NULL);
/* 搜索并删除所有已保存的 AP */
int count = wifi_mgr_storage_search_ap(list, WIFI_MGR_SEARCH_AP_BUFFER_SIZE, SEARCH_ALL, NULL);
for (int i = 0; i < count; i++) {
LOGI("Deleting saved AP: ssid=%s", list[i].ssid);
wifi_mgr_storage_delete_ap(&list[i]);
}
// 保存 AP 配置并启动自动连接
wifi_mgr_storage_save_ap(&cfg);
wifi_mgr_auto_connect_start(&autoconn_cfg);
LOGI("WiFi auto-connect started");
}
static int wait_for_wifi_connection(void)
{
int timeout = 30; // 30 秒超时
LOGI("Waiting for WiFi connection and IP address...");
// 等待 WiFi 连接和 IP 获取
while (timeout > 0) {
if (g_wifi_connected && g_get_ip_success) {
LOGI("WiFi connected and IP obtained, starting WebSocket test...");
vTaskDelay(pdMS_TO_TICKS(500)); // 短暂延迟确保网络稳定
return 0;
}
vTaskDelay(pdMS_TO_TICKS(1000));
timeout--;
if (timeout % 5 == 0) {
LOGI("Waiting... (WiFi:%d, IP:%d, timeout:%d)", g_wifi_connected, g_get_ip_success, timeout);
}
}
LOGI("WiFi connection or IP acquisition timeout (WiFi:%d, IP:%d)", g_wifi_connected, g_get_ip_success);
return -1;
}
static void get_ws_data_cb(lisa_ws_data_t *data)
{
LOGI("ws recv data: %s", data->buf);
}
static void get_ws_event_cb(lisa_ws_event_t *event)
{
g_event = event->what;
if (event->what == LISA_WS_ON_CONNECTED) {
LOGI("lisa ws connect");
} else if (event->what == LISA_WS_ON_DISCONNECTED) {
LOGI("lisa ws disconnect");
} else {
LOGI("event %d", event->what);
}
}
int main(int argc, char **argv)
{
int ret;
LOGI("WebSocket Test Starting...");
// 初始化 MAC 管理器
user_mac_manager_init();
// 注册 DHCP 状态回调
net_dhcp_register_status_callback(dhcp_status_callback, NULL);
// 初始化 LISA WiFi
lisa_wifi_ops_t ops = {
.custom_mac = custom_get_wifi_mac,
};
lisa_wifi_init(&ops);
// 初始化文件系统和 KV 存储
user_fs_init();
lisa_kv_init();
// 初始化 WiFi 管理器并启动自动连接
user_wifi_manager_init();
// 等待 WiFi 连接成功
ret = wait_for_wifi_connection();
if (ret != 0) {
LOGI("Failed to connect to WiFi");
return -1;
}
LOGI("\n========================================");
LOGI("WebSocket 功能测试");
LOGI("========================================\n");
// 初始化 WebSocket 请求
g_event = LISA_WS_ON_DISCONNECTED;
lisa_ws_request_t lisa_ws_req;
lisa_ws_req.scheme = WS_TEST_SCHEME;
lisa_ws_req.host = WS_TEST_HOST;
lisa_ws_req.port = WS_TEST_PORT;
lisa_ws_req.path = WS_TEST_PATH;
lisa_ws_req.timeout = WS_SEND_RETRY_DELAY;
lisa_ws_req.on_data = get_ws_data_cb;
lisa_ws_req.on_event = get_ws_event_cb;
lisa_ws_req.user = NULL;
lisa_ws_req.extra_header = NULL;
LOGI("Connecting to WebSocket server: %s://%s:%s%s", WS_TEST_SCHEME, WS_TEST_HOST, WS_TEST_PORT, WS_TEST_PATH);
lisa_ws_t *lisa_ws_ins = lisa_ws_init(&lisa_ws_req);
if (lisa_ws_ins == NULL) {
LOGE("lisa ws init failed");
return -1;
}
if (lisa_ws_connect(lisa_ws_ins) != 0) {
LOGE("lisa evs websocket connect error");
ret = -1;
goto ERR;
}
// 等待 WebSocket 连接成功
int check_conn_count = 0;
while (g_event != LISA_WS_ON_CONNECTED) {
lisa_thread_mdelay(WS_SEND_RETRY_SLEEP_TIME);
check_conn_count++;
if (check_conn_count >= (WS_SEND_RETRY_DELAY / WS_SEND_RETRY_SLEEP_TIME)) {
LOGE("lisa ws connect timeout");
goto ERR;
}
}
LOGI("\nWebSocket 连接成功,开始发送测试数据...\n");
// 发送测试数据
int ws_test_count = 0;
while (1) {
LOGI("发送消息 %d/%d: %s", ws_test_count + 1, WS_TEST_MAX_COUNT, WS_TEST_CONTENT);
lisa_ws_send_text(lisa_ws_ins, WS_TEST_CONTENT);
lisa_thread_mdelay(1000);
ws_test_count++;
if (ws_test_count >= WS_TEST_MAX_COUNT) {
break;
}
}
LOGI("\n========================================");
LOGI("WebSocket 测试完成");
LOGI("发送消息数: %d", ws_test_count);
LOGI("========================================\n");
ret = 0;
ERR:
if (lisa_ws_ins != NULL) {
lisa_ws_disconnect(lisa_ws_ins);
lisa_ws_cleanup(lisa_ws_ins);
lisa_ws_ins = NULL;
}
return ret;
}

View File

@@ -0,0 +1,29 @@
#!/usr/bin/python
import sys
from os.path import dirname
sys.path.append(dirname(__file__) + "/python-websocket-server")
from websocket_server import WebsocketServer
import logging
# Called for every client connecting (after handshake)
def new_client(client, server):
print("New client connected and was given id %d" % client['id'])
#server.send_message_to_all("Hey all, a new client has joined us")
# Called for every client disconnecting
def client_left(client, server):
print("Client(%d) disconnected" % client['id'])
# Called when a client sends a message
def message_received(client, server, message):
print("Client(%d) sent[%d]" % (client['id'], len(message)))
server.send_message(client, message)
PORT=9001
server = WebsocketServer(host="0.0.0.0", port=PORT, loglevel=logging.INFO)
server.set_fn_new_client(new_client)
server.set_fn_client_left(client_left)
server.set_fn_message_received(message_received)
server.run_forever()

View File

@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(listenai-cmake REQUIRED HINTS $ENV{ARCS_BASE})
project(arcs)
listenai_include_directories(
./
)
add_subdirectory(src)
listenai_add_executable(${PROJECT_NAME})

View File

@@ -0,0 +1 @@
osource "$ARCS_BASE/Kconfig"

View File

@@ -0,0 +1,142 @@
# WiFi & BLE 单核共存示例
## 功能说明
此示例演示如何在 CP (Communication Processor) 单核上同时运行 WiFi 和 BLE 协议栈,实现无线功能的共存。
主要功能:
1. **协议栈共存**:同时初始化 WiFi 和 BLE 协议栈,共享射频资源。
2. **WiFi 功能**:支持扫描、连接 AP、DHCP 获取 IP 等基础 WiFi 功能。
3. **BLE 功能**:支持 BLE 初始化及基础功能。
4. **NVS 存储**:演示如何初始化 NVS (Non-Volatile Storage) 用于存储网络配置或蓝牙绑定信息。
5. **RF 校准**:执行射频校准以确保最佳无线性能。
## 硬件连接
本示例使用芯片内部 WiFi 和 BLE 外设,无需额外接线。
需通过串口查看日志输出:
- 连接到 PC 串口工具
- 波特率:**921600** (默认)
## 使用场景
适用于以下场景:
- 需要同时使用 WiFi 和 BLE 功能的 IoT 设备(如智能配网、网关设备)。
- 验证单核模式下 WiFi 与 BLE 的共存性能。
- 开发基于 WiFi/BLE 的混合应用。
## 示例步骤
1. **系统初始化**:初始化 Shell, Crypto, Event 等基础组件。
2. **存储初始化**:初始化 Flash 和 NVS 文件系统。
3. **RF 初始化**
- 探测射频硬件 (`ls_rf_probe`)。
- 执行射频校准 (`ls_rf_cali_proc`)。
4. **协议栈启动**
- 初始化 WiFi 协议栈 (`ls_wifi_init`)。
- 初始化 BLE 协议栈 (`ls_ble_init`)。
5. **Shell 交互**:通过串口 Shell 使用 `wifi``ble` 相关命令(如果已使能)。
## 编译运行
**编译:**
```{eval-rst}
.. include:: /sample_build.rst
```
**烧录:**
```{eval-rst}
.. include:: /sample_flash.rst
```
## 预期输出
系统启动后,终端将输出初始化日志:
```
[INFO] app-wifi: EVENT_WIFI_INIT_DONE
...
[INFO] app-wifi: EVENT_WIFI_AP_STARTED
...
```
## 核心 API
| API | 说明 |
|-----|------|
| `arcs_nvs_init()` | 初始化 NVS 存储系统 |
| `ls_rf_probe()` | 探测射频硬件 |
| `ls_rf_cali_proc()` | 执行射频校准 |
| `ls_wifi_init()` | 初始化 WiFi 协议栈 |
| `ls_ble_init()` | 初始化 BLE 协议栈 |
## 关键代码
### 1. 系统与存储初始化
初始化 NVS 以支持配置存储,随后进行 RF 校准。
```c
int main(int argc, char **argv)
{
atcmd_init();
#if CONFIG_ARCS_HAL_MODULE_SHELL
shell_init(cli_shell_process);
#endif
// 初始化 NVS
arcs_nvs_init();
// RF 初始化与校准
ls_rf_probe();
ls_rf_cali_proc();
// ...
}
```
### 2. 协议栈共存初始化
依次初始化 WiFi 和 BLE 协议栈。
```c
// 初始化 WiFi
ls_wifi_init();
// 初始化 BLE
ls_ble_init();
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
```
### 3. WiFi 事件处理
```c
static int _app_wifi_event_cb(void *arg, event_module_t event_module, int event_id, void *event_data)
{
switch (event_id)
{
case EVENT_WIFI_INIT_DONE:
LISA_LOGI(TAG, "EVENT_WIFI_INIT_DONE");
break;
case EVENT_WIFI_CONNECTED:
LISA_LOGI(TAG, "EVENT_WIFI_CONNECTED");
net_if_up(net_if_get(WIFI_VIF_STA_IDX));
ls_dhcpc_start(WIFI_VIF_STA_IDX);
break;
// ... 其他事件
}
return 0;
}
```
## 注意事项
1. **资源共享**WiFi 和 BLE 共享同一个射频前端驱动层会自动处理时分复用TDM但在高吞吐量场景下可能会有性能相互影响。
2. **初始化顺序**:建议先进行 RF 校准,再初始化协议栈。
3. **NVS 配置**:本示例使用 Flash 的最后 32KB 作为 NVS 分区,请确保该区域未被其他数据(如固件)覆盖。

View File

@@ -0,0 +1,117 @@
/* Don't modify this file, this file is generate from bt_configy.py */
#ifndef _BT_CONFIG_H_
#define _BT_CONFIG_H_
//#
//# bt ip config
//#
//# Top level product configuration
//# controller
#define BT_EMB_PRESENT 0
#define BLE_EMB_PRESENT 1
#define BLE_ISO_PRESENT 0
#define BT_DUAL_MODE (BT_EMB_PRESENT && BLE_EMB_PRESENT)
//# run ble only on dual mode set 1, run dual mode set 0
#define SINGLE_RUN_ON_DUAL 1
//# host
#define BLE_HOST_PRESENT 1
#define BT_STACK_PRESENT 0
//# classic profile
#define BT_MUSIC_PRESENT 0
#define BT_CALL_PRESENT 0
//# ble profile
#define BLE_GAF_PRESENT 0
#define SMP_PRESENT 1
#define LEA_PRESENT 0
#define MESH_PRESENT 0
//# transport
#define HCIT_UART_PRESENT 0
#define HCIT_USB_PRESENT 0
//#hci audio access
#define HCIT_AUD_PRESENT 0
#define HCI_PRESENT 1
#define AHI_PRESENT 0
#define BLE_APP_PRESENT 1
#define TWS_PRESENT 0
#define LISTENAI_TWS_SUPPORT TWS_PRESENT
//# classic bt config
//# Maximum number of ACL links
#define MAX_NB_ACTIVE_ACL 4
//# Maximum number of Synchronous connections (0 to 2)
#define MAX_NB_SYNC 2
//# FPGA RF board
#define RF_MAX2830_SUPPORT 0
#define RF_ARCS_B0_SUPPORT 0
//# DEBUG SETUP
#define LS_DEBUG 1
#define LS_DEBUG_MEM 1
#define LS_DEBUG_FLASH 0
#define LS_DEBUG_STACK_PROF 0
#define DBG_LOG_PRESENT 0
//# for fpga two node direct connect test
#define LS_DEBUG_FPGA_DIRECT_MODE 0
//#for bis aes generate gsk in interrupt,for testcase iso_bis_p2p@31
#define LS_BIS_GEN_GSK_INT 1
//# TRACER SETUP
#define TRACER_PRESENT 0
//# TRACE MASK, see dbg_trc_cfg_fields
#define TRACE_CFG_MASK 0xffffffff
//#/// Support HL Message API
#define BLE_HL_MSG_API 1
//#/// Support GATT Client
#define BLE_GATT_CLI 1
//#/// Number of L2CAP COC channel that can be created per connection
#define L2CAP_COC_CHAN_PER_CON_NB (10)
//#/// Total Number of L2CAP channel and GATT bearer that can be allocated in environment heap
#define L2CAP_CHAN_IN_ENV_NB (10)
//#/// Maximal authorized MTU / MPS value - Depends on memory size available
#define GAP_LE_MTU_MAX (2048)
#define GAP_LE_MPS_MAX (2048)
//#/// Maximum attribute value length
#define GATT_MAX_VALUE (2048)
//#/// Maximum number of devices in RAL
#define BLE_RAL_MAX (3)
//#/// Maximum number of simultaneous BLE activities (scan, connection, advertising, initiating)
#define BLE_ACTIVITY_MAX (5)
//#/// Maximum number of simultaneous connections
#define BLE_CONNECTION_MAX (3)
//#/// LE Power Control
#define BLE_PWR_CTRL (1)
//# Maximum number of advertising BLE activities
#define BLE_ACTIVITY_ADV_MAX (1)
//# Maximum number of scan BLE activities
#define BLE_ACTIVITY_SCAN_MAX (1)
//# Maximum number of connection BLE activities
#define BLE_ACTIVITY_CON_MAX (1)
//# Maximum number of initiating BLE activities
#define BLE_ACTIVITY_INIT_MAX (1)
//# ISO configure
//#// Connected Isochronous Stream
#define BLE_CIS 1
//#// Broadcast Isochronous Stream
#define BLE_BIS 1
//#/// Maximum number of ISO channel / streams
#define BLE_ISO_CON 4
//#/// Proprietary ISO over HCI
#define BLE_ISOOHCI 1
//#/// Internal ISO generator for validation purpose
#define BLE_ISOGEN 1
//#notify
#define BT_RTOS_NOTIFY_SUPPORT 1
#endif // _BT_CONFIG_H_

View File

@@ -0,0 +1,177 @@
#!/bin/bash
set -e
usage() {
echo "使用方式: $0 [选项]"
echo "选项:"
echo " -S, --Source <path> 指定项目源码路径 (默认为当前脚本所在目录)"
echo " -t, --target <target> 指定构建目标 (如 menuconfig)"
echo " -C, --Clean 清理构建目录"
echo " -B, --build 构建输出目录"
echo " -h, --help 显示此帮助信息"
echo " -r, --release 以 Release 模式构建 (移除 DEBUG_PATH 信息)"
echo " -w, --warnings-as-errors 将警告视为错误"
echo " -D<var>=<value> 传递 CMake 变量 (可多次使用)"
echo ""
echo "示例:"
echo " $0 -S samples/helloworld -DBOARD=arcs_mini 指定板型构建"
echo " $0 -S samples/helloworld -DBOARD=arcs_evb 使用 EVB 板型"
echo " $0 -S samples/helloworld -t menuconfig -DBOARD=arcs_mini 运行 menuconfig"
echo " $0 -C -S samples/helloworld -DBOARD=arcs_mini 清理并重新构建"
echo " $0 -S samples/helloworld -DBOARD=my_board -DBOARD_SEARCH_PATH=/path/to/boards 使用自定义板型"
exit 1
}
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_PATH="$SCRIPT_DIR"
TARGET=""
CLEAN=false
OUTPUT="build"
WARNINGS_AS_ERRORS=false
RELEASE=false
ARCS_BASE_DIR_NAME="arcs-sdk"
ARCS_DEV_TOOLS_DIR_NAME="listenai-dev-tools"
ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME="gcc"
ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME="listenai-tools"
find_arcs_base() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_BASE_DIR_NAME"
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found ARCS_BASE: $current_dir/$dir_name"
export ARCS_BASE="$current_dir/$dir_name"
return 0
fi
current_dir=$(dirname "$current_dir")
done
echo "ARCS_BASE not found, Please add ARCS_BASE environment variable or set ARCS_BASE_DIR_NAME to the correct directory."
echo "Current Target ARCS_BASE directory name: $ARCS_BASE_DIR_NAME."
exit 1
}
find_dev_tools() {
local current_dir=$(cd "$(dirname "$0")" && pwd)
local dir_name="$ARCS_DEV_TOOLS_DIR_NAME"
echo "trying to find $dir_name in parent directories..."
while [ "$current_dir" != "/" ]; do
if [ -d "$current_dir/$dir_name" ]; then
echo "Found $dir_name: $current_dir/$dir_name"
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME" ]; then
echo "Found LISTENAI_TOOLS_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
export LISTENAI_TOOLS_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_LISTENAI_TOOLS_DIR_NAME"
fi
if [ -d "$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME" ]; then
echo "Found NUCLEI_TOOLCHAIN_PATH: $current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
export NUCLEI_TOOLCHAIN_PATH="$current_dir/$dir_name/$ARCS_DEV_TOOL_TOOLCHAIN_DIR_NAME"
fi
return 0
fi
current_dir=$(dirname "$current_dir")
done
}
while [[ $# -gt 0 ]]; do
case $1 in
-S|--Source)
PROJECT_PATH="$2"
shift 2
;;
-B|--build)
OUTPUT="$2"
shift 2
;;
-t|--target)
TARGET="$2"
shift 2
;;
-C|--Clean)
CLEAN=true
shift 1
;;
-w|--warnings-as-errors)
WARNINGS_AS_ERRORS=true
shift 1
;;
-h|--help)
usage
;;
-r|--release)
RELEASE=true
shift 1
;;
-D*)
CMAKE_VARS+=("$1")
shift 1
;;
*)
echo "未知参数: $1"
usage
;;
esac
done
echo "Source: $PROJECT_PATH"
echo "Target: $TARGET"
echo "Clean : $CLEAN"
if [ -z "$LISTENAI_TOOLS_PATH" ] || [ -z "$NUCLEI_TOOLCHAIN_PATH" ]; then
find_dev_tools
fi
if [ -z "${LISTENAI_TOOLS_PATH}" ]; then
export LISTENAI_TOOLS_PATH="请添加 LISTENAI_TOOLS_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 LISTENAI_TOOLS_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
if [ -z "${NUCLEI_TOOLCHAIN_PATH}" ]; then
export NUCLEI_TOOLCHAIN_PATH="请添加 NUCLEI_TOOLCHAIN_PATH 环境变量,或在此行设置正确的路径"
echo "请添加 NUCLEI_TOOLCHAIN_PATH 环境变量或者修改脚本后, 注释脚本第 $LINENO";exit 1;
fi
############### 下面代码不用修改 ##################
# 构建工具的位置
CMAKE_PROGRAM="$LISTENAI_TOOLS_PATH/cmake/bin/cmake"
NINJA_PROGRAM="$LISTENAI_TOOLS_PATH/ninja/ninja"
# 配置环境变量 ARCS_BASE
if [ -z "$ARCS_BASE" ]; then
find_arcs_base
fi
if [ "$CLEAN" = true ]; then
rm -rf $OUTPUT
fi
# Initialize CMAKE_VARS array if it doesn't exist
declare -a CMAKE_VARS
# Add warnings-as-errors flag if enabled
if [ "$WARNINGS_AS_ERRORS" = true ]; then
CMAKE_VARS+=("-DCMAKE_C_FLAGS=-Werror")
CMAKE_VARS+=("-DCMAKE_CXX_FLAGS=-Werror")
echo "Treating warnings as errors"
fi
# Add release flags if enabled
if [ "$RELEASE" = true ]; then
CMAKE_VARS+=("-DENABLE_DEBUG_PATH=OFF")
echo "Release mode enabled (-DENABLE_DEBUG_PATH=OFF)"
fi
$CMAKE_PROGRAM -B "$OUTPUT" -G Ninja -S "$PROJECT_PATH" \
-DCMAKE_MAKE_PROGRAM="$NINJA_PROGRAM" \
"${CMAKE_VARS[@]}"
if [ -z "$TARGET" ]; then
$CMAKE_PROGRAM --build "$OUTPUT" -j4
else
$CMAKE_PROGRAM --build "$OUTPUT" --target "$TARGET"
fi

View File

@@ -0,0 +1,19 @@
/*
* PowerManager.c
*
* Created on: Feb 2, 2023
* Author: USER
*/
#include "PowerManager.h"
void PowerManager_CallOnce_Hook(pm_callonce_hook* __hook){
static uint8_t call_once_flag = 0;
if (call_once_flag == 0 && __hook){
__hook();
}
// Never edit
call_once_flag = 1;
}

View File

@@ -0,0 +1,16 @@
/*
* PowerManager.h
*
* Created on: Feb 2, 2023
* Author: USER
*/
#ifndef SRC_VEGAP_REMOTE_PM_POWERMANAGER_H_
#define SRC_VEGAP_REMOTE_PM_POWERMANAGER_H_
#include "stdint.h"
typedef void pm_callonce_hook(void);
void PowerManager_CallOnce_Hook(pm_callonce_hook* __hook);
#endif /* SRC_VEGAP_REMOTE_PM_POWERMANAGER_H_ */

View File

@@ -0,0 +1,48 @@
CONFIG_BACKTRACE=y
CONFIG_HEAP_SIZE=0xC000
CONFIG_PSRAM_HEAP_SIZE=0x200000
CONFIG_SDK_MODULE_LETTER_SHELL_ENHANCE=y
CONFIG_RTOS_AL=y
CONFIG_ARCS_CP_CORE=y
CONFIG_ARCS_HAL_WCND=y
CONFIG_ARCS_HAL_AT_CMD=y
CONFIG_ARCS_HAL_FLASH_IF=y
CONFIG_ARCS_HAL_NVS=y
CONFIG_LISA_PORTING=y
CONFIG_LISA_OS=y
CONFIG_LISA_DEVICE=y
CONFIG_LISA_SHELL=y
CONFIG_LISA_WIFI=y
CONFIG_LISA_SDMMC_DEVICE=y
CONFIG_WIFI_MANAGER=y
CONFIG_WIFI_LWIP_SAME_CORE=y
CONFIG_MAC_MANAGER=y
CONFIG_MAC_MANAGER_MAC_BY_CHIP_ID=y
CONFIG_FILE_SYSTEM=y
CONFIG_FATFS_FILESYSTEM=y
CONFIG_DISK_DRIVER=y
CONFIG_DISK_DRIVER_SDMMC=y
CONFIG_LSFS=y
CONFIG_LSFS_FAT=y
CONFIG_LSFS_REGISTER_VFS=y
CONFIG_LVFS=y
# CONFIG_MODULE_EASYFLASH=y
# Lisa Bluetooth
CONFIG_BLE=y
CONFIG_LISA_BLUETOOTH=y
CONFIG_BLE_PROFILE_HOGPD=y
CONFIG_BLE_PROFILE_NETCFG_BLES=y
CONFIG_BLE_PROFILE_BASS=y

View File

@@ -0,0 +1,7 @@
tests:
samples.wifi_ble_single_core:
programmer: arcs
runner: uart
log_analyzer:
type: "regex"
pattern: "EVENT_WIFI_INIT_DONE"

View File

@@ -0,0 +1,12 @@
listenai_library_named(app)
listenai_library_sources(
main.c
app_at_cmd.c
app_net_cfg.c
fs/user_fs.c
)
listenai_include_directories(
./
fs/
)

View File

@@ -0,0 +1,24 @@
#include "shell.h"
#include "shell_passthrough.h"
// 透传模式处理函数(在shell_task中运行,快速返回)
static int at_passthrough_handler(char *data, unsigned short len)
{
atcmd_handler(data, len);
return 0; // 立即返回,不阻塞shell_task
}
// 初始化AT命令处理系统
void at_cmd_init(void)
{
// shell任务同步执行AT命令,无需额外队列
}
// 注册透传模式命令
// 使用方式:
// 交互模式: AT (进入后可输入AT+?,不需要空格)
// 单行模式: AT AT+? (一次性执行)
SHELL_EXPORT_PASSTROUGH(SHELL_CMD_PERMISSION(0), AT, AT>>, at_passthrough_handler, AT command passthrough mode);

View File

@@ -0,0 +1,142 @@
/**
****************************************************************************************
*
* @file netcfg.c
*
* @brief net config
*
* Copyright (C) ListenAI 2020-2099
*
*
****************************************************************************************
*/
/*
* MACROS
****************************************************************************************
*/
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <stdio.h>
#include "netcfg_ble.h"
#include "netcfg_bles.h"
#include "ble_gap.h"
#include "ble_gatt.h"
#include "ble_prf.h"
#include "ls_wifi_type.h"
#include "nvds_tag_def.h"
#include "wifi_api.h"
#include "btos_al.h"
#define BLE_AUTO_SEND_NET_CFG_SUCESS (1)
extern void HAL_PMU_Chip_Software_Reset_Enable(void);
/// netcfg_ble service environment
static void netcfg_ble_wifi_cfg_store(const int8_t *ssid, const int8_t *pwd, const uint8_t *auto_conn)
{
#if CFG_NVS
nvds_put(NVDS_TAG_WIFI_STA_SSID, NVDS_LEN_WIFI_STA_SSID, ssid);
if (strlen(pwd)) {
nvds_put(NVDS_TAG_WIFI_STA_PWD, NVDS_LEN_WIFI_STA_PWD, pwd);
} else {
nvds_del(NVDS_TAG_WIFI_STA_PWD);
}
nvds_put(NVDS_TAG_WIFI_STA_AUTOCONN, NVDS_LEN_WIFI_STA_AUTOCONN, auto_conn);
#endif
}
static int netcfg_ble_wifi_connect(const int8_t *ssid, const int8_t *pwd)
{
wifi_connect_cfg_t sta_config = {
.dhcp_mode = DHCP_CLIENT,
};
if (!ssid || !pwd)
return -1;
strcpy(sta_config.ssid, ssid);
strcpy(sta_config.key, pwd);
wifi_sta_connect(&sta_config);
return 0;
}
void netcfg_bles_con_cleanup(uint8_t conidx, uint16_t reason)
{
void (*pReset)(void);
if(netcfg_bles_get_state() == NETCFG_BLE_REBOOTING)
HAL_PMU_Chip_Software_Reset_Enable();
else
netcfg_bles_set_state(NETCFG_BLE_IDLE);
}
uint16_t netcfg_ble_notify_wifi(struct netcfg_ble_data *data)
{
int status = 0;
uint8_t auto_conn = 1;
#if NETCFG_BLE_DBG
NETCFG_BLE_LOGD("[%s]: Get ssid = %s, pwd = %s\n", __func__, data->ssid, data->pwd);
#endif
/*
* Fix me: Instead of directly call wifi connect, we need to create a task handling
* the messages and wifi/ble status exchange here.
* Will implement this later.
*/
status = netcfg_ble_wifi_connect((const int8_t *)data->ssid, (const int8_t *)data->pwd);
if (status)
return NETCFG_BLE_ERR;
netcfg_ble_wifi_cfg_store((const int8_t *)data->ssid, (const int8_t *)data->pwd, &auto_conn);
return NETCFG_BLE_SUCCESS;
}
#if (BLE_AUTO_SEND_NET_CFG_SUCESS == 1)
/// dummy to send
void netcfg_bles_send_connect_status_cb(TimerHandle_t time_id)
{
app_ble_netcfg_bles_send_notify(0, 0, 0, 0, 0);
btos_timer_cancel(time_id);
}
void netcfg_bles_send_connect_status_dummy(uint32_t milli_seconds)
{
TimerHandle_t update_id = btos_timer_creat(TIMER_TYPE_SINGLE, milli_seconds, netcfg_bles_send_connect_status_cb);
}
#endif
uint16_t netcfg_bles_profile_set_cb(uint8_t conidx, uint8_t att_idx, uint16_t op, uint8_t *p_value)
{
uint16_t sta = NETCFG_BLE_ERR;
switch(op)
{
case NETCFG_BLE_OP_DONE:
{
sta = netcfg_ble_notify_wifi((struct netcfg_ble_data *)p_value);
///dummy to send connect status.
#if (BLE_AUTO_SEND_NET_CFG_SUCESS == 1)
netcfg_bles_send_connect_status_dummy(1000);
#endif
}
break;
case NETCFG_BLE_OP_REBOOT:
{
ble_gap_disconnect(conidx, 0x13);
}
break;
default:break;
}
return sta;
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lsfs.h"
#include "disk/disk_access.h"
#include "lisa_sdmmc.h"
#include "log_print.h"
#if CONFIG_LVFS
#include "lvfs.h"
#endif
// #define FLASHDISK_DEVICE "NAND:"
#define DISK_DEVICE "SD:"
#define DISK_MOUNT_POINT "/" DISK_DEVICE
static struct lsfs_mount_t flash_lsfs_mnt = {
.type = LSFS_FATFS,
.mnt_point = DISK_MOUNT_POINT,
.fs_data = NULL,
};
static int fs_mount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_mount(mp);
if (ret != 0)
{
CLOG("No file system, try reformatting, mount ret: %d", ret);
/* 格式化底层设备,去掉挂载点前面的'/' 作为设备名,例如 "/SD:" -> "SD:" */
ret = lsfs_mkfs(mp->type, &mp->mnt_point[1], NULL, 0);
if (ret == 0)
{
CLOG("mkfs success, retry mount");
ret = lsfs_mount(mp);
}
else
{
CLOG("mkfs failed, ret: %d", ret);
}
}
if (ret != 0)
{
CLOG("Failed to mount filesystem: %d", ret);
return ret;
}
CLOG("%s mounted successfully", mp->mnt_point);
return 0;
}
static int fs_unmount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_unmount(mp);
if (ret != 0)
{
CLOG("%s unmount failed!\n", mp->mnt_point);
}
CLOG("%s unmount success!\n", mp->mnt_point);
}
static int list_dir(const char *path)
{
struct lsfs_dir_t dir;
struct lsfs_dirent entry;
int ret;
int nfile = 0, ndir = 0;
/* 初始化目录对象 */
lsfs_dir_t_init(&dir);
ret = lsfs_opendir(&dir, path);
CLOG("%s opendir: %d", path, ret);
if (ret == 0)
{
while (1)
{
ret = lsfs_readdir(&dir, &entry);
if (ret != 0 || entry.name[0] == 0)
break; /* Error or end of dir */
if (entry.type == LSFS_DIR_ENTRY_DIR)
{
/* Directory */
CLOG(" <DIR> %s", entry.name);
ndir++;
}
else
{
/* File */
CLOG("%10lu %s", entry.size, entry.name);
nfile++;
}
}
lsfs_closedir(&dir);
CLOG("%d dirs, %d files.", ndir, nfile);
}
else
{
CLOG("Failed to open \"%s\". (%u)", path, ret);
}
return ret;
}
static void user_fs_change_to_root_folder(void)
{
int ret;
ret = lvfs_chdrive(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change drive to %s, error: %d\n", DISK_MOUNT_POINT, ret);
}
ret = lvfs_chdir(DISK_MOUNT_POINT);
if (ret != 0) {
CLOG("Failed to change directory to /SD:/, error: %d\n", ret);
} else {
CLOG("Changed to directory /SD:/\n");
}
}
int user_fs_init(void){
lisa_sdmmc_probe(lisa_device_get("sdmmc0"));
disk_init(NULL);
#if CONFIG_LVFS
lvfs_init();
#endif
lsfs_init();
if(0 != fs_mount(&flash_lsfs_mnt)){
return -1;
}
user_fs_change_to_root_folder();
return 0;
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
int user_fs_init(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,357 @@
#include <string.h>
#include <stdbool.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include "arcs_ap_base.h"
#include "cli_main.h"
#include "spiflash.h"
#include "lisa_bluetooth.h"
#include "bt_app_if.h"
#include "bt_stack_if.h"
#include "hogpd_msg.h"
#include "hogpd.h"
#include "bass.h"
#include "diss.h"
#include "netcfg_bles.h"
#include "FreeRTOS.h"
#include "task.h"
#include "nvs.h"
#include "shell.h"
#define TAG "app-wifi"
#include "lisa_log.h"
#include "lisa_shell.h"
#include "mac_manager.h"
#include "mac_manager_ops.h"
#include "wifi_manager/wifi_manager.h"
#if CONFIG_LISA_WIFI
#include "lisa_wifi.h"
#endif
#include "ls_event.h"
#include "ls_wifi_type.h"
#include "net_al.h"
#include "net_def.h"
#define TARGET_WIFI_SSID "listenai"
#define TARGET_WIFI_PWD "listenai"
#define WIFI_MGR_SEARCH_AP_BUFFER_SIZE 10
#define WIFI_MGR_AUTO_CONNECT_INTERVAL_MS 2000
static mac_manager_t *m_mac_manager;
static wifi_mgr_sta_config_t list[WIFI_MGR_SEARCH_AP_BUFFER_SIZE] = { 0 };
#if CFG_NVS
#define NVDS_FLASH_ADDRESS_OFFSET (0xFF8000) //The last 32KB of 16B flash
#define NVDS_FLASH_SIZE (0x8000) //32KB
struct nvs_fs arcs_nvs_fs;
FLASH_DEV arcs_flash_dev = {
.base_addr = CMN_FLASHC_BASE,
.d_width = 4,
.sclk_div = 0xFF, //divider is 1
.run_mod = RUN_WITHOUT_INT,
.timeout = 0x180000
};
int arcs_nvs_init(void)
{
struct flash_pages_info info;
flash_if_init(&arcs_flash_dev, 0, 0);
//init nvs module
arcs_nvs_fs.offset = NVDS_FLASH_ADDRESS_OFFSET;
arcs_nvs_fs.flash_device = &arcs_flash_dev;
flash_get_page_info_by_offs(&arcs_flash_dev, arcs_nvs_fs.offset, &info);
arcs_nvs_fs.sector_size = info.size;
arcs_nvs_fs.sector_count = NVDS_FLASH_SIZE/info.size;
nvds_init(&arcs_nvs_fs);
return 0;
}
#endif
static mac_manager_t *m_mac_manager;
static void user_wifi_manager_init(void);
static void cb_lisa_wifi_init_done(void)
{
LOGI("lisa_wifi_init_done");
user_fs_init();
lisa_kv_init();
user_wifi_manager_init();
}
static int8_t custom_get_wifi_mac(uint8_t mac_addr[6])
{
int8_t ret = 0;
if (!mac_addr)
return -1;
ret = mac_manager_get(m_mac_manager, mac_addr, 6);
LOGI("custom_get_wifi_mac: %d, %02X:%02X:%02X:%02X:%02X:%02X", ret, mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
return ret;
}
static void dhcp_status_callback(int vif_idx, bool success, uint32_t ip_addr, uint32_t netmask, uint32_t gateway, void *arg)
{
if (success) {
LOGI("DHCP Success on VIF-%d: IP=%d.%d.%d.%d, Mask=%d.%d.%d.%d, GW=%d.%d.%d.%d",
vif_idx,
ip_addr & 0xff, (ip_addr >> 8) & 0xff, (ip_addr >> 16) & 0xff, (ip_addr >> 24) & 0xff,
netmask & 0xff, (netmask >> 8) & 0xff, (netmask >> 16) & 0xff, (netmask >> 24) & 0xff,
gateway & 0xff, (gateway >> 8) & 0xff, (gateway >> 16) & 0xff, (gateway >> 24) & 0xff);
} else {
LOGI("DHCP Failed on VIF-%d", vif_idx);
}
}
static void wifi_mgr_connection_cb(wifi_mgr_connection_info_t *connection_info, void *arg)
{
net_if_t *net_if;
LOGI("mgr connection: %d", connection_info->status);
switch (connection_info->status)
{
case WIFI_MGR_STA_CONNECTED:
LOGI("WiFi connected, starting network interface");
net_if = net_if_get(WIFI_VIF_STA_IDX);
net_if_up(net_if);
if (!net_if->static_ip) {
ls_dhcpc_start(WIFI_VIF_STA_IDX);
}
break;
case WIFI_MGR_STA_DISCONNECTED:
LOGI("WiFi disconnected, stopping network interface");
ls_dhcpc_stop(WIFI_VIF_STA_IDX);
net_if_down(net_if_get(WIFI_VIF_STA_IDX));
break;
case WIFI_MGR_STA_CONNECTING:
LOGI("WiFi connecting...");
break;
default:
break;
}
}
static void user_mac_manager_init(void)
{
mac_manager_config_t config = {
.random_mac_if_mac_invalid = false,
};
m_mac_manager = mac_manager_init(mac_manager_ops_get()->mem_ops, mac_manager_ops_get()->content_ops, &config);
if (m_mac_manager == NULL) {
LOGI("[user_wifi]mac_manager_init failed\n");
assert(0);
}
}
static void user_wifi_manager_init(void)
{
wifi_mgr_sta_config_t cfg = {
.ssid = TARGET_WIFI_SSID,
.pwd = TARGET_WIFI_PWD,
};
wifi_mgr_autoconn_config_t autoconn_cfg = {
.interval_ms = WIFI_MGR_AUTO_CONNECT_INTERVAL_MS,
};
wifi_mgr_init(wifi_mgr_ops_get());
wifi_mgr_sta_enable();
wifi_mgr_sta_add_connection_cb(wifi_mgr_connection_cb, NULL);
/* Search and delete all saved APs */
int count = wifi_mgr_storage_search_ap(list, 10, SEARCH_ALL, NULL);
for (int i = 0; i < count; i++) {
LOGI("Deleting saved AP: ssid=%s", list[i].ssid);
wifi_mgr_storage_delete_ap(&list[i]);
}
// wifi_mgr_storage_save_ap(&cfg);
// wifi_mgr_auto_connect_start(&autoconn_cfg);
}
uint8_t manufacturer_data[18] = {
0xab, 0x0a, 0xa1, 0xdc, 0xa8, 0x76, 0x83, 0x65, 0x73, 0x83, 0x72, 0x65, 0x82, 0x67, 0x83, 0x68, 0x00, 0x78,
};
ble_gap_cfg_t user_bt_stack_dev_cfg = {
.addr = {{0x44, 0x55, 0x66, 0x03, 0x23, 0x20}, 0},
.name_len = sizeof(DEVICE_NAME),
.name = DEVICE_NAME,
.appearance = GAP_APP_GENERIC_MEDIA_PLAYER, // hid_keyboard
.iocap = GAP_IO_CAP_NO_INPUT_NO_OUTPUT,
.auth = GAP_SEC_NOT_ENC,
.pairing_mode = GAPM_PAIRING_LEGACY,
};
#if (ADV_USER_DATA)
uint8_t lisa_ble_gen_user_adv_data(uint8_t *p_data)
{
uint8_t nb_uuid = 1;
uint16_t uuids[1] = {HID_UUID};
// Remaining Length
uint8_t rem_len = LEGA_ADV_DATA_LEN - 3;
uint8_t *p_buf = p_data;
uint8_t length = 0;
/// add Manufacturer specific
*p_buf++ = sizeof(manufacturer_data) + 1;
*p_buf++ = 0xff; // GAP_AD_TYPE_MANU_SPECIFIC_DATA;
memcpy(p_buf, manufacturer_data, 18);
p_buf += sizeof(manufacturer_data);
length += (sizeof(manufacturer_data) + 2);
// Sanity check
ASSERT_ERR(rem_len >= LEGA_ADV_DATA_LEN - 3);
// Get remaining space in the Advertising Data - 2 bytes are used for name length/flag
rem_len -= length;
// Check if additional data can be added to the Advertising data - 2 bytes needed for type and length
if (rem_len > 2) {
uint8_t dev_name_length = MIN(user_bt_stack_dev_cfg.name_len, (rem_len - 2));
// Device name length
*p_buf = dev_name_length + 1;
// Device name flag (check if device name is complete or not)
*(p_buf + 1) = (dev_name_length == user_bt_stack_dev_cfg.name_len)
? 0x09
: 0x08; // GAP_AD_TYPE_COMPLETE_NAME : GAP_AD_TYPE_SHORTENED_NAME;
// Copy device name
memcpy(p_buf + 2, user_bt_stack_dev_cfg.name, dev_name_length);
// Update advertising data length
length += (dev_name_length + 2);
}
return length;
}
#endif
uint8_t adv_user_data[LEGA_ADV_DATA_LEN];
const uint8_t *lisa_bt_get_adv_data(uint8_t *len)
{
*len = lisa_ble_gen_user_adv_data(adv_user_data);
return adv_user_data;
}
extern uint16_t netcfg_bles_profile_set_cb(uint8_t conidx, uint8_t att_idx, uint16_t op, uint8_t *p_value);
/// Message callback handle from APP
static const netcfg_bles_cb_t netcfg_app_cb = {
.cb_value_set = netcfg_bles_profile_set_cb,
};
const diss_cb_t user_bt_stack_ble_diss_msg_cb = {
.cb_value_get = NULL, // dis_profile_get_cb,
};
/**
* @brief 初始化自定义服务
*
* 注册 GATT 用户回调并添加服务到数据库。
* 此函数应在协议栈初始化完成后调用。
*/
void app_ble_init_cmp(void)
{
// enable bass service
ble_bass_init();
ble_bass_enable(0, bt_stack_vbat_percent_get());
// enable net config.
ble_netcfg_bles_init((netcfg_bles_cb_t *)&netcfg_app_cb);
// enable diss.
ble_diss_init((diss_cb_t *)&user_bt_stack_ble_diss_msg_cb);
#if BLE_VOICE_SIMULATOR
// enable hid service
uint8_t svc_features = HOGPD_CFG_KEYBOARD | HOGPD_CFG_MOUSE | HOGPD_CFG_PROTO_MODE | HOGPD_CFG_REPORT_NTF_EN;
uint8_t report_char_cfg = HOGPD_CFG_REPORT_IN;
hogpd_report_map_t report_map = {sizeof(hid_report_map), 0, (uint8_t *)hid_report_map};
ble_hogpd_init(svc_features, report_char_cfg, (hogpd_cb_t *)&bt_stack_ble_hogpd_msg_cb, &report_map);
ble_hogpd_enable(0);
#endif
}
int main(int argc, char **argv)
{
int ret = 0;
atcmd_init();
arcs_nvs_init();
ret = lisa_shell_init();
if (ret != 0) {
LOGI("Failed to initialize shell (error: %d)\n", ret);
}
user_mac_manager_init();
// 注册 DHCP 状态回调
net_dhcp_register_status_callback(dhcp_status_callback, NULL);
lisa_wifi_ops_t ops = {
.init_done = cb_lisa_wifi_init_done,
.custom_mac = custom_get_wifi_mac,
};
lisa_wifi_init(&ops);
// BLE 初始化
lisa_bluetooth_init();
// Start Advertising
app_ble_adv_start(0, BLE_ADV_GEN);
// 初始化AT命令处理系统
at_cmd_init();
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
return 0;
}
static int user_wifi_command(int argc, char *argv[])
{
char cmd_str[256] = {0};
for (int i = 1; i < argc; i++) {
strncat(cmd_str, argv[i], sizeof(cmd_str) - strlen(cmd_str) - 1);
if (i < argc - 1) {
strncat(cmd_str, " ", sizeof(cmd_str) - strlen(cmd_str) - 1);
}
}
if (cmd_str[0]) {
wifi_cmd_handler(cmd_str, strlen(cmd_str) + 1);
}
return 0;
}
SHELL_EXPORT_CMD(SHELL_CMD_PERMISSION(0) | SHELL_CMD_TYPE(SHELL_TYPE_CMD_MAIN) | SHELL_CMD_DISABLE_RETURN, wifi,
user_wifi_command, wifi commands);

Some files were not shown because too many files have changed in this diff Show More