Init: 导入AvaotaF1客户端源码

This commit is contained in:
Kevin Wong
2025-12-31 15:13:39 +08:00
parent 77acf96967
commit cadb16dfce
30 changed files with 7982 additions and 36 deletions

1068
Device_Tree/board.dts Normal file

File diff suppressed because it is too large Load Diff

95
ESP32S3/ICM42688.cpp Normal file
View File

@@ -0,0 +1,95 @@
// --- START OF ICM42688.cpp ---
#include "ICM42688.h"
ICM42688::ICM42688(TwoWire &bus, uint8_t address) {
_bus = &bus;
_address = address;
_accelScale = 0.0f;
_gyroScale = 0.0f;
}
int ICM42688::begin() {
uint8_t who_am_i = 0;
readRegisters(ICM42688_WHO_AM_I, 1, &who_am_i);
if(who_am_i != ICM42688_DEVICE_ID) {
return -1; // Wrong device
}
// Reset device
writeRegister(0x4E, 0x01);
delay(100);
// Set accel and gyro to standby
writeRegister(0x4E, 0x1F);
delay(1);
// Set accel full scale
writeRegister(0x4F, (uint8_t)AFS::AFS_16G << 5 | (uint8_t)ODR::ODR_1KHZ);
_accelScale = 16.0f / 32768.0f;
// Set gyro full scale
writeRegister(0x50, (uint8_t)GFS::GFS_2000DPS << 5 | (uint8_t)ODR::ODR_1KHZ);
_gyroScale = 2000.0f / 32768.0f;
// Turn on accel and gyro
writeRegister(0x4E, 0x0F);
delay(100);
return 0;
}
int ICM42688::readSensor() {
uint8_t data[14];
readRegisters(0x1D, 14, data);
_t = (int16_t)data[0] << 8 | data[1];
_ax = (int16_t)data[2] << 8 | data[3];
_ay = (int16_t)data[4] << 8 | data[5];
_az = (int16_t)data[6] << 8 | data[7];
_gx = (int16_t)data[8] << 8 | data[9];
_gy = (int16_t)data[10] << 8 | data[11];
_gz = (int16_t)data[12] << 8 | data[13];
return 0;
}
float ICM42688::getAccelX_mss() { return (float)_ax * _accelScale * _G; }
float ICM42688::getAccelY_mss() { return (float)_ay * _accelScale * _G; }
float ICM42688::getAccelZ_mss() { return (float)_az * _accelScale * _G; }
float ICM42688::getGyroX_rads() { return (float)_gx * _gyroScale * _d2r; }
float ICM42688::getGyroY_rads() { return (float)_gy * _gyroScale * _d2r; }
float ICM42688::getGyroZ_rads() { return (float)_gz * _gyroScale * _d2r; }
float ICM42688::getGyroX_dps() { return (float)_gx * _gyroScale; }
float ICM42688::getGyroY_dps() { return (float)_gy * _gyroScale; }
float ICM42688::getGyroZ_dps() { return (float)_gz * _gyroScale; }
float ICM42688::getTemperature_C() { return ((float)_t / _tempScale) + _tempOffset; }
void ICM42688::writeRegister(uint8_t reg, uint8_t data) {
_bus->beginTransmission(_address);
_bus->write(reg);
_bus->write(data);
_bus->endTransmission();
}
uint8_t ICM42688::readRegister(uint8_t reg) {
_bus->beginTransmission(_address);
_bus->write(reg);
_bus->endTransmission(false);
_bus->requestFrom(_address, (uint8_t)1);
uint8_t data = _bus->read();
return data;
}
void ICM42688::readRegisters(uint8_t reg, uint8_t count, uint8_t *dest) {
_bus->beginTransmission(_address);
_bus->write(reg);
_bus->endTransmission(false);
_bus->requestFrom(_address, count);
for(uint8_t i = 0; i < count; i++){
dest[i] = _bus->read();
}
}
// --- END OF ICM42688.cpp ---

85
ESP32S3/ICM42688.h Normal file
View File

@@ -0,0 +1,85 @@
// --- START OF ICM42688.h ---
#ifndef ICM42688_H
#define ICM42688_H
#include "Arduino.h"
#include "Wire.h"
#include "SPI.h"
// See datasheet for details
#define ICM42688_DEVICE_ID 0x47
#define ICM42688_WHO_AM_I 0x75
/*
ICM42688_I2C class definition
*/
class ICM42688
{
public:
enum class AFS {
AFS_16G = 0,
AFS_8G,
AFS_4G,
AFS_2G
};
enum class GFS {
GFS_2000DPS = 0,
GFS_1000DPS,
GFS_500DPS,
GFS_250DPS,
GFS_125DPS,
GFS_62_5DPS,
GFS_31_25DPS,
GFS_15_625DPS
};
enum class ODR {
ODR_32KHZ = 0x01,
ODR_16KHZ = 0x02,
ODR_8KHZ = 0x03,
ODR_4KHZ = 0x04,
ODR_2KHZ = 0x05,
ODR_1KHZ = 0x06,
ODR_200HZ = 0x07,
ODR_100HZ = 0x08,
ODR_50HZ = 0x09,
ODR_25HZ = 0x0A,
ODR_12_5HZ = 0x0B,
ODR_500HZ = 0x0F
};
ICM42688(TwoWire &bus, uint8_t address);
int begin();
int readSensor();
float getAccelX_mss();
float getAccelY_mss();
float getAccelZ_mss();
float getGyroX_rads();
float getGyroY_rads();
float getGyroZ_rads();
float getGyroX_dps();
float getGyroY_dps();
float getGyroZ_dps();
float getTemperature_C();
private:
TwoWire *_bus;
uint8_t _address;
float _accelScale;
float _gyroScale;
const float _tempScale = 333.87f;
const float _tempOffset = 21.0f;
const float _G = 9.807f;
const float _d2r = 3.14159265359f/180.0f;
int16_t _ax, _ay, _az;
int16_t _gx, _gy, _gz;
int16_t _t;
void writeRegister(uint8_t reg, uint8_t data);
uint8_t readRegister(uint8_t reg);
void readRegisters(uint8_t reg, uint8_t count, uint8_t *dest);
};
#endif
// --- END OF ICM42688.h ---

60
ESP32S3/camera_pins.h Normal file
View File

@@ -0,0 +1,60 @@
#if defined(CAMERA_MODEL_XIAO_ESP32S3)
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 10
#define SIOD_GPIO_NUM 40
#define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 11
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 16
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 17
#define Y2_GPIO_NUM 15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM 47
#define PCLK_GPIO_NUM 13
#elif defined(CAMERA_MODEL_AI_THINKER)
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
#elif defined(CAMERA_MODEL_M5STACK_PSRAM)
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM 15
#define XCLK_GPIO_NUM 27
#define SIOD_GPIO_NUM 25
#define SIOC_GPIO_NUM 23
#define Y9_GPIO_NUM 19
#define Y8_GPIO_NUM 36
#define Y7_GPIO_NUM 18
#define Y6_GPIO_NUM 39
#define Y5_GPIO_NUM 5
#define Y4_GPIO_NUM 34
#define Y3_GPIO_NUM 35
#define Y2_GPIO_NUM 32
#define VSYNC_GPIO_NUM 22
#define HREF_GPIO_NUM 26
#define PCLK_GPIO_NUM 21
#else
#error "Camera model not selected"
#endif

1014
ESP32S3/compile.ino Normal file

File diff suppressed because it is too large Load Diff

346
README.md
View File

@@ -1,2 +1,346 @@
# NaviGlassFirmware # Avaota F1 开发与集成
> 面向仓库根目录的总览文档,整合 Day1Day9 日志、任务清单与完成总结。
---
## 1. 项目简介
本项目面向 **Avaota F1 AI 机器人**,基于 **全志 V821 / 32-bit RISCV** SoC目标是在 Tina Linux 固件上实现一套「可量产」的机器人终端固件,集成:
- 音频采集板载模拟麦克风与播放I2S + MAX98357A
- IMU 六轴姿态传感ICM42688P
- GC2083 MIPI 摄像头 JPEG 图像采集
- UDP / HTTP / WebSocket 网络通讯
- 多线程主程序与静态链接交叉编译流程
文档与代码组织按「Day1Day9 开发日志 + 任务清单 + 完成总结」推进,可作为以后移植到其他 Tina Linux / 全志平台的模板工程。
目前对话的位置是本地Windows 11系统的主机用于开发的环境是局域网中的Ubuntu服务器。
---
## 2. 硬件与开发环境
### 2.1 目标硬件(板端)
- **SoC**:全志 V82132bit RISCV 架构
- **板载外设**
- 模拟麦克风 → 内置 Audio Codecaudiocodec
- I2S 数字功放MAX98357ABCLK/LRCK/DOUTPD12/PD13/PD15
- 摄像头GC2083MIPICSI2典型输出 1280×720 @ 20fps
- IMUICM42688PGPIO 模拟 SPI
- **操作系统**Tina Linux基于 OpenWrt 的 Allwinner SDK
### 2.2 主机开发环境PC
- **系统**Ubuntu 24.04 LTS同时也有 Windows + WSL 的混合开发)
- **工具链 & SDK**
- Tina SDK`tina-v821-release`
- `/home/rongye/ProgramFiles/AvaotaF1/avaota_sdk/tina-v821-release` 这是SDK位置
- 交叉编译工具链:
- **当前使用**`prebuilt/rootfsbuilt/riscv/nds32le-linux-musl-v5d`**musl 工具链(与开发板兼容)**
- 编译器前缀:`riscv32-linux-musl-`
- ~~已废弃~~`out/toolchain/nds32le-linux-glibc-v5d` (glibc 工具链,与板端 musl libc 1.2.4 不兼容)
- `/home/rongye/ProgramFiles/AvaotaF1/avaota_app_demo` 这是交叉编译的位置
- Python通过 `python-is-python3` 或为遗留脚本装 python2 并软链到 `python`
### 2.3 开发主机准备要点Ubuntu 24.04
- 开启 i386 架构以兼容旧版 32 位库
- 手动安装被移除的 `libncurses5` / `libtinfo5`
- 安装 bison / flex / 交叉编译依赖,修复 `lunch` / `make` 时报错
- 注意 `make -j` 一定要限制并行度(如 `-j8`),避免 OOM
---
## 3. 仓库结构建议
实际仓库结构可按以下思路组织(示例):
```text
.
├── ../Docs/DevLogs/ # (位于上级目录) 每日开发日志
│ ├── Day1.md … Day18.md
├── docs/ # 本地硬件/SDK文档
│ ├── 1.png … 2.png
│ ├── AvaotaF1.md
│ ├── 引脚.md
│ ├── TinaSDK-Docs/
│ ├── tina_files_clean.csv
├── ESP32S3/ # 可参考的ESP32S3固件
├── src/
│ ├── audio/ # AudioCapture / AudioPlayer
│ ├── camera/ # Camera 类 / MPP 封装
│ ├── imu/ # ICM42688 SPI 驱动与测试
│ ├── network/ # UDP / HTTP / WebSocket 客户端
│ ├── utils/
│ ├── main.cpp # 主程序入口(多线程集成)
│ ├── main_test.cpp # 本地硬件自检程序
│ ├── Makefile # 交叉编译配置
│ └── build_*.sh # 构建脚本main/test/phaseX
├── build_main.sh
├── logs.md # 编译时的日志(实时填写)
└── README.md # 本文档
```
你可以直接把 Day1Day7、任务清单与完成总结放到 `../docs/DevLogs/` 目录,并用当前 README 作为入口索引。
---
## 4. 功能模块概览
### 4.1 音频系统
**输入:板载模拟麦克风**
- 通过 SoC 内部 **Audio Codec (audiocodec)** 采集
- ALSA 设备:`hw:audiocodec``hw:0,0`(录音推荐 `plughw:0,0`
- 运行时配置:
- `MIC Switch` 开启
- `MIC Gain` 建议值 25在音量与底噪之间平衡
- `adc-vol``lineout-gain` 使用 DTS 默认值或适度调整
- 采样参数:
- 16 kHz, S16_LE, Mono
- 典型链路:
- `setup_mic.sh``arecord``/tmp/test_mic.wav``aplay` / 上行网络
**输出I2S + MAX98357A**
- I2S0 接 MAX98357A扬声器输出
- Device Tree 为每个引脚创建独立 pinctrl 节点:
- `i2s0_bclk_pin`PD12
- `i2s0_lrck_pin`PD13
- `i2s0_dout0_pin`PD15
- I2S 平台设备 `&i2s0_plat` 绑定这些引脚,`status = "okay"`
- 通过 ALSA `aplay` / 自己的 `AudioPlayer` 类进行播放
### 4.2 IMU 传感器ICM42688P
- 最终采用 **GPIO 模拟 SPI**
- SCLK: PD3
- MOSI: PD2
- MISO: PD4
- CS : PD5
- SPI Mode0软件 bitbang速率约 500 kHz可按需提升
- 主要特性:
- WHO_AM_I = 0x47 识别验证
- 加速度计±16g1 kHz ODR
- 陀螺仪±2000 °/s1 kHz ODR
- 温度通道:用于环境监控与漂移补偿
- 静止状态验证:
- 合加速度 ≈ 9.8 m/s²
- 陀螺仪接近 0 °/s
- 上层封装:
- `ICM42688` 类提供采样与单位转换
- 独立 `test_imu` 自检程序
- 在主程序中通过 UDP 周期性上报 JSON/结构体数据
### 4.3 摄像头系统GC2083 + MPP
- 使用 Allwinner EyeseeMPP 框架SYS → VI → ISP → VENC
- 流水线:
- VI接 MIPI 摄像头)
- ISP自动曝光、白平衡、降噪
- VENCJPEG 编码)
- 关键配置:
- 分辨率1280×720 @ 20fps可调
- JPEG 质量80在质量与码率之间折中
- VI Buffer5 帧
- VBV Buffer4 MB避免 `VBV FULL` 错误
- `Camera` 类职责:
- 完成 MPP 初始化 / 绑定 / 销毁
- 按需抓拍单帧 JPEG用于 WebSocket 发送或本地保存)
- 提供阻塞式 `capture_frame()` 接口与重试机制
- 测试程序:
- `test_camera` 抓拍多张 JPEG 保存到 SD 卡
- 实测成功率 100%,文件大小 2080 KB 之间,画面曝光正常
### 4.4 网络通讯
**UDP**
- 轻量 IMU 数据上报通道
- 使用 POSIX socket静态链接无额外依赖
- 典型用法:
- `UDPSender` 初始化目标 IP/Port
- 周期性发送 IMU/状态数据
**HTTPlibcurl 或轻量封装)**
- 主要用途:
- 从服务器拉取 TTS 音频流(如 `stream.wav`
- 拉取配置文件或诊断信息
- 流式下载接口已预留,可边下边写入 `AudioPlayer` 播放
**WebSocket**
- 早期方案SDK 内置 `libuwsc`(后期为规避依赖,可用自实现轻量 WS 客户端)
- 主要用途:
- 上行:摄像头 JPEG 帧、音频 PCM 片段
- 下行:控制指令 / 状态同步
- 接口设计:
- `WSClient` 负责 TCP 连接、握手、帧收发、心跳与重连
- 主线程中,每个子模块持有各自的 WSClient 实例或共享连接
---
## 5. 编译与构建流程
### 5.1 Tina SDK 环境准备(概要)
1. 解压 SDK
```bash
mkdir -p ~/ProgramFiles/avaota_sdk
tar -xvf tina-v821-*.tar.xz -C ~/ProgramFiles/avaota_sdk
```
2. 初始化环境:
```bash
cd ~/ProgramFiles/avaota_sdk/tina-v821-release
source build/envsetup.sh
lunch # 选择 avaota_f1 / v821 相关配置
```
3. 全量编译:
```bash
make -j8
pack # 需要时打包固件
```
### 5.2 应用程序交叉编译
在 `src/` 目录中提供一个或多个构建脚本,例如:
```bash
#!/bin/bash
set -e
SDK_ROOT=~/ProgramFiles/AvaotaF1/avaota_sdk/tina-v821-release
# ⚠️ 使用 musl 工具链(与开发板兼容)
TOOLCHAIN=${SDK_ROOT}/prebuilt/rootfsbuilt/riscv/nds32le-linux-musl-v5d/bin
export PATH=${TOOLCHAIN}:$PATH
make clean
make all -j4 # 或按目标拆分make main / make test_audio / make test_imu
```
Makefile 要点:
- 使用 `riscv32-linux-musl-g++` 编译器musl 工具链)
- 动态链接器:`/lib32/ld.so.1`(板端需创建符号链接:`ln -s /lib32/ilp32d/libc.so /lib32/ld.so.1`
- 链接静态或最小依赖的动态库
- 注意链接顺序:业务库 → MPP/ISP/cedarx → `-lpthread -lrt -lm -ldl -lstdc++`
### 5.3 部署与运行
1. 将交叉编译好的可执行文件拷贝到 SD 卡:
```bash
cp avaota_client /media/$USER/SDCARD/
```
2. 板端挂载 SD 卡并复制到 `/tmp`
```bash
mount /dev/mmcblk0p1 /mnt/extsd
cp /mnt/extsd/avaota_client /tmp/
chmod +x /tmp/avaota_client
```
3. 运行:
```bash
/tmp/avaota_client
```
> 推荐所有测试程序(`test_audio` / `test_imu` / `test_camera` / `avaota_test`)都统一走上述流程,避免执行权限与只读分区问题。
---
## 6. 主程序(集成)架构示意
主程序建议使用多线程模型,将各个模块解耦:
```cpp
// 伪代码示例
int main() {
init_logging();
init_network_config(); // 服务器 IP / 端口 / 路径等
init_signal_handler(); // Ctrl+C 安全退出
std::thread cam_thread(run_camera_loop);
std::thread mic_thread(run_audio_capture_loop);
std::thread spk_thread(run_audio_play_loop);
std::thread imu_thread(run_imu_udp_loop);
// 可选:主线程处理下行指令 / 状态机
run_main_control_loop();
// 等待退出
cam_thread.join();
mic_thread.join();
spk_thread.join();
imu_thread.join();
return 0;
}
```
每个线程内部:
- 初始化各自的硬件和网络客户端
- 进入循环:
- 采集 → 编码(可选) → 通过 UDP/WS 发送
- 或从 HTTP/WS 接收 → 解码(可选) → 播放/控制硬件
- 捕获异常并尝试重连 / 恢复,必要时上报主线程
---
## 7. 日志与文档索引
可以在 README 中给出所有开发日志与计划文档的入口,方便回溯:
- `../Docs/DevLogs/Day1.md`SDK 编译与 32 位环境踩坑
- `../Docs/DevLogs/Day2.md`UDP 通信、网络库编译、libuwsc / libcurl 准备
- `../Docs/DevLogs/Day3.md`:音频采集 & 播放模块ALSA + I2S
- `../Docs/DevLogs/Day4.md`:模拟麦克风配置 + IMU SPI 驱动与验证
- `../Docs/DevLogs/Day5.md`GC2083 摄像头 MPP 集成与 JPEG 捕获
- `../Docs/DevLogs/Day6.md`:硬件全功能验证、本地测试程序、网络库诊断
- `../Docs/DevLogs/Day7.md`:交叉编译 Makefile 收敛、工具链配置
- `../Docs/DevLogs/Day8.md`整体编译成功、Cedar 库链接完成
- `../Docs/DevLogs/Day9.md`:⭐ **musl 工具链修复 + 板上测试通过**
- `../Docs/task_complete.md`完整任务清单与进度条97% 完成)
- `../Docs/implementation_plan_complete.md`:实现计划 & 各阶段目标拆解
---
## 8. 任务完成度与后续工作
### 8.1 当前完成度(参考任务清单与总结)
- SDK/工具链/固件:✅
- musl 工具链修复:✅ **Day 9 关键突破)**
- 音频采集 & 播放:✅
- IMU 采集:✅ **(板上测试通过)**
- 摄像头采集:✅ **(板上测试通过)**
- UDP / HTTP / WebSocket 基础:✅(库与客户端实现已完成)
- WiFi 网络配置:✅ **192.168.110.132**
- 板端硬件测试:✅ **(所有模块通过)**
- 网络服务器通信测试:⏳(服务器已部署,待测试)
- 性能评估与稳定性验证:⏳(待完成)
整体项目 **97%** 完成,核心功能全部验证通过!🎉
### 8.2 建议的后续 TODO
- 将服务器 IP / 端口、音频参数、帧率等抽象为配置文件JSON / INI
- 丰富错误日志并加上日志轮转
- 对 WebSocket / HTTP 加入重连与指数退避策略
- 若后续量产,考虑:
- 用硬件 SPI 替换 GPIO bitbang
- 对功耗与休眠策略进行优化
- 引入简单看门狗机制,防止长期运行卡死
---
## 9. 如何使用本 README
- **新成员上手**:从本 README 入手,结合 `../Docs/DevLogs/DayX.md` 逐步了解每个子系统的设计与坑点。
- **以后复用到新项目**:可以直接复制「编译流程」「外设接线 + DTS 配置」「主程序多线程架构」这几部分,稍作修改即可移植到其他全志 / RISCV 设备。
- **代码导航入口**:按模块查找 `src/audio`, `src/imu`, `src/camera`, `src/network` 目录,结合对应的 DayX 日志阅读。
祝使用愉快 🎉,也欢迎在后续开发阶段继续补充和更新本 README。

View File

@@ -0,0 +1,375 @@
# Musl 工具链编译说明
**更新时间**: 2025-12-04
**问题**: 开发板使用 musl libc但最初用 glibc 工具链编译导致无法运行
**解决**: 切换到 musl 工具链重新编译
---
## 📋 问题诊断过程
### 在开发板上的错误
```bash
root@(none):/# ldd /tmp/avaota_client
/bin/sh: /tmp/avaota_client: not found
root@(none):/# /lib/ld-musl-riscv32.so.1 /tmp/avaota_client
Error loading shared library ld-linux-riscv32-ilp32d.so.1: No such file or directory
Error relocating /tmp/avaota_client: __register_atfork: symbol not found
```
### 开发板环境确认
```bash
root@(none):/# /lib32/ilp32d/libc.so
musl libc (riscv32)
Version 1.2.4
Dynamic Program Loader
```
**结论**: 开发板运行 **musl libc 1.2.4**,但程序用 **glibc** 编译,导致:
1. 动态链接器路径不匹配(`ld-linux-riscv32-ilp32d.so.1` vs `ld-musl-riscv32.so.1`
2. glibc 特有符号 `__register_atfork` 在 musl 中不存在
---
## ✅ 解决方案
### 已修改的文件
修改了 `src/Makefile`,添加 musl 工具链支持:
```makefile
# 切换工具链:设置 USE_MUSL=1 使用 musl 工具链(推荐)
USE_MUSL := 1
ifeq ($(USE_MUSL),1)
# musl 工具链(与开发板兼容)
TOOLCHAIN_DIR := $(SDK_ROOT)/out/toolchain/nds32le-linux-musl-v5d/bin
CROSS_COMPILE := riscv32-linux-musl-
$(info [INFO] Using musl toolchain for board compatibility)
else
# glibc 工具链(仅用于对比测试,开发板不支持)
TOOLCHAIN_DIR := $(SDK_ROOT)/out/toolchain/nds32le-linux-glibc-v5d/bin
CROSS_COMPILE := riscv32-unknown-linux-
$(warning [WARNING] Using glibc toolchain - will NOT run on board!)
endif
```
---
## 🚀 编译步骤(在 Ubuntu 服务器上)
### 前提条件
确保 musl 工具链存在:
```bash
cd ~/ProgramFiles/AvaotaF1/avaota_sdk/tina-v821-release
# 检查 musl 工具链
ls -la out/toolchain/nds32le-linux-musl-v5d/bin/
# 应该看到类似这些文件:
# riscv32-linux-musl-gcc
# riscv32-linux-musl-g++
# riscv32-linux-musl-ar
# ...
```
**如果不存在**,可能需要:
1. 解压工具链压缩包(如果在 `out/toolchain/` 下有 `.tar.xz``.tar.gz` 文件)
2. 或在 `prebuilt/rootfsbuilt/riscv/` 下查找
3. 或重新编译 SDK会自动生成
---
### 步骤 1: 上传修改后的代码
将修改后的 `avaota_app_demo` 文件夹上传到 Ubuntu 服务器:
```bash
# 在 Windows 上(使用 SCP 或 WinSCP
# 目标路径:/home/rongye/ProgramFiles/AvaotaF1/avaota_app_demo
```
---
### 步骤 2: SSH 连接到服务器
```bash
ssh rongye@<服务器IP>
```
---
### 步骤 3: 验证工具链路径
```bash
cd ~/ProgramFiles/AvaotaF1/avaota_app_demo/src
# 检查 Makefile 变量
grep "TOOLCHAIN_DIR" Makefile
grep "CROSS_COMPILE" Makefile
grep "USE_MUSL" Makefile
# 测试编译器
SDK_ROOT=~/ProgramFiles/AvaotaF1/avaota_sdk/tina-v821-release
$SDK_ROOT/out/toolchain/nds32le-linux-musl-v5d/bin/riscv32-linux-musl-gcc --version
```
**预期输出**
```
riscv32-linux-musl-gcc (GCC) 10.x.x
```
---
### 步骤 4: 清理旧编译文件
```bash
cd ~/ProgramFiles/AvaotaF1/avaota_app_demo/src
make clean
```
---
### 步骤 5: 重新编译
```bash
cd ~/ProgramFiles/AvaotaF1/avaota_app_demo
# 方法 1: 使用构建脚本
./build_main.sh
# 方法 2: 直接 make
cd src
make all -j4
```
编译时应该看到:
```
[INFO] Using musl toolchain for board compatibility
```
---
### 步骤 6: 验证编译结果
```bash
cd ~/ProgramFiles/AvaotaF1/avaota_app_demo
# 查看文件信息
ls -lh build/bin/avaota_client
file build/bin/avaota_client
# 【关键】检查动态链接器
strings build/bin/avaota_client | grep "/lib/ld-"
```
**正确结果应该是**
```
/lib/ld-musl-riscv32.so.1
```
**如果看到这个就是错的**(说明还在用 glibc
```
/lib/ld-linux-riscv32-ilp32d.so.1
```
---
### 步骤 7: 上传到开发板
```bash
# 从服务器上传到开发板
cd ~/ProgramFiles/AvaotaF1/avaota_app_demo
scp build/bin/avaota_client root@<开发板IP>:/tmp/avaota_client_musl
```
---
### 步骤 8: 在开发板上测试
SSH 到开发板:
```bash
ssh root@<开发板IP>
# 添加执行权限
chmod +x /tmp/avaota_client_musl
# 验证依赖(现在应该正常显示)
ldd /tmp/avaota_client_musl
# 运行程序
/tmp/avaota_client_musl
```
**成功标志**
-`ldd` 不再报 "not found" 错误
- ✅ 能看到需要的动态库列表
- ✅ 程序启动,打印初始化日志
- ✅ 各模块开始工作
---
## 🔧 故障排查
### 问题 1: 找不到 musl 工具链
**错误**
```bash
make: /home/rongye/.../nds32le-linux-musl-v5d/bin/riscv32-linux-musl-gcc: No such file or directory
```
**解决方案 A**: 查找并解压
```bash
cd ~/ProgramFiles/AvaotaF1/avaota_sdk/tina-v821-release
# 查找压缩包
find . -name "*musl*.tar*" 2>/dev/null
# 如果找到,解压到 out/toolchain/
cd out/tool chain
tar -xJf nds32le-linux-musl-v5d.tar.xz # 或 .tar.gz
```
**解决方案 B**: 使用其他位置的工具链
如果工具链在 `prebuilt/` 目录,修改 Makefile
```makefile
TOOLCHAIN_DIR := $(SDK_ROOT)/prebuilt/rootfsbuilt/riscv/nds32le-linux-musl-v5d/bin
```
**解决方案 C**: 临时切换回 glibc
如果实在找不到 musl 工具链,可以暂时用 glibc 重新编译(虽然不推荐):
```makefile
USE_MUSL := 0
```
但这样编译的程序**无法在开发板上运行**。
---
### 问题 2: 编译器前缀不对
**错误**
```
make: riscv32-linux-musl-gcc: command not found
```
**解决方案**:
检查实际的编译器名称:
```bash
ls ~/ProgramFiles/AvaotaF1/avaota_sdk/tina-v821-release/out/toolchain/nds32le-linux-musl-v5d/bin/riscv32-*gcc
```
可能的前缀:
- `riscv32-linux-musl-`
- `riscv32-unknown-linux-musl-`
修改 Makefile 中的 `CROSS_COMPILE` 变量。
---
### 问题 3: 编译时缺少头文件
**错误**
```
fatal error: xxx.h: No such file or directory
```
**原因**: musl 的系统头文件路径可能与 glibc 不同
**解决方案**:
在 Makefile 中添加 musl 头文件路径:
```makefile
ifeq ($(USE_MUSL),1)
CXXFLAGS += -I$(TOOLCHAIN_DIR)/../include
CXXFLAGS += -I$(TOOLCHAIN_DIR)/../riscv32-linux-musl/include
endif
```
---
### 问题 4: 链接时找不到库
**错误**
```
cannot find -lxxx
```
**解决方案**:
检查库是否存在于 musl 环境:
```bash
find ~/ProgramFiles/AvaotaF1/avaota_sdk/tina-v821-release/out/v821/avaota_f1/openwrt/staging_dir/target/usr/lib -name "libxxx.a"
```
如果不存在,可能需要:
1. 重新编译 SDK 以包含 musl 版本的库
2. 或移除对该库的依赖
---
## 📊 工具链对比
| 项目 | glibc 工具链 | musl 工具链 |
|------|-------------|-------------|
| 路径 | `nds32le-linux-glibc-v5d` | `nds32le-linux-musl-v5d` |
| 编译器前缀 | `riscv32-unknown-linux-` | `riscv32-linux-musl-` |
| C 库 | glibc (GNU C Library) | musl libc 1.2.4 |
| 动态链接器 | `/lib/ld-linux-riscv32-ilp32d.so.1` | `/lib/ld-musl-riscv32.so.1` |
| 二进制大小 | 较大 | 较小 |
| 兼容性 | ❌ 不兼容 Avaota F1 | ✅ 完美兼容 |
| 用途 | 仅用于实验 | **生产环境必须使用** |
---
## ✅ 编译验证清单
重新编译完成后,验证以下内容:
- [ ] musl 工具链路径存在
- [ ] Makefile 中 `USE_MUSL := 1`
- [ ] `make clean` 清理旧文件
- [ ] 编译时看到 "Using musl toolchain" 提示
- [ ] 编译成功,生成 `build/bin/avaota_client`
- [ ] `strings` 检查链接器为 `ld-musl-riscv32.so.1`
- [ ] 上传到开发板
- [ ] `ldd` 正常显示依赖
- [ ] 程序成功运行
---
## 📝 后续优化
如果后续需要调整(比如完全静态链接),可以修改:
```makefile
# 完全静态链接(不依赖任何动态库)
LDFLAGS := -static -static-libgcc -static-libstdc++
# 移除 -Wl,-Bdynamic 部分
# LDFLAGS += -Wl,-Bdynamic -lasound -lglog ...
# 全部静态链接
LDFLAGS += -lasound -lglog -llog -lpthread -lm -lstdc++ -lrt -ldl -lz
```
这样可以避免任何运行时库依赖问题,但会显著增加程序大小(可能从 3.9MB 增加到 6-8MB
---
**下一步**: 按照上述步骤在服务器上重新编译,然后在开发板上测试!🚀

168
avaota_app_demo/README.md Normal file
View File

@@ -0,0 +1,168 @@
# Avaota F1 应用程序编译
**更新时间**: 2025-12-04
**重要**: 必须使用 **musl 工具链**编译,开发板使用 musl libc 1.2.4
---
## 🚨 重要提示
开发板运行的是 **musl libc 1.2.4**,不是 glibc
-**正确**: 使用 `nds32le-linux-musl-v5d` 工具链
-**错误**: 使用 `nds32le-linux-glibc-v5d` 工具链(编译的程序无法运行)
---
## 📋 快速编译步骤
### 1. 准备 Tina SDK
将 Tina SDK 放置到以下路径:
```
/home/rongye/ProgramFiles/AvaotaF1/avaota_sdk/tina-v821-release
```
> 如需修改路径,请同时更新 `src/Makefile` 和 `build_main.sh` 中的 `SDK_ROOT` 变量
### 2. 上传代码到服务器
`avaota_app_demo` 文件夹上传到:
```
/home/rongye/ProgramFiles/AvaotaF1/avaota_app_demo
```
### 3. SSH 连接到服务器
```bash
ssh rongye@<服务器IP>
```
### 4. 验证 Makefile 配置
```bash
cd ~/ProgramFiles/AvaotaF1/avaota_app_demo/src
grep "USE_MUSL" Makefile
```
应该看到:
```makefile
USE_MUSL := 1
```
### 5. 清理并编译
```bash
cd ~/ProgramFiles/AvaotaF1/avaota_app_demo
# 清理
cd src
make clean
# 编译
cd ..
./build_main.sh
```
### 5. 验证编译结果
```bash
# 检查动态链接器(关键!)
strings build/bin/avaota_client | grep "/lib/ld-"
```
**正确结果**
```
/lib/ld-musl-riscv32.so.1
```
**错误结果**(如果看到这个说明还在用 glibc
```
/lib/ld-linux-riscv32-ilp32d.so.1
```
### 6. 上传到开发板
```bash
scp build/bin/avaota_client root@<开发板IP>:/tmp/avaota_client_musl
```
### 7. 在开发板上运行
```bash
ssh root@<开发板IP>
chmod +x /tmp/avaota_client_musl
ldd /tmp/avaota_client_musl # 应该正常显示依赖
/tmp/avaota_client_musl # 运行程序
```
---
## 📁 文件说明
```
avaota_app_demo/
├── src/ # 源代码目录
│ ├── main.cpp # 主程序
│ ├── audio/ # 音频模块
│ ├── camera/ # 摄像头模块
│ ├── imu/ # IMU 传感器
│ ├── network/ # 网络通信
│ ├── utils/ # 工具函数
│ └── Makefile # 编译配置(已设置 USE_MUSL=1
├── build_main.sh # 构建脚本
├── MUSL_COMPILE.md # 详细编译指南
└── README.md # 本文件
```
---
## 🔧 如果遇到问题
### 问题 1: 找不到 musl 工具链
**错误**:
```
make: /home/rongye/.../nds32le-linux-musl-v5d/bin/riscv32-linux-musl-gcc: No such file or directory
```
**解决**:
```bash
# 查找工具链
cd ~/ProgramFiles/AvaotaF1/avaota_sdk/tina-v821-release
find . -name "*musl*.tar*" 2>/dev/null
# 如果找到压缩包,解压
cd out/toolchain
tar -xJf nds32le-linux-musl-v5d.tar.xz
```
详细故障排查请参考 [MUSL_COMPILE.md](MUSL_COMPILE.md)
---
## 📚 相关文档
- **[MUSL_COMPILE.md](MUSL_COMPILE.md)** - 完整的 musl 工具链编译指南
- **[../../docs/Day9.md](../../docs/Day9.md)** - Day 9 开发日志(问题发现过程)
- **[../../docs/board_test_checklist.md](../../docs/board_test_checklist.md)** - 板上测试清单
- **[../../docs/musl_toolchain_fix.md](../../docs/musl_toolchain_fix.md)** - 工具链问题修复指南
---
## ✅ 编译验证清单
- [ ] 修改后的代码已上传到服务器
- [ ] Makefile 中 `USE_MUSL := 1`
- [ ] musl 工具链路径存在
- [ ] `make clean` 清理完成
- [ ] 编译成功,生成 `build/bin/avaota_client`
- [ ] `strings` 检查链接器为 `ld-musl-riscv32.so.1`
- [ ] 程序已上传到开发板
- [ ] `ldd` 正常显示依赖
- [ ] 程序成功运行
---
**祝编译顺利!** 🚀

View File

@@ -0,0 +1,113 @@
#!/bin/bash
# 编译 AvaotaF1 客户端主程序
# 【开发者注意】请将 Tina SDK 放置到 SDK_ROOT 指定的路径
set -e # 错误时退出
# SDK 路径配置
# 【开发者注意】请确保 SDK 位于此路径,或修改此变量
SDK_ROOT="/home/rongye/ProgramFiles/TinaSDK/tina-v821-release"
echo "========================================="
echo "AvaotaF1 Client Build Script"
echo "========================================="
echo ""
# 检查 SDK 路径
if [ ! -d "$SDK_ROOT" ]; then
echo "错误SDK 路径不存在: $SDK_ROOT"
echo "请将 Tina SDK 放置到上述路径,或修改脚本中的 SDK_ROOT 变量"
exit 1
fi
# 保存项目目录在切换到SDK前
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 1. 进入 SDK 目录并设置环境
echo "1. 设置编译环境..."
cd "$SDK_ROOT"
source build/envsetup.sh 2>&1 || true # 忽略 envsetup.sh 的无害警告
# 2. 检查必需的库
echo ""
echo "2. 检查依赖库..."
STAGING_DIR="$SDK_ROOT/out/v821/avaota_f1/openwrt/staging_dir/target/usr/lib"
check_lib() {
local lib_name=$1
if [ -f "$STAGING_DIR/$lib_name" ]; then
echo "$lib_name 存在"
return 0
else
echo "$lib_name 缺失"
return 1
fi
}
all_libs_ok=true
check_lib "libssl.so" || all_libs_ok=false
check_lib "libcrypto.so" || all_libs_ok=false
check_lib "libcurl.so" || all_libs_ok=false
check_lib "libasound.so" || all_libs_ok=false
if [ "$all_libs_ok" = false ]; then
echo ""
echo "警告:部分库缺失,可能导致链接失败"
echo "建议在 SDK 中运行 'make menuconfig' 启用相应的库"
echo ""
fi
# 3. 清理并编译
echo ""
echo "3. 开始编译..."
echo ""
echo "项目目录: $PROJECT_DIR"
# 检查 src 目录是否存在
if [ ! -d "$PROJECT_DIR/src" ]; then
echo "错误:找不到 src 目录"
echo "当前路径:$PROJECT_DIR"
echo "请确认脚本在项目根目录执行"
exit 1
fi
# 切换到项目src目录
cd "$PROJECT_DIR/src"
# 清理旧文件
echo "清理旧的编译文件..."
make clean
# 编译主程序
echo ""
echo "编译 avaota_client..."
make all -j$(nproc)
# 4. 检查编译结果
echo ""
echo "========================================="
if [ -f "../build/bin/avaota_client" ]; then
echo "✅ 编译成功!"
echo "========================================="
echo ""
echo "输出文件:"
echo " ../build/bin/avaota_client"
echo ""
# 文件信息
file ../build/bin/avaota_client
ls -lh ../build/bin/avaota_client
echo ""
echo "下一步:"
echo " 1. 将程序上传到板子:"
echo " scp ../build/bin/avaota_client root@<板子IP>:/usr/bin/"
echo " 2. 在板子上运行:"
echo " avaota_client"
else
echo "❌ 编译失败"
echo "========================================="
exit 1
fi

View File

@@ -0,0 +1,267 @@
# AvaotaF1 客户端 Makefile
# 交叉编译配置 - 32位 RISC-V
# ===== 工具链配置 =====
# 开发板使用 musl libc 1.2.4,必须用 musl 工具链编译
# 【开发者注意】请将 Tina SDK 放置到以下路径,或修改此变量
SDK_ROOT := /home/rongye/ProgramFiles/TinaSDK/tina-v821-release
# 切换工具链:设置 USE_MUSL=1 使用 musl 工具链(推荐)
USE_MUSL := 1
ifeq ($(USE_MUSL),1)
# musl 工具链(与开发板兼容)
# 注意musl 工具链在 prebuilt 目录,不是 out/toolchain
TOOLCHAIN_DIR := $(SDK_ROOT)/prebuilt/rootfsbuilt/riscv/nds32le-linux-musl-v5d/bin
CROSS_COMPILE := riscv32-linux-musl-
$(info [INFO] Using musl toolchain for board compatibility)
else
# glibc 工具链(仅用于对比测试,开发板不支持)
TOOLCHAIN_DIR := $(SDK_ROOT)/out/toolchain/nds32le-linux-glibc-v5d/bin
CROSS_COMPILE := riscv32-unknown-linux-
$(warning [WARNING] Using glibc toolchain - will NOT run on board!)
endif
CC := $(TOOLCHAIN_DIR)/$(CROSS_COMPILE)gcc
CXX := $(TOOLCHAIN_DIR)/$(CROSS_COMPILE)g++
AR := $(TOOLCHAIN_DIR)/$(CROSS_COMPILE)ar
STRIP := $(TOOLCHAIN_DIR)/$(CROSS_COMPILE)strip
# ===== 目标配置 =====
TARGET := avaota_client
SRC_DIR := .
BUILD_DIR := ../build
OBJ_DIR := $(BUILD_DIR)/obj
BIN_DIR := $(BUILD_DIR)/bin
# ===== 源文件 =====
SRCS := $(SRC_DIR)/main.cpp \
$(SRC_DIR)/camera/camera.cpp \
$(SRC_DIR)/audio/audio_capture.cpp \
$(SRC_DIR)/audio/audio_player.cpp \
$(SRC_DIR)/imu/icm42688.cpp \
$(SRC_DIR)/network/ws_client.cpp \
$(SRC_DIR)/network/udp_sender.cpp \
$(SRC_DIR)/utils/logger.cpp
# Day 14: 移除 HTTP TTS 相关文件,改用 WebSocket TTS
OBJS := $(patsubst $(SRC_DIR)/%.cpp, $(OBJ_DIR)/%.o, $(SRCS))
# ===== 编译选项 =====
CXXFLAGS := -std=c++14 -O2 -Wall -Wextra
CXXFLAGS += -I$(SRC_DIR)
# 系统头文件
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include
# eyesee-mpp 头文件
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/eyesee-mpp/middleware/include
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/eyesee-mpp/middleware/include/media
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/eyesee-mpp/middleware/include/utils
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/eyesee-mpp/middleware/media/include/component
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/eyesee-mpp/system/public/include
# libisp 头文件
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/libisp
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/libisp/include
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/libisp/include/device
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/libisp/include/V4l2Camera
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/libisp/isp_dev
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/libisp/isp_tuning
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/libisp/isp_manage
# libcedarc 头文件 (视频编解码器 - 添加所有子目录)
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/libcedarc
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/libcedarc/include
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/libcedarc/base
CXXFLAGS += -I$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/include/libcedarc/base/include
# 定义芯片类型
CXXFLAGS += -DAWCHIP=AW_V821
# ===== 库路径与链接 =====
# 使用静态链接(与 test_camera 相同的配置)
LDFLAGS := -static
LDFLAGS += -L$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/lib
LDFLAGS += -L$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/lib/eyesee-mpp
# 开始静态库组 - 顺序很重要!
LDFLAGS += -Wl,--start-group
# 核心 MPP 库
LDFLAGS += -law_mpp -lmedia_utils -lawion -lexpat
# ISP 库 (摄像头图像处理)
LDFLAGS += -lISP -lisp_dev -lisp_ini -liniparser
LDFLAGS += -lisp_ae -lisp_af -lisp_afs -lisp_awb -lisp_base
LDFLAGS += -lisp_gtm -lisp_iso -lisp_math -lisp_md -lisp_pltm -lisp_rolloff
# 视频编码器
LDFLAGS += -lvencoder -lvenc_base -lvenc_common -lvenc_jpeg -lvenc_h264
LDFLAGS += -lMemAdapter -lVE
# 音频处理库
LDFLAGS += -ladecoder -lResample -lAudioVps
LDFLAGS += -lwav -laac
LDFLAGS += -lcedarx_aencoder -laacenc
LDFLAGS += -lAgc -lAec -lAns
# Muxer (文件封装)
LDFLAGS += -lmuxers -lmp4_muxer -lraw_muxer
LDFLAGS += -lmpeg2ts_muxer -laac_muxer -lmp3_muxer -lwav_muxer
LDFLAGS += -lffavutil -lFsWriter -lcedarxstream
# Demuxer (文件解析)
LDFLAGS += -lcedarxdemuxer -lcdx_parser -lcdx_stream -lcdx_file_stream
LDFLAGS += -lcdx_aac_parser -lcdx_id3v2_parser -lcdx_mov_parser
LDFLAGS += -lcdx_mp3_parser -lcdx_ts_parser -lcdx_wav_parser
# 视频解码器 (MJPEG)
LDFLAGS += -lvdecoder -lvideoengine -lawmjpegplus
LDFLAGS += -lcedarx_tencoder
# 配置文件解析
LDFLAGS += -lPluginMpp -lIniParserMpp -lsample_confparser
# Cedar 核心库包含CDC_LOG_LEVEL_NAME等符号
LDFLAGS += -lcdc_base -lcdx_base
# 显示库
LDFLAGS += -lcedarxrender -lhwdisplay
# ALSA 音频库(静态库不存在,且音频播放已禁用)
# LDFLAGS += -lasound
# OpenSSL (用于 WebSocket 握手)
LDFLAGS += -lssl -lcrypto
# 结束静态库组
LDFLAGS += -Wl,--end-group
# 系统动态库
# 指定正确的动态链接器路径,避免开发板上需要手动创建 /lib32/ld.so.1 符号链接
LDFLAGS += -Wl,--dynamic-linker=/lib/ld-musl-riscv32.so.1
LDFLAGS += -Wl,-Bdynamic -lasound -lpthread -lm -lrt -ldl -lz -static-libstdc++
# ===== 目标规则 =====
.PHONY: all clean install test_imu test_gpio test_camera
all: $(BIN_DIR)/$(TARGET)
$(BIN_DIR)/$(TARGET): $(OBJS)
@mkdir -p $(BIN_DIR)
$(CXX) $(OBJS) $(LDFLAGS) -o $@
@echo "Build complete: $@"
@echo "Strip binary..."
$(STRIP) $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -rf $(BUILD_DIR)
@echo "Clean complete"
install:
@echo "Installing to target device..."
scp $(BIN_DIR)/$(TARGET) root@192.168.1.100:/usr/bin/
@echo "Install complete"
# ===== 测试程序 =====
# test_imu 只需要基本库
TEST_IMU_LDFLAGS := -static -lpthread -lm -lstdc++
test_imu: $(OBJ_DIR)/test_imu.o $(OBJ_DIR)/imu/icm42688.o $(OBJ_DIR)/utils/logger.o
@mkdir -p $(BIN_DIR)
$(CXX) $^ $(TEST_IMU_LDFLAGS) -o $(BIN_DIR)/$@
@echo "Build complete: $(BIN_DIR)/$@"
@echo "Strip binary..."
$(STRIP) $(BIN_DIR)/$@
# test_gpio 测试 GPIO 输出
test_gpio: $(OBJ_DIR)/test_gpio.o
@mkdir -p $(BIN_DIR)
$(CXX) $^ $(TEST_IMU_LDFLAGS) -o $(BIN_DIR)/$@
@echo "Build complete: $(BIN_DIR)/$@"
@echo "Strip binary..."
$(STRIP) $(BIN_DIR)/$@
# test_camera 测试摄像头 (需要 MPP 库)
TEST_CAMERA_LDFLAGS := -static
TEST_CAMERA_LDFLAGS += -L$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/lib
TEST_CAMERA_LDFLAGS += -L$(SDK_ROOT)/out/v821/avaota_f1/openwrt/staging_dir/target/usr/lib/eyesee-mpp
# 静态库组 - 顺序很重要!
TEST_CAMERA_LDFLAGS += -Wl,--start-group
# 核心 MPP 库
TEST_CAMERA_LDFLAGS += -law_mpp -lmedia_utils -lawion -lexpat
# ISP 库 (摄像头图像处理)
TEST_CAMERA_LDFLAGS += -lISP -lisp_dev -lisp_ini -liniparser
TEST_CAMERA_LDFLAGS += -lisp_ae -lisp_af -lisp_afs -lisp_awb -lisp_base
TEST_CAMERA_LDFLAGS += -lisp_gtm -lisp_iso -lisp_math -lisp_md -lisp_pltm -lisp_rolloff
# 视频编码器
TEST_CAMERA_LDFLAGS += -lvencoder -lvenc_base -lvenc_common -lvenc_jpeg -lvenc_h264
TEST_CAMERA_LDFLAGS += -lMemAdapter -lVE
# 音频处理库
TEST_CAMERA_LDFLAGS += -ladecoder -lResample -lAudioVps
TEST_CAMERA_LDFLAGS += -lwav -laac
TEST_CAMERA_LDFLAGS += -lcedarx_aencoder -laacenc
TEST_CAMERA_LDFLAGS += -lAgc -lAec -lAns
# Muxer (文件封装)
TEST_CAMERA_LDFLAGS += -lmuxers -lmp4_muxer -lraw_muxer
TEST_CAMERA_LDFLAGS += -lmpeg2ts_muxer -laac_muxer -lmp3_muxer -lwav_muxer
TEST_CAMERA_LDFLAGS += -lffavutil -lFsWriter -lcedarxstream
# Demuxer (文件解析)
TEST_CAMERA_LDFLAGS += -lcedarxdemuxer -lcdx_parser -lcdx_stream -lcdx_file_stream
TEST_CAMERA_LDFLAGS += -lcdx_aac_parser -lcdx_id3v2_parser -lcdx_mov_parser
TEST_CAMERA_LDFLAGS += -lcdx_mp3_parser -lcdx_ts_parser -lcdx_wav_parser
# 视频解码器 (MJPEG)
TEST_CAMERA_LDFLAGS += -lvdecoder -lvideoengine -lawmjpegplus
TEST_CAMERA_LDFLAGS += -lcedarx_tencoder
# 配置文件解析
TEST_CAMERA_LDFLAGS += -lPluginMpp -lIniParserMpp -lsample_confparser
# Cedar 核心库
TEST_CAMERA_LDFLAGS += -lcdc_base -lcdx_base
# 显示库
TEST_CAMERA_LDFLAGS += -lcedarxrender -lhwdisplay
TEST_CAMERA_LDFLAGS += -Wl,--end-group
# 系统动态库
TEST_CAMERA_LDFLAGS += -Wl,-Bdynamic -lasound -lglog -llog -lpthread -lm -lstdc++ -lrt -ldl -lz
test_camera: $(OBJ_DIR)/test_camera.o $(OBJ_DIR)/camera/camera.o
@mkdir -p $(BIN_DIR)
$(CXX) $^ $(TEST_CAMERA_LDFLAGS) -o $(BIN_DIR)/$@
@echo "Build complete: $(BIN_DIR)/$@"
@echo "Strip binary..."
$(STRIP) $(BIN_DIR)/$@
# ===== 帮助 =====
help:
@echo "AvaotaF1 Client Makefile"
@echo "------------------------"
@echo "Targets:"
@echo " all - Build the application (default)"
@echo " clean - Remove build artifacts"
@echo " install - Deploy to device via SCP"
@echo " test_imu - Build IMU test program"
@echo " test_gpio - Build GPIO test program"
@echo " test_camera - Build camera test program"
@echo ""
@echo "Before build, ensure:"
@echo " 1. SDK_ROOT points to Tina SDK"
@echo " 2. Toolchain is available"
@echo " 3. SDK has been compiled"

View File

@@ -0,0 +1,357 @@
/**
* @file audio_capture.cpp
* @brief 音频采集实现 - 基于 ALSA
* @date 2024-11-24
* @platform Avaota F1 (V821 / RISC-V)
*/
#include "audio_capture.h"
#include "../utils/logger.h"
#include <cstring>
#include <errno.h>
/**
* @brief 构造函数
*/
AudioCapture::AudioCapture(const std::string& device, int sample_rate, int channels)
: m_device(device)
, m_sample_rate(sample_rate)
, m_channels(channels)
, m_pcm_handle(nullptr)
{
}
/**
* @brief 析构函数
*/
AudioCapture::~AudioCapture() {
if (m_pcm_handle) {
snd_pcm_drain(m_pcm_handle);
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
LOG_INFO("[AudioCapture] Device closed");
}
}
/**
* @brief 配置混音器以启用麦克风输入
* @param card_name 声卡名称,如 "hw:0"
* @return true 如果成功
*/
bool AudioCapture::setup_mixer(const char* card_name) {
snd_mixer_t *mixer = nullptr;
snd_mixer_selem_id_t *sid = nullptr;
int err;
// 打开混音器
err = snd_mixer_open(&mixer, 0);
if (err < 0) {
LOG_WARN("[AudioCapture] Cannot open mixer: %s", snd_strerror(err));
return false;
}
// 附加到声卡
err = snd_mixer_attach(mixer, card_name);
if (err < 0) {
LOG_WARN("[AudioCapture] Cannot attach mixer to %s: %s", card_name, snd_strerror(err));
snd_mixer_close(mixer);
return false;
}
// 注册混音器
err = snd_mixer_selem_register(mixer, NULL, NULL);
if (err < 0) {
LOG_WARN("[AudioCapture] Cannot register mixer: %s", snd_strerror(err));
snd_mixer_close(mixer);
return false;
}
// 加载混音器元素
err = snd_mixer_load(mixer);
if (err < 0) {
LOG_WARN("[AudioCapture] Cannot load mixer: %s", snd_strerror(err));
snd_mixer_close(mixer);
return false;
}
LOG_INFO("[AudioCapture] Mixer opened successfully, scanning controls...");
// 遍历所有混音器元素并打印信息
snd_mixer_selem_id_alloca(&sid);
snd_mixer_elem_t *elem;
int control_count = 0;
for (elem = snd_mixer_first_elem(mixer); elem; elem = snd_mixer_elem_next(elem)) {
if (!snd_mixer_selem_is_active(elem)) continue;
snd_mixer_selem_get_id(elem, sid);
const char *name = snd_mixer_selem_id_get_name(sid);
control_count++;
// 检查是否有捕获能力
bool has_capture = snd_mixer_selem_has_capture_volume(elem) ||
snd_mixer_selem_has_capture_switch(elem);
if (has_capture) {
LOG_INFO("[AudioCapture] Found capture control: '%s'", name);
// 尝试启用捕获开关
if (snd_mixer_selem_has_capture_switch(elem)) {
snd_mixer_selem_set_capture_switch_all(elem, 1);
LOG_INFO("[AudioCapture] Enabled capture switch for '%s'", name);
}
// 尝试设置捕获音量到最大
if (snd_mixer_selem_has_capture_volume(elem)) {
long min, max;
snd_mixer_selem_get_capture_volume_range(elem, &min, &max);
snd_mixer_selem_set_capture_volume_all(elem, max);
LOG_INFO("[AudioCapture] Set capture volume to max (%ld) for '%s'", max, name);
}
}
// 尝试匹配常见的麦克风相关控制项
if (strstr(name, "MIC") || strstr(name, "Mic") || strstr(name, "mic") ||
strstr(name, "ADC") || strstr(name, "Capture") || strstr(name, "Input") ||
strstr(name, "Line") || strstr(name, "LINEIN")) {
LOG_INFO("[AudioCapture] Found potential mic control: '%s'", name);
// Day 23 fix: Restore playback volume to MAX for ADC/LINEOUT.
// On some codecs (e.g. AC108/ES8388), "Playback Volume" on ADC actually controls
// the digital gain of the ADC signal *before* it splits to capture/loopback.
// Setting it to 0 silences the capture. We will deal with echo via software if needed.
if (snd_mixer_selem_has_playback_volume(elem)) {
long min, max;
snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
// Day 23: Set to 80% to balance capture signal vs loopback noise
long target = min + (max - min) * 0.8;
snd_mixer_selem_set_playback_volume_all(elem, target);
LOG_INFO("[AudioCapture] Set playback volume to 80%% (%ld/%ld) for '%s'", target, max, name);
}
// Enable playback switch (to ensure hardware is powered/active)
if (snd_mixer_selem_has_playback_switch(elem)) {
snd_mixer_selem_set_playback_switch_all(elem, 1); // Enable
LOG_INFO("[AudioCapture] Enabled playback switch for '%s' (required for capture)", name);
}
}
}
LOG_INFO("[AudioCapture] Scanned %d mixer controls", control_count);
snd_mixer_close(mixer);
return true;
}
/**
* @brief 初始化 ALSA 采集设备
*/
bool AudioCapture::init() {
int err;
// 0. 首先配置混音器,启用麦克风输入
LOG_INFO("[AudioCapture] Setting up mixer for microphone...");
setup_mixer("hw:0");
// 1. 打开 PCM 设备 (CAPTURE 模式,非阻塞)
// 使用 SND_PCM_NONBLOCK 避免 read 阻塞
err = snd_pcm_open(&m_pcm_handle, m_device.c_str(),
SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
if (err < 0) {
LOG_ERROR("[AudioCapture] Cannot open device '%s': %s",
m_device.c_str(), snd_strerror(err));
return false;
}
LOG_INFO("[AudioCapture] Opened device: %s (non-blocking)", m_device.c_str());
// 2. 分配硬件参数对象
snd_pcm_hw_params_t* hw_params;
snd_pcm_hw_params_alloca(&hw_params);
// 3. 初始化硬件参数
err = snd_pcm_hw_params_any(m_pcm_handle, hw_params);
if (err < 0) {
LOG_ERROR("[AudioCapture] Cannot initialize hw params: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
// 4. 设置访问模式 (交织模式)
err = snd_pcm_hw_params_set_access(m_pcm_handle, hw_params,
SND_PCM_ACCESS_RW_INTERLEAVED);
if (err < 0) {
LOG_ERROR("[AudioCapture] Cannot set access type: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
// 5. 设置采样格式 (16-bit signed little-endian)
err = snd_pcm_hw_params_set_format(m_pcm_handle, hw_params,
SND_PCM_FORMAT_S16_LE);
if (err < 0) {
LOG_ERROR("[AudioCapture] Cannot set sample format: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
// 6. 设置采样率
unsigned int actual_rate = m_sample_rate;
err = snd_pcm_hw_params_set_rate_near(m_pcm_handle, hw_params,
&actual_rate, 0);
if (err < 0) {
LOG_ERROR("[AudioCapture] Cannot set sample rate: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
if (actual_rate != (unsigned int)m_sample_rate) {
LOG_WARN("[AudioCapture] Sample rate %d Hz not supported, using %d Hz",
m_sample_rate, actual_rate);
}
// 7. 设置声道数
err = snd_pcm_hw_params_set_channels(m_pcm_handle, hw_params, m_channels);
if (err < 0) {
LOG_ERROR("[AudioCapture] Cannot set channel count: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
// 8. 设置缓冲区大小 (增大以提高稳定性)
// Period: 480 frames (30ms @ 16kHz)
// Buffer: 4800 frames (300ms @ 16kHz)
// Day 22 优化: 使用20ms包(320 samples)与服务器ASR期望一致
snd_pcm_uframes_t period_size = 320; // 20ms @ 16kHz = 320 samples
snd_pcm_uframes_t buffer_size = 4800;
err = snd_pcm_hw_params_set_period_size_near(m_pcm_handle, hw_params,
&period_size, 0);
if (err < 0) {
LOG_WARN("[AudioCapture] Cannot set period size: %s", snd_strerror(err));
}
err = snd_pcm_hw_params_set_buffer_size_near(m_pcm_handle, hw_params,
&buffer_size);
if (err < 0) {
LOG_WARN("[AudioCapture] Cannot set buffer size: %s", snd_strerror(err));
}
// 9. 应用硬件参数
err = snd_pcm_hw_params(m_pcm_handle, hw_params);
if (err < 0) {
LOG_ERROR("[AudioCapture] Cannot apply hw params: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
// 10. 准备设备
err = snd_pcm_prepare(m_pcm_handle);
if (err < 0) {
LOG_ERROR("[AudioCapture] Cannot prepare device: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
// 11. 启动捕获流 (非阻塞模式必须显式启动)
err = snd_pcm_start(m_pcm_handle);
if (err < 0) {
LOG_ERROR("[AudioCapture] Cannot start capture: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
LOG_INFO("[AudioCapture] Initialized: %d Hz, %d channels, S16_LE",
actual_rate, m_channels);
LOG_INFO("[AudioCapture] Period: %lu frames, Buffer: %lu frames",
period_size, buffer_size);
LOG_INFO("[AudioCapture] Capture stream started");
return true;
}
/**
* @brief 读取 PCM 数据
* @param buffer 输出缓冲区 (int16_t 数组)
* @param frames 需要读取的帧数
* @return 实际读取的帧数,<0 表示错误
*/
snd_pcm_sframes_t AudioCapture::read(int16_t* buffer, snd_pcm_uframes_t frames) {
if (!m_pcm_handle) {
LOG_ERROR("[AudioCapture] Device not initialized");
return -1;
}
snd_pcm_sframes_t frames_read = snd_pcm_readi(m_pcm_handle, buffer, frames);
if (frames_read < 0) {
// 错误处理
if (frames_read == -EAGAIN) {
// 非阻塞模式:没有可用数据,返回 0 让调用者等待
return 0;
} else if (frames_read == -EPIPE) {
// Overrun (缓冲区溢出)
LOG_WARN("[AudioCapture] Overrun occurred, recovering...");
snd_pcm_prepare(m_pcm_handle);
return 0; // 本次读取失败,下次重试
} else if (frames_read == -ESTRPIPE) {
// Suspend (设备挂起)
LOG_WARN("[AudioCapture] Device suspended, resuming...");
while ((frames_read = snd_pcm_resume(m_pcm_handle)) == -EAGAIN) {
usleep(100000); // 等待 100ms
}
if (frames_read < 0) {
// 恢复失败,重新准备
frames_read = snd_pcm_prepare(m_pcm_handle);
if (frames_read < 0) {
LOG_ERROR("[AudioCapture] Cannot recover from suspend: %s",
snd_strerror(frames_read));
return frames_read;
}
}
return 0;
} else {
// 其他错误
LOG_ERROR("[AudioCapture] Read error: %s", snd_strerror(frames_read));
// 尝试自动恢复
int err = snd_pcm_recover(m_pcm_handle, frames_read, 0);
if (err < 0) {
LOG_ERROR("[AudioCapture] Cannot recover: %s, attempting to reinitialize...", snd_strerror(err));
// 关闭设备
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
// 等待一下再重新初始化
usleep(500000); // 500ms
// 重新初始化
if (!init()) {
LOG_ERROR("[AudioCapture] Failed to reinitialize device");
return -1; // 彻底失败
}
LOG_INFO("[AudioCapture] Device reinitialized successfully");
return 0; // 本次读取失败,但设备已恢复
}
return 0;
}
}
// 成功读取
if (frames_read != (snd_pcm_sframes_t)frames) {
LOG_DEBUG("[AudioCapture] Short read: expected %lu, got %ld frames",
frames, frames_read);
}
return frames_read;
}

View File

@@ -0,0 +1,49 @@
/**
* @file audio_capture.h
* @brief 音频采集封装 - 基于 ALSA
*/
#ifndef AUDIO_CAPTURE_H
#define AUDIO_CAPTURE_H
#include <alsa/asoundlib.h>
#include <string>
#include <cstdint>
class AudioCapture {
public:
/**
* @brief 构造函数
* @param device ALSA 设备名,如 "hw:0,0" 或 "default"
* @param sample_rate 采样率 (Hz)
* @param channels 声道数 (1=单声道, 2=立体声)
*/
AudioCapture(const std::string& device, int sample_rate, int channels);
~AudioCapture();
/**
* @brief 初始化 ALSA 采集设备
* @return true 成功, false 失败
*/
bool init();
/**
* @brief 读取 PCM 数据
* @param buffer 输出缓冲区 (int16_t 数组)
* @param frames 帧数 (1帧 = channels * 2字节)
* @return 实际读取的帧数,<0 表示错误
*/
snd_pcm_sframes_t read(int16_t* buffer, snd_pcm_uframes_t frames);
private:
std::string m_device;
int m_sample_rate;
int m_channels;
snd_pcm_t* m_pcm_handle;
// 配置混音器以启用麦克风输入
bool setup_mixer(const char* card_name);
};
#endif // AUDIO_CAPTURE_H

View File

@@ -0,0 +1,327 @@
/**
* @file audio_player.cpp
* @brief 音频播放实现 - 基于 ALSA
* @date 2024-11-24
* @platform Avaota F1 (V821 / RISC-V)
*/
#include "audio_player.h"
#include "../utils/logger.h"
#include <cstring>
#include <errno.h>
/**
* @brief 构造函数
*/
AudioPlayer::AudioPlayer(const std::string& device, int sample_rate, int channels)
: m_device(device)
, m_sample_rate(sample_rate)
, m_channels(channels)
, m_pcm_handle(nullptr)
{
}
/**
* @brief 配置混音器以启用扬声器输出
*/
bool AudioPlayer::setup_mixer(const char* card_name) {
snd_mixer_t *mixer = nullptr;
snd_mixer_selem_id_t *sid = nullptr;
int err;
// 打开混音器
err = snd_mixer_open(&mixer, 0);
if (err < 0) {
LOG_WARN("[AudioPlayer] Cannot open mixer: %s", snd_strerror(err));
return false;
}
// 附加到声卡
err = snd_mixer_attach(mixer, card_name);
if (err < 0) {
LOG_WARN("[AudioPlayer] Cannot attach mixer to %s: %s", card_name, snd_strerror(err));
snd_mixer_close(mixer);
return false;
}
// 注册混音器
err = snd_mixer_selem_register(mixer, NULL, NULL);
if (err < 0) {
LOG_WARN("[AudioPlayer] Cannot register mixer: %s", snd_strerror(err));
snd_mixer_close(mixer);
return false;
}
// 加载混音器元素
err = snd_mixer_load(mixer);
if (err < 0) {
LOG_WARN("[AudioPlayer] Cannot load mixer: %s", snd_strerror(err));
snd_mixer_close(mixer);
return false;
}
LOG_INFO("[AudioPlayer] Mixer opened, scanning controls for playback...");
snd_mixer_elem_t *elem;
for (elem = snd_mixer_first_elem(mixer); elem; elem = snd_mixer_elem_next(elem)) {
if (!snd_mixer_selem_is_active(elem)) continue;
snd_mixer_selem_id_alloca(&sid);
snd_mixer_selem_get_id(elem, sid);
const char *name = snd_mixer_selem_id_get_name(sid);
if (snd_mixer_selem_has_playback_volume(elem)) {
long min, max;
snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
// Day 22: 跳过 ADC (麦克风) 的 playback volume 设置
// 避免覆盖 AudioCapture 中设置的静音逻辑,防止严重的回环噪声
if (strstr(name, "ADC") || strstr(name, "Input") || strstr(name, "Capture")) {
LOG_INFO("[AudioPlayer] Skipping playback volume for '%s' (handled by AudioCapture)", name);
} else {
snd_mixer_selem_set_playback_volume_all(elem, max);
LOG_INFO("[AudioPlayer] Unmuted playback volume for '%s'", name);
}
}
if (snd_mixer_selem_has_playback_switch(elem)) {
// Day 22 修复: 只禁用 loopback debug 开关
// 注意:禁用 MIC playback switch 会导致麦克风无数据 (Day 12 发现)
std::string switch_name(name);
// 检查是否是 loopback 开关
bool is_loopback = (switch_name.find("loopback") != std::string::npos) ||
(switch_name.find("Loopback") != std::string::npos) ||
(switch_name.find("LOOPBACK") != std::string::npos);
// Day 23 fix: User confirmed disabling "MIC Playback Switch" kills the microphone capture.
// We MUST enable it. Loopback/echo will be handled by other means or accepted for now.
if (is_loopback) {
snd_mixer_selem_set_playback_switch_all(elem, 0); // 禁用
LOG_INFO("[AudioPlayer] DISABLED playback switch for '%s' (noise reduction)", name);
} else {
snd_mixer_selem_set_playback_switch_all(elem, 1); // 启用
LOG_INFO("[AudioPlayer] Enabled playback switch for '%s'", name);
}
}
}
snd_mixer_close(mixer);
return true;
}
/**
* @brief 析构函数
*/
AudioPlayer::~AudioPlayer() {
if (m_pcm_handle) {
snd_pcm_drain(m_pcm_handle);
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
LOG_INFO("[AudioPlayer] Device closed");
}
}
/**
* @brief 初始化 ALSA 播放设备
*/
bool AudioPlayer::init() {
int err;
// 0. 配置混音器
// 尝试配置 hw:1 (可能是 I2S 接口) 和 hw:0 (可能是 Codec)
setup_mixer("hw:1");
// 很多板子的扬声器音量其实还是得在 Codec (hw:0) 上调
if (m_device.find("hw:1") != std::string::npos) {
setup_mixer("hw:0");
}
// 1. 打开 PCM 设备 (PLAYBACK 模式)
err = snd_pcm_open(&m_pcm_handle, m_device.c_str(),
SND_PCM_STREAM_PLAYBACK, 0);
if (err < 0) {
LOG_ERROR("[AudioPlayer] Cannot open device '%s': %s",
m_device.c_str(), snd_strerror(err));
return false;
}
LOG_INFO("[AudioPlayer] Opened device: %s", m_device.c_str());
// 2. 分配硬件参数对象
snd_pcm_hw_params_t* hw_params;
snd_pcm_hw_params_alloca(&hw_params);
// 3. 初始化硬件参数
err = snd_pcm_hw_params_any(m_pcm_handle, hw_params);
if (err < 0) {
LOG_ERROR("[AudioPlayer] Cannot initialize hw params: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
// 4. 设置访问模式 (交织模式)
err = snd_pcm_hw_params_set_access(m_pcm_handle, hw_params,
SND_PCM_ACCESS_RW_INTERLEAVED);
if (err < 0) {
LOG_ERROR("[AudioPlayer] Cannot set access type: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
// 5. 设置采样格式 (16-bit signed little-endian)
err = snd_pcm_hw_params_set_format(m_pcm_handle, hw_params,
SND_PCM_FORMAT_S16_LE);
if (err < 0) {
LOG_ERROR("[AudioPlayer] Cannot set sample format: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
// 6. 设置采样率
unsigned int actual_rate = m_sample_rate;
err = snd_pcm_hw_params_set_rate_near(m_pcm_handle, hw_params,
&actual_rate, 0);
if (err < 0) {
LOG_ERROR("[AudioPlayer] Cannot set sample rate: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
if (actual_rate != (unsigned int)m_sample_rate) {
LOG_WARN("[AudioPlayer] Sample rate %d Hz not supported, using %d Hz",
m_sample_rate, actual_rate);
}
// 7. 设置声道数
err = snd_pcm_hw_params_set_channels(m_pcm_handle, hw_params, m_channels);
if (err < 0) {
LOG_ERROR("[AudioPlayer] Cannot set channel count: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
// 8. 设置缓冲区大小 (可选,优化延迟)
// Period: 160 frames (10ms @ 16kHz)
// Buffer: 1600 frames (100ms @ 16kHz)
snd_pcm_uframes_t period_size = 160;
snd_pcm_uframes_t buffer_size = 1600;
err = snd_pcm_hw_params_set_period_size_near(m_pcm_handle, hw_params,
&period_size, 0);
if (err < 0) {
LOG_WARN("[AudioPlayer] Cannot set period size: %s", snd_strerror(err));
}
err = snd_pcm_hw_params_set_buffer_size_near(m_pcm_handle, hw_params,
&buffer_size);
if (err < 0) {
LOG_WARN("[AudioPlayer] Cannot set buffer size: %s", snd_strerror(err));
}
// 9. 应用硬件参数
err = snd_pcm_hw_params(m_pcm_handle, hw_params);
if (err < 0) {
LOG_ERROR("[AudioPlayer] Cannot apply hw params: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
// 10. 准备设备
err = snd_pcm_prepare(m_pcm_handle);
if (err < 0) {
LOG_ERROR("[AudioPlayer] Cannot prepare device: %s", snd_strerror(err));
snd_pcm_close(m_pcm_handle);
m_pcm_handle = nullptr;
return false;
}
LOG_INFO("[AudioPlayer] Initialized: %d Hz, %d channels, S16_LE",
actual_rate, m_channels);
LOG_INFO("[AudioPlayer] Period: %lu frames, Buffer: %lu frames",
period_size, buffer_size);
return true;
}
/**
* @brief 写入 PCM 数据到扬声器
* @param buffer 输入缓冲区 (int16_t 数组)
* @param frames 需要写入的帧数
* @return 实际写入的帧数,<0 表示错误
*/
snd_pcm_sframes_t AudioPlayer::write(const int16_t* buffer, snd_pcm_uframes_t frames) {
if (!m_pcm_handle) {
LOG_ERROR("[AudioPlayer] Device not initialized");
return -1;
}
snd_pcm_sframes_t frames_written = snd_pcm_writei(m_pcm_handle, buffer, frames);
if (frames_written < 0) {
// 错误处理
if (frames_written == -EPIPE) {
// Underrun (缓冲区欠载)
LOG_WARN("[AudioPlayer] Underrun occurred, recovering...");
snd_pcm_prepare(m_pcm_handle);
// 重试写入
frames_written = snd_pcm_writei(m_pcm_handle, buffer, frames);
if (frames_written < 0) {
LOG_ERROR("[AudioPlayer] Write failed after underrun recovery");
return frames_written;
}
} else if (frames_written == -ESTRPIPE) {
// Suspend (设备挂起)
LOG_WARN("[AudioPlayer] Device suspended, resuming...");
while ((frames_written = snd_pcm_resume(m_pcm_handle)) == -EAGAIN) {
usleep(100000); // 等待 100ms
}
if (frames_written < 0) {
// 恢复失败,重新准备
frames_written = snd_pcm_prepare(m_pcm_handle);
if (frames_written < 0) {
LOG_ERROR("[AudioPlayer] Cannot recover from suspend: %s",
snd_strerror(frames_written));
return frames_written;
}
}
// 重试写入
frames_written = snd_pcm_writei(m_pcm_handle, buffer, frames);
if (frames_written < 0) {
LOG_ERROR("[AudioPlayer] Write failed after suspend recovery");
return frames_written;
}
} else {
// 其他错误
LOG_ERROR("[AudioPlayer] Write error: %s", snd_strerror(frames_written));
// 尝试自动恢复
int err = snd_pcm_recover(m_pcm_handle, frames_written, 0);
if (err < 0) {
LOG_ERROR("[AudioPlayer] Cannot recover: %s", snd_strerror(err));
return frames_written;
}
// 重试写入
frames_written = snd_pcm_writei(m_pcm_handle, buffer, frames);
if (frames_written < 0) {
LOG_ERROR("[AudioPlayer] Write failed after generic recovery");
return frames_written;
}
}
}
// 成功写入
if (frames_written != (snd_pcm_sframes_t)frames) {
LOG_DEBUG("[AudioPlayer] Short write: expected %lu, wrote %ld frames",
frames, frames_written);
}
return frames_written;
}

View File

@@ -0,0 +1,49 @@
/**
* @file audio_player.h
* @brief 音频播放封装 - 基于 ALSA
*/
#ifndef AUDIO_PLAYER_H
#define AUDIO_PLAYER_H
#include <alsa/asoundlib.h>
#include <string>
#include <cstdint>
class AudioPlayer {
public:
/**
* @brief 构造函数
* @param device ALSA 设备名,如 "hw:0,0" 或 "default"
* @param sample_rate 采样率 (Hz)
* @param channels 声道数 (1=单声道, 2=立体声)
*/
AudioPlayer(const std::string& device, int sample_rate, int channels);
~AudioPlayer();
/**
* @brief 初始化 ALSA 播放设备
* @return true 成功, false 失败
*/
bool init();
/**
* @brief 写入 PCM 数据到扬声器
* @param buffer 输入缓冲区 (int16_t 数组)
* @param frames 帧数 (1帧 = channels * 2字节)
* @return 实际写入的帧数,<0 表示错误
*/
snd_pcm_sframes_t write(const int16_t* buffer, snd_pcm_uframes_t frames);
private:
std::string m_device;
int m_sample_rate;
int m_channels;
snd_pcm_t* m_pcm_handle;
// 私有方法: 配置混音器
bool setup_mixer(const char* card_name);
};
#endif // AUDIO_PLAYER_H

View File

@@ -0,0 +1,425 @@
/**
* @file camera.cpp
* @brief GC2083 摄像头驱动实现 - 基于全志 MPP 框架
*
* 架构: VI (Video Input) → ISP → VENC (JPEG Encoder)
* 模式: 绑定模式 (自动流转,高性能)
*/
#include "camera.h"
#include <cstring>
#include <ctime>
#include <sys/time.h>
#include <unistd.h>
#include <stdio.h>
extern "C" {
#include <mpi_sys.h>
#include <mm_common.h>
#include <mpi_videoformat_conversion.h>
}
// 日志宏
#define LOG_TAG "Camera"
#define LOGD(fmt, ...) printf("[%s][D] " fmt "\n", LOG_TAG, ##__VA_ARGS__)
#define LOGI(fmt, ...) printf("[%s][I] " fmt "\n", LOG_TAG, ##__VA_ARGS__)
#define LOGW(fmt, ...) printf("[%s][W] " fmt "\n", LOG_TAG, ##__VA_ARGS__)
#define LOGE(fmt, ...) printf("[%s][E] " fmt "\n", LOG_TAG, ##__VA_ARGS__)
// 默认配置 (Day 13: 降低负载减少网络拥堵)
#define DEFAULT_WIDTH 640 // Day 13: 从1280降低到640
#define DEFAULT_HEIGHT 480 // Day 13: 从720降低到480
// Day 22 优化: 户外稳定模式适应4G手机热点网络波动
#define DEFAULT_FPS 8 // 8fps 平衡流畅与带宽
#define DEFAULT_QUALITY 45 // Qfactor=45适度压缩节省带宽
#define VI_BUFFER_NUM 5 // 增加缓冲区数量避免溢出 (Day 11)
#define VBV_BUFFER_SIZE 2048 // Day 13: 降低到2MB640x480不需要4MB
// GC2083 配置
#define VIPP_DEV_ID 0 // sensor0 (GC2083)
#define ISP_DEV_ID 0
#define VI_CHN_ID 0
#define VENC_CHN_START 0
Camera::Camera()
: m_vi_dev(VIPP_DEV_ID)
, m_vi_chn(VI_CHN_ID)
, m_isp_dev(ISP_DEV_ID)
, m_venc_chn(MM_INVALID_CHN)
, m_width(DEFAULT_WIDTH)
, m_height(DEFAULT_HEIGHT)
, m_quality(DEFAULT_QUALITY)
, m_fps(0.0f)
, m_frame_count(0)
, m_last_time(0)
{
LOGD("Camera constructor");
}
Camera::~Camera()
{
LOGD("Camera destructor");
// 停止编码器
if (m_venc_chn != MM_INVALID_CHN) {
AW_MPI_VENC_StopRecvPic(m_venc_chn);
AW_MPI_VENC_DestroyChn(m_venc_chn);
}
// 停止 VI
AW_MPI_VI_DisableVirChn(m_vi_dev, m_vi_chn);
AW_MPI_VI_DestroyVirChn(m_vi_dev, m_vi_chn);
AW_MPI_VI_DisableVipp(m_vi_dev);
// 停止 ISP
AW_MPI_ISP_Stop(m_isp_dev);
AW_MPI_VI_DestroyVipp(m_vi_dev);
// 退出系统
AW_MPI_SYS_Exit();
}
void Camera::deinit()
{
LOGI("[Camera] Deinitializing for recovery...");
// 停止编码器
if (m_venc_chn != MM_INVALID_CHN) {
AW_MPI_VENC_StopRecvPic(m_venc_chn);
AW_MPI_VENC_DestroyChn(m_venc_chn);
m_venc_chn = MM_INVALID_CHN;
}
// 停止 VI
AW_MPI_VI_DisableVirChn(m_vi_dev, m_vi_chn);
AW_MPI_VI_DestroyVirChn(m_vi_dev, m_vi_chn);
AW_MPI_VI_DisableVipp(m_vi_dev);
// 停止 ISP
AW_MPI_ISP_Stop(m_isp_dev);
AW_MPI_VI_DestroyVipp(m_vi_dev);
// 退出系统
AW_MPI_SYS_Exit();
// 重置统计
m_fps = 0.0f;
m_frame_count = 0;
m_last_time = 0;
LOGI("[Camera] Deinitialized, ready for reinit");
}
bool Camera::init()
{
LOGI("Initializing camera: %dx%d @%dfps", m_width, m_height, DEFAULT_FPS);
// 1. 初始化 MPP 系统
MPP_SYS_CONF_S sys_conf;
memset(&sys_conf, 0, sizeof(MPP_SYS_CONF_S));
sys_conf.nAlignWidth = 32;
AW_MPI_SYS_SetConf(&sys_conf);
if (AW_MPI_SYS_Init() != SUCCESS) {
LOGE("AW_MPI_SYS_Init failed");
return false;
}
LOGD("MPP system initialized");
// 2. 创建 VI 设备 (初始化 sensor subdev)
LOGD("Creating VI device %d...", m_vi_dev);
ERRORTYPE ret = AW_MPI_VI_CreateVipp(m_vi_dev);
if (ret != SUCCESS) {
LOGE("AW_MPI_VI_CreateVipp failed: 0x%x", ret);
AW_MPI_SYS_Exit();
return false;
}
LOGD("VI device %d created successfully", m_vi_dev);
// 配置 VIPP 属性 (使用 LBC 2.5x 压缩)
LOGD("Configuring VI attributes: %dx%d @%dfps", m_width, m_height, DEFAULT_FPS);
VI_ATTR_S vi_attr;
memset(&vi_attr, 0, sizeof(VI_ATTR_S));
vi_attr.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
vi_attr.memtype = V4L2_MEMORY_MMAP;
vi_attr.format.pixelformat = map_PIXEL_FORMAT_E_to_V4L2_PIX_FMT(MM_PIXEL_FORMAT_YUV_AW_LBC_2_5X);
vi_attr.format.field = V4L2_FIELD_NONE;
vi_attr.format.colorspace = V4L2_COLORSPACE_REC709;
vi_attr.format.width = m_width;
vi_attr.format.height = m_height;
vi_attr.nbufs = VI_BUFFER_NUM;
vi_attr.nplanes = 2;
vi_attr.fps = DEFAULT_FPS;
vi_attr.use_current_win = 0;
vi_attr.wdr_mode = 0;
vi_attr.capturemode = V4L2_MODE_VIDEO;
vi_attr.drop_frame_num = 0;
if (AW_MPI_VI_SetVippAttr(m_vi_dev, &vi_attr) != SUCCESS) {
LOGE("AW_MPI_VI_SetVippAttr failed");
return false;
}
LOGD("VI device created and configured");
// 3. 启动 ISP (连接到 sensor)
if (!init_isp()) {
LOGE("init_isp failed");
return false;
}
// 4. 创建 VI 通道并启用
if (AW_MPI_VI_CreateVirChn(m_vi_dev, m_vi_chn, NULL) != SUCCESS) {
LOGE("AW_MPI_VI_CreateVirChn failed");
return false;
}
if (AW_MPI_VI_EnableVipp(m_vi_dev) != SUCCESS) {
LOGE("AW_MPI_VI_EnableVipp failed");
return false;
}
LOGD("VI channel created and enabled");
// 5. 初始化 VENC (Video Encoder)
if (!init_venc()) {
LOGE("init_venc failed");
return false;
}
// 6. 绑定 VI → VENC
MPP_CHN_S vi_chn = {MOD_ID_VIU, m_vi_dev, m_vi_chn};
MPP_CHN_S venc_chn = {MOD_ID_VENC, 0, m_venc_chn};
if (AW_MPI_SYS_Bind(&vi_chn, &venc_chn) != SUCCESS) {
LOGE("AW_MPI_SYS_Bind failed");
return false;
}
LOGD("VI-VENC bind successful");
// 7. 启动采集和编码
if (AW_MPI_VI_EnableVirChn(m_vi_dev, m_vi_chn) != SUCCESS) {
LOGE("AW_MPI_VI_EnableVirChn failed");
return false;
}
if (AW_MPI_VENC_StartRecvPic(m_venc_chn) != SUCCESS) {
LOGE("AW_MPI_VENC_StartRecvPic failed");
return false;
}
LOGI("Camera initialized successfully");
return true;
}
bool Camera::init_vi()
{
// VI 初始化已在 init() 中完成
return true;
}
bool Camera::init_isp()
{
LOGD("Initializing ISP...");
// 启动 ISP
if (AW_MPI_ISP_Run(m_isp_dev) != SUCCESS) {
LOGE("AW_MPI_ISP_Run failed");
return false;
}
LOGD("ISP running");
return true;
}
bool Camera::init_venc()
{
LOGD("Initializing VENC (JPEG)...");
// 使用固定的大VBV缓冲区 (4MB) 避免 VBV FULL 错误
unsigned int vbv_buf_size = VBV_BUFFER_SIZE * 1024; // 4MB
unsigned int vbv_thresh_size = m_width * m_height;
LOGD("VBV buffer size: %u KB (%u bytes)", VBV_BUFFER_SIZE, vbv_buf_size);
// 配置 VENC 通道属性
VENC_CHN_ATTR_S venc_attr;
memset(&venc_attr, 0, sizeof(VENC_CHN_ATTR_S));
venc_attr.VeAttr.Type = PT_JPEG;
venc_attr.VeAttr.mVippID = m_vi_dev;
venc_attr.VeAttr.MaxKeyInterval = 1;
venc_attr.VeAttr.SrcPicWidth = m_width;
venc_attr.VeAttr.SrcPicHeight = m_height;
venc_attr.VeAttr.Field = VIDEO_FIELD_FRAME;
venc_attr.VeAttr.PixelFormat = MM_PIXEL_FORMAT_YUV_AW_LBC_2_5X;
venc_attr.VeAttr.mColorSpace = V4L2_COLORSPACE_REC709;
venc_attr.VeAttr.Rotate = ROTATE_NONE;
// JPEG 特定配置
venc_attr.VeAttr.AttrJpeg.MaxPicWidth = 0;
venc_attr.VeAttr.AttrJpeg.MaxPicHeight = 0;
venc_attr.VeAttr.AttrJpeg.BufSize = vbv_buf_size;
venc_attr.VeAttr.AttrJpeg.mThreshSize = vbv_thresh_size;
venc_attr.VeAttr.AttrJpeg.bByFrame = TRUE;
venc_attr.VeAttr.AttrJpeg.PicWidth = m_width;
venc_attr.VeAttr.AttrJpeg.PicHeight = m_height;
venc_attr.VeAttr.AttrJpeg.bSupportDCF = FALSE;
// 创建编码通道
m_venc_chn = VENC_CHN_START;
bool success = false;
while (m_venc_chn < VENC_MAX_CHN_NUM) {
ERRORTYPE ret = AW_MPI_VENC_CreateChn(m_venc_chn, &venc_attr);
if (ret == SUCCESS) {
success = true;
break;
} else if (ret == ERR_VENC_EXIST) {
m_venc_chn++;
} else {
LOGE("AW_MPI_VENC_CreateChn failed: 0x%x", ret);
m_venc_chn++;
}
}
if (!success) {
LOGE("Failed to create VENC channel");
m_venc_chn = MM_INVALID_CHN;
return false;
}
// 设置 JPEG 质量
VENC_PARAM_JPEG_S jpeg_param;
memset(&jpeg_param, 0, sizeof(VENC_PARAM_JPEG_S));
jpeg_param.Qfactor = m_quality;
AW_MPI_VENC_SetJpegParam(m_venc_chn, &jpeg_param);
// 允许丢帧以防止VBV满时完全阻塞 (Day 11: 改为允许丢帧)
AW_MPI_VENC_ForbidDiscardingFrame(m_venc_chn, FALSE);
LOGD("VENC initialized: channel=%d, VBV=%uKB, quality=%d",
m_venc_chn, vbv_buf_size/1024, m_quality);
return true;
}
bool Camera::capture_frame(uint8_t** jpeg_data, size_t* jpeg_size)
{
if (!jpeg_data || !jpeg_size) {
LOGE("Invalid parameters");
return false;
}
// 获取编码流
VENC_STREAM_S stream;
VENC_PACK_S pack;
memset(&stream, 0, sizeof(VENC_STREAM_S));
memset(&pack, 0, sizeof(VENC_PACK_S));
stream.mpPack = &pack;
stream.mPackCount = 1;
// 增加超时时间到10秒并添加重试机制
const int max_retries = 3;
const int timeout_ms = 10000; // 10秒超时
ERRORTYPE ret = ERR_VENC_BUF_EMPTY;
for (int retry = 0; retry < max_retries && ret != SUCCESS; retry++) {
if (retry > 0) {
LOGW("Retry %d/%d getting stream...", retry, max_retries);
usleep(100000); // 100ms延迟再重试
}
ret = AW_MPI_VENC_GetStream(m_venc_chn, &stream, timeout_ms);
}
if (ret != SUCCESS) {
LOGE("AW_MPI_VENC_GetStream failed after %d retries: 0x%x", max_retries, ret);
return false;
}
// 计算总大小
size_t total_size = stream.mpPack[0].mLen0 + stream.mpPack[0].mLen1 + stream.mpPack[0].mLen2;
if (total_size == 0) {
LOGE("Empty JPEG frame");
AW_MPI_VENC_ReleaseStream(m_venc_chn, &stream);
return false;
}
// 分配内存并拷贝数据
uint8_t* buffer = new uint8_t[total_size];
size_t offset = 0;
if (stream.mpPack[0].mLen0 > 0 && stream.mpPack[0].mpAddr0) {
memcpy(buffer + offset, stream.mpPack[0].mpAddr0, stream.mpPack[0].mLen0);
offset += stream.mpPack[0].mLen0;
}
if (stream.mpPack[0].mLen1 > 0 && stream.mpPack[0].mpAddr1) {
memcpy(buffer + offset, stream.mpPack[0].mpAddr1, stream.mpPack[0].mLen1);
offset += stream.mpPack[0].mLen1;
}
if (stream.mpPack[0].mLen2 > 0 && stream.mpPack[0].mpAddr2) {
memcpy(buffer + offset, stream.mpPack[0].mpAddr2, stream.mpPack[0].mLen2);
offset += stream.mpPack[0].mLen2;
}
// 释放流
AW_MPI_VENC_ReleaseStream(m_venc_chn, &stream);
// 更新 FPS 统计
update_fps_stats();
// 返回结果
*jpeg_data = buffer;
*jpeg_size = total_size;
LOGD("Captured JPEG: %zu bytes, FPS: %.1f", total_size, m_fps);
return true;
}
void Camera::release_frame(uint8_t* jpeg_data)
{
if (jpeg_data) {
delete[] jpeg_data;
}
}
bool Camera::set_framesize(const std::string& size)
{
LOGW("set_framesize not implemented yet: %s", size.c_str());
// TODO: 动态调整分辨率需要重新配置 VI 和 VENC
return false;
}
void Camera::set_quality(int quality)
{
if (quality < 1 || quality > 99) {
LOGW("Invalid quality %d, using default %d", quality, DEFAULT_QUALITY);
quality = DEFAULT_QUALITY;
}
m_quality = quality;
if (m_venc_chn != MM_INVALID_CHN) {
VENC_PARAM_JPEG_S jpeg_param;
memset(&jpeg_param, 0, sizeof(VENC_PARAM_JPEG_S));
jpeg_param.Qfactor = m_quality;
AW_MPI_VENC_SetJpegParam(m_venc_chn, &jpeg_param);
LOGD("JPEG quality updated: %d", m_quality);
}
}
void Camera::update_fps_stats()
{
m_frame_count++;
struct timeval tv;
gettimeofday(&tv, NULL);
uint64_t current_time = tv.tv_sec * 1000000ULL + tv.tv_usec;
if (m_last_time == 0) {
m_last_time = current_time;
return;
}
uint64_t elapsed = current_time - m_last_time;
if (elapsed >= 1000000) { // 1秒
m_fps = (float)m_frame_count * 1000000.0f / elapsed;
m_frame_count = 0;
m_last_time = current_time;
}
}

View File

@@ -0,0 +1,100 @@
/**
* @file camera.h
* @brief 摄像头封装 - 基于全志 MPP 库
*
* 功能:
* - 初始化 GC2083 MIPI CSI 摄像头
* - 配置 ISP (Image Signal Processor)
* - JPEG 硬件编码
* - 动态调整分辨率和画质
*/
#ifndef CAMERA_H
#define CAMERA_H
#include <cstdint>
#include <string>
// MPP 库头文件 (根据 SDK 实际路径调整)
extern "C" {
#include <mpi_vi.h>
#include <mpi_isp.h>
#include <mpi_venc.h>
}
class Camera {
public:
Camera();
~Camera();
/**
* @brief 初始化摄像头
* @return true 成功, false 失败
*/
bool init();
/**
* @brief 采集一帧 JPEG 图像
* @param jpeg_data 输出参数,指向 JPEG 数据的指针
* @param jpeg_size 输出参数JPEG 数据大小(字节)
* @return true 成功, false 失败
* @note 调用后必须使用 release_frame() 释放
*/
bool capture_frame(uint8_t** jpeg_data, size_t* jpeg_size);
/**
* @brief 释放帧缓冲
* @param jpeg_data capture_frame() 返回的指针
*/
void release_frame(uint8_t* jpeg_data);
/**
* @brief 设置分辨率
* @param size "VGA" / "SVGA" / "XGA" / "SXGA"
* @return true 成功, false 失败
*/
bool set_framesize(const std::string& size);
/**
* @brief 设置 JPEG 压缩质量
* @param quality 5-40 (数值越小越清晰)
*/
void set_quality(int quality);
/**
* @brief 获取当前 FPS 统计
*/
float get_fps() const { return m_fps; }
/**
* @brief 关闭摄像头(支持错误恢复后重新 init
* @note Day 11 新增:用于错误恢复
*/
void deinit();
private:
// MPP 句柄
VI_DEV m_vi_dev;
VI_CHN m_vi_chn;
ISP_DEV m_isp_dev;
VENC_CHN m_venc_chn;
// 配置参数
int m_width;
int m_height;
int m_quality;
// 性能统计
float m_fps;
uint64_t m_frame_count;
uint64_t m_last_time;
// 内部方法
bool init_vi(); // Video Input
bool init_isp(); // Image Signal Processor
bool init_venc(); // Video Encoder
void update_fps_stats();
};
#endif // CAMERA_H

View File

@@ -0,0 +1,349 @@
/**
* @file icm42688_spi.cpp
* @brief ICM-42688/ICM-42670 IMU 驱动 - GPIO 模拟 SPI 实现
* @date 2025-11-27
* @platform Avaota F1 (V821 / RISC-V)
*
* 硬件连接:
* - VCC → 3.3V
* - GND → GND
* - SCL/SCLK → PD3 (GPIO 99)
* - SDA/MOSI → PD2 (GPIO 98)
* - AD0/MISO → PD4 (GPIO 100)
* - CS → PD5 (GPIO 101)
*
* 使用 GPIO 模拟 SPI速度约 1MHz
*/
#include "icm42688.h"
#include "../utils/logger.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/gpio.h>
#include <cstring>
// GPIO 引脚定义
#define GPIO_SCLK 99 // PD3 - SPI 时钟
#define GPIO_MOSI 98 // PD2 - 主→从数据
#define GPIO_MISO 100 // PD4 - 从→主数据
#define GPIO_CS 101 // PD5 - 片选
#define GPIO_CHIP "/dev/gpiochip0"
#define SPI_DELAY_US 1 // 1us 延迟,约 500kHz
// GPIO 句柄
static int gpio_chip_fd = -1;
static int sclk_fd = -1;
static int mosi_fd = -1;
static int miso_fd = -1;
static int cs_fd = -1;
// GPIO 控制函数
static bool gpio_init() {
gpio_chip_fd = open(GPIO_CHIP, O_RDWR);
if (gpio_chip_fd < 0) {
LOG_ERROR("[IMU] Failed to open GPIO chip %s", GPIO_CHIP);
return false;
}
// 请求 SCLK (输出)
struct gpiohandle_request req_sclk;
memset(&req_sclk, 0, sizeof(req_sclk));
req_sclk.lineoffsets[0] = GPIO_SCLK;
req_sclk.flags = GPIOHANDLE_REQUEST_OUTPUT;
req_sclk.default_values[0] = 0; // 默认低电平
req_sclk.lines = 1;
strcpy(req_sclk.consumer_label, "icm_sclk");
if (ioctl(gpio_chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &req_sclk) < 0) {
LOG_ERROR("[IMU] Failed to request SCLK GPIO %d", GPIO_SCLK);
close(gpio_chip_fd);
return false;
}
sclk_fd = req_sclk.fd;
// 请求 MOSI (输出)
struct gpiohandle_request req_mosi;
memset(&req_mosi, 0, sizeof(req_mosi));
req_mosi.lineoffsets[0] = GPIO_MOSI;
req_mosi.flags = GPIOHANDLE_REQUEST_OUTPUT;
req_mosi.default_values[0] = 0;
req_mosi.lines = 1;
strcpy(req_mosi.consumer_label, "icm_mosi");
if (ioctl(gpio_chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &req_mosi) < 0) {
LOG_ERROR("[IMU] Failed to request MOSI GPIO %d", GPIO_MOSI);
close(sclk_fd);
close(gpio_chip_fd);
return false;
}
mosi_fd = req_mosi.fd;
// 请求 MISO (输入)
struct gpiohandle_request req_miso;
memset(&req_miso, 0, sizeof(req_miso));
req_miso.lineoffsets[0] = GPIO_MISO;
req_miso.flags = GPIOHANDLE_REQUEST_INPUT;
req_miso.lines = 1;
strcpy(req_miso.consumer_label, "icm_miso");
if (ioctl(gpio_chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &req_miso) < 0) {
LOG_ERROR("[IMU] Failed to request MISO GPIO %d", GPIO_MISO);
close(mosi_fd);
close(sclk_fd);
close(gpio_chip_fd);
return false;
}
miso_fd = req_miso.fd;
// 请求 CS (输出,默认高电平 = 未选中)
struct gpiohandle_request req_cs;
memset(&req_cs, 0, sizeof(req_cs));
req_cs.lineoffsets[0] = GPIO_CS;
req_cs.flags = GPIOHANDLE_REQUEST_OUTPUT;
req_cs.default_values[0] = 1; // 默认高电平(未选中)
req_cs.lines = 1;
strcpy(req_cs.consumer_label, "icm_cs");
if (ioctl(gpio_chip_fd, GPIO_GET_LINEHANDLE_IOCTL, &req_cs) < 0) {
LOG_ERROR("[IMU] Failed to request CS GPIO %d", GPIO_CS);
close(miso_fd);
close(mosi_fd);
close(sclk_fd);
close(gpio_chip_fd);
return false;
}
cs_fd = req_cs.fd;
LOG_INFO("[IMU] GPIO SPI initialized: SCLK=PD3(GPIO%d), MOSI=PD2(GPIO%d), MISO=PD4(GPIO%d), CS=PD5(GPIO%d)",
GPIO_SCLK, GPIO_MOSI, GPIO_MISO, GPIO_CS);
return true;
}
static void sclk_high() {
struct gpiohandle_data data;
data.values[0] = 1;
ioctl(sclk_fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
usleep(SPI_DELAY_US);
}
static void sclk_low() {
struct gpiohandle_data data;
data.values[0] = 0;
ioctl(sclk_fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
usleep(SPI_DELAY_US);
}
static void mosi_high() {
struct gpiohandle_data data;
data.values[0] = 1;
ioctl(mosi_fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
}
static void mosi_low() {
struct gpiohandle_data data;
data.values[0] = 0;
ioctl(mosi_fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
}
static bool miso_read() {
struct gpiohandle_data data;
ioctl(miso_fd, GPIOHANDLE_GET_LINE_VALUES_IOCTL, &data);
return data.values[0];
}
static void cs_select() {
struct gpiohandle_data data;
data.values[0] = 0; // 低电平 = 选中
ioctl(cs_fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
usleep(SPI_DELAY_US);
}
static void cs_deselect() {
struct gpiohandle_data data;
data.values[0] = 1; // 高电平 = 未选中
ioctl(cs_fd, GPIOHANDLE_SET_LINE_VALUES_IOCTL, &data);
usleep(SPI_DELAY_US);
}
// SPI 传输一个字节Mode 0: CPOL=0, CPHA=0
static uint8_t spi_transfer_byte(uint8_t tx_byte) {
uint8_t rx_byte = 0;
for (int i = 7; i >= 0; i--) {
// 设置 MOSI时钟下降沿前设置数据
if (tx_byte & (1 << i)) {
mosi_high();
} else {
mosi_low();
}
// 上升沿:从设备采样 MOSI
sclk_high();
// 读取 MISO时钟上升沿后读取数据
if (miso_read()) {
rx_byte |= (1 << i);
}
// 下降沿:从设备改变 MISO
sclk_low();
}
return rx_byte;
}
// 构造函数
ICM42688::ICM42688(const std::string& i2c_device, uint8_t address)
: m_i2c_device(i2c_device), m_address(address), m_fd(-1),
m_ax(0), m_ay(0), m_az(0),
m_gx(0), m_gy(0), m_gz(0),
m_temp(0),
m_accel_scale(0.0f), m_gyro_scale(0.0f) {
}
// 析构函数
ICM42688::~ICM42688() {
if (cs_fd >= 0) close(cs_fd);
if (miso_fd >= 0) close(miso_fd);
if (mosi_fd >= 0) close(mosi_fd);
if (sclk_fd >= 0) close(sclk_fd);
if (gpio_chip_fd >= 0) close(gpio_chip_fd);
}
// 初始化 IMU
bool ICM42688::init() {
// 1. 初始化 GPIO
if (!gpio_init()) {
LOG_ERROR("[IMU] GPIO initialization failed");
return false;
}
m_fd = 1; // 标记已初始化
// 等待芯片上电稳定
LOG_INFO("[IMU] Waiting for chip power-up (100ms)...");
usleep(100000);
// 2. 读取 WHO_AM_I 验证设备
LOG_INFO("[IMU] Reading WHO_AM_I register...");
uint8_t who_am_i = read_register(ICM42688_WHO_AM_I);
if (who_am_i != ICM42688_DEVICE_ID && who_am_i != ICM42670_DEVICE_ID) {
LOG_ERROR("[IMU] ICM42688 not found, WHO_AM_I = 0x%02X (expected 0x47 or 0x67)", who_am_i);
return false;
}
LOG_INFO("[IMU] ICM42688 detected, WHO_AM_I = 0x%02X", who_am_i);
// 3. 软复位
write_register(ICM42688_PWR_MGMT0, 0x01); // SOFT_RESET
usleep(100000); // 等待 100ms
// 4. 设置加速度计和陀螺仪为待机
write_register(ICM42688_PWR_MGMT0, 0x1F);
usleep(1000);
// 5. 配置加速度计±16g, 1kHz ODR
write_register(ICM42688_ACCEL_CONFIG0,
(uint8_t)AccelScale::AFS_16G << 5 | (uint8_t)ODR::ODR_1KHZ);
m_accel_scale = 16.0f / 32768.0f; // g
// 6. 配置陀螺仪±2000°/s, 1kHz ODR
write_register(ICM42688_GYRO_CONFIG0,
(uint8_t)GyroScale::GFS_2000DPS << 5 | (uint8_t)ODR::ODR_1KHZ);
m_gyro_scale = 2000.0f / 32768.0f; // °/s
// 7. 启动加速度计和陀螺仪
write_register(ICM42688_PWR_MGMT0, 0x0F); // ACCEL + GYRO ON
usleep(100000); // 等待 100ms
LOG_INFO("[IMU] ICM42688 initialized successfully");
return true;
}
// 读取传感器数据
bool ICM42688::read_sensor() {
uint8_t data[14];
if (!read_registers(ICM42688_TEMP_DATA1, 14, data)) {
LOG_ERROR("[IMU] Failed to read sensor data");
return false;
}
// 解析数据(大端序)
m_temp = (int16_t)(data[0] << 8 | data[1]);
m_ax = (int16_t)(data[2] << 8 | data[3]);
m_ay = (int16_t)(data[4] << 8 | data[5]);
m_az = (int16_t)(data[6] << 8 | data[7]);
m_gx = (int16_t)(data[8] << 8 | data[9]);
m_gy = (int16_t)(data[10] << 8 | data[11]);
m_gz = (int16_t)(data[12] << 8 | data[13]);
return true;
}
// 获取数据
float ICM42688::get_accel_x() { return (float)m_ax * m_accel_scale * G; }
float ICM42688::get_accel_y() { return (float)m_ay * m_accel_scale * G; }
float ICM42688::get_accel_z() { return (float)m_az * m_accel_scale * G; }
float ICM42688::get_gyro_x() { return (float)m_gx * m_gyro_scale; }
float ICM42688::get_gyro_y() { return (float)m_gy * m_gyro_scale; }
float ICM42688::get_gyro_z() { return (float)m_gz * m_gyro_scale; }
float ICM42688::get_temperature() { return ((float)m_temp / TEMP_SCALE) + TEMP_OFFSET; }
// 写入寄存器SPI
bool ICM42688::write_register(uint8_t reg, uint8_t value) {
cs_select();
// 写操作:寄存器地址最高位为 0
spi_transfer_byte(reg & 0x7F);
spi_transfer_byte(value);
cs_deselect();
return true;
}
// 读取单个寄存器SPI
uint8_t ICM42688::read_register(uint8_t reg) {
cs_select();
// 读操作:寄存器地址最高位为 1
spi_transfer_byte(reg | 0x80);
uint8_t value = spi_transfer_byte(0x00); // 发送 dummy 字节读取数据
cs_deselect();
return value;
}
// 读取多个寄存器SPI
bool ICM42688::read_registers(uint8_t reg, uint8_t count, uint8_t* data) {
cs_select();
// 读操作:寄存器地址最高位为 1
spi_transfer_byte(reg | 0x80);
for (uint8_t i = 0; i < count; i++) {
data[i] = spi_transfer_byte(0x00); // 发送 dummy 字节读取数据
}
cs_deselect();
return true;
}
// 读取所有传感器数据(便捷方法)
bool ICM42688::read_sensors(float& temp_c, float& ax, float& ay, float& az,
float& gx, float& gy, float& gz) {
if (!read_sensor()) {
return false;
}
temp_c = get_temperature();
ax = get_accel_x();
ay = get_accel_y();
az = get_accel_z();
gx = get_gyro_x();
gy = get_gyro_y();
gz = get_gyro_z();
return true;
}

View File

@@ -0,0 +1,202 @@
/**
* @file icm42688.h
* @brief ICM-42688/ICM-42670 IMU 驱动 - Linux I2C 用户空间实现
* @date 2025-11-26
* @platform Avaota F1 (V821 / RISC-V)
*
* ICM-42688 是一款高性能 6 轴 IMU支持 I2C 接口
* - 3 轴加速度计±2g, ±4g, ±8g, ±16g
* - 3 轴陀螺仪±15.625°/s ~ ±2000°/s
* - 温度传感器
*/
#ifndef ICM42688_H
#define ICM42688_H
#include <cstdint>
#include <string>
// ICM-42688 寄存器定义
#define ICM42688_WHO_AM_I 0x75
#define ICM42688_DEVICE_ID 0x47 // ICM-42688
#define ICM42670_DEVICE_ID 0x67 // ICM-42670
#define ICM42688_PWR_MGMT0 0x4E // 电源管理
#define ICM42688_GYRO_CONFIG0 0x4F // 陀螺仪配置
#define ICM42688_ACCEL_CONFIG0 0x50 // 加速度计配置
#define ICM42688_TEMP_DATA1 0x1D // 温度数据寄存器起始地址
/**
* @brief ICM-42688 IMU 类
*
* 使用 Linux I2C 用户空间 API 与 ICM-42688 通信
*/
class ICM42688 {
public:
/**
* @brief 加速度计量程
*/
enum class AccelScale {
AFS_16G = 0, // ±16g
AFS_8G = 1, // ±8g
AFS_4G = 2, // ±4g
AFS_2G = 3 // ±2g
};
/**
* @brief 陀螺仪量程
*/
enum class GyroScale {
GFS_2000DPS = 0, // ±2000°/s
GFS_1000DPS = 1, // ±1000°/s
GFS_500DPS = 2, // ±500°/s
GFS_250DPS = 3, // ±250°/s
GFS_125DPS = 4, // ±125°/s
GFS_62_5DPS = 5, // ±62.5°/s
GFS_31_25DPS = 6, // ±31.25°/s
GFS_15_625DPS = 7 // ±15.625°/s
};
/**
* @brief 输出数据率 (ODR)
*/
enum class ODR {
ODR_32KHZ = 0x01,
ODR_16KHZ = 0x02,
ODR_8KHZ = 0x03,
ODR_4KHZ = 0x04,
ODR_2KHZ = 0x05,
ODR_1KHZ = 0x06, // 推荐使用
ODR_200HZ = 0x07,
ODR_100HZ = 0x08,
ODR_50HZ = 0x09,
ODR_25HZ = 0x0A,
ODR_12_5HZ = 0x0B
};
/**
* @brief 构造函数
* @param i2c_device I2C 设备路径,如 "/dev/i2c-0"
* @param address I2C 地址0x68 (AD0=GND) 或 0x69 (AD0=VCC)
*/
ICM42688(const std::string& i2c_device, uint8_t address = 0x68);
/**
* @brief 析构函数
*/
~ICM42688();
/**
* @brief 初始化 IMU
* @return true 成功false 失败
*/
bool init();
/**
* @brief 读取传感器数据
* @return true 成功false 失败
*/
bool read_sensor();
/**
* @brief 获取加速度计 X 轴数据
* @return 加速度 (m/s²)
*/
float get_accel_x();
/**
* @brief 获取加速度计 Y 轴数据
* @return 加速度 (m/s²)
*/
float get_accel_y();
/**
* @brief 获取加速度计 Z 轴数据
* @return 加速度 (m/s²)
*/
float get_accel_z();
/**
* @brief 获取陀螺仪 X 轴数据
* @return 角速度 (°/s)
*/
float get_gyro_x();
/**
* @brief 获取陀螺仪 Y 轴数据
* @return 角速度 (°/s)
*/
float get_gyro_y();
/**
* @brief 获取陀螺仪 Z 轴数据
* @return 角速度 (°/s)
*/
float get_gyro_z();
/**
* @brief 获取温度
* @return 温度 (°C)
*/
float get_temperature();
/**
* @brief 读取所有传感器数据(便捷方法)
* @param temp_c 温度 (°C)
* @param ax 加速度 X (m/s²)
* @param ay 加速度 Y (m/s²)
* @param az 加速度 Z (m/s²)
* @param gx 陀螺仪 X (°/s)
* @param gy 陀螺仪 Y (°/s)
* @param gz 陀螺仪 Z (°/s)
* @return true 成功false 失败
*/
bool read_sensors(float& temp_c, float& ax, float& ay, float& az,
float& gx, float& gy, float& gz);
private:
// I2C 设备信息
std::string m_i2c_device; // I2C 设备路径
uint8_t m_address; // I2C 地址
int m_fd; // I2C 文件描述符
// 传感器原始数据
int16_t m_ax, m_ay, m_az; // 加速度计原始值
int16_t m_gx, m_gy, m_gz; // 陀螺仪原始值
int16_t m_temp; // 温度原始值
// 缩放因子
float m_accel_scale; // 加速度计缩放因子 (g)
float m_gyro_scale; // 陀螺仪缩放因子 (°/s)
// 常量
static constexpr float G = 9.80665f; // 重力加速度 (m/s²)
static constexpr float TEMP_SCALE = 333.87f; // 温度缩放因子
static constexpr float TEMP_OFFSET = 21.0f; // 温度偏移
/**
* @brief 写入寄存器
* @param reg 寄存器地址
* @param value 写入的值
* @return true 成功false 失败
*/
bool write_register(uint8_t reg, uint8_t value);
/**
* @brief 读取单个寄存器
* @param reg 寄存器地址
* @return 读取的值(失败返回 0
*/
uint8_t read_register(uint8_t reg);
/**
* @brief 读取多个寄存器
* @param reg 起始寄存器地址
* @param count 读取字节数
* @param data 数据缓冲区
* @return true 成功false 失败
*/
bool read_registers(uint8_t reg, uint8_t count, uint8_t* data);
};
#endif // ICM42688_H

View File

@@ -0,0 +1,377 @@
/**
* @file main.cpp
* @brief AvaotaF1 智能眼镜客户端 - 主程序
* @date 2025-11-21
* @platform Avaota F1 (Allwinner V821 / RISC-V)
*
* 功能模块:
* - 摄像头视频流 (WebSocket /ws/camera)
* - 音频采集上传 (WebSocket /ws_audio)
* - TTS 音频播放 (WebSocket /ws_audio - 双向)
* - IMU 数据上报 (UDP :12345)
*/
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
#include <signal.h>
#include <unistd.h>
#include "camera/camera.h"
#include "audio/audio_capture.h"
#include "audio/audio_player.h"
// Day 21: VAD 已移到服务器端 (Silero VAD),不再需要客户端 VAD
#include "imu/icm42688.h"
#include "network/ws_client.h"
#include "network/udp_sender.h"
#include "utils/logger.h"
// ===== 配置参数 =====
const char* SERVER_HOST = "8.148.25.142";
const int SERVER_PORT = 8081;
const char* CAM_WS_PATH = "/ws/camera";
const char* AUD_WS_PATH = "/ws_audio";
const int UDP_PORT = 12345;
// ===== 全局控制标志 =====
std::atomic<bool> g_running{true};
// ===== 信号处理 =====
void signal_handler(int sig) {
LOG_INFO("Received signal %d, shutting down...", sig);
g_running = false;
}
// ===== 摄像头线程 =====
void camera_thread() {
LOG_INFO("[CAM] Thread started");
Camera camera;
if (!camera.init()) {
LOG_ERROR("[CAM] Init failed");
return;
}
WSClient ws_cam(SERVER_HOST, SERVER_PORT, CAM_WS_PATH);
// Day 11: 添加连续失败计数和恢复机制
int consecutive_failures = 0;
const int MAX_FAILURES = 5;
while (g_running) {
// 连接 WebSocket
bool connected = ws_cam.is_connected();
if (!connected) {
if (ws_cam.connect()) {
LOG_INFO("[CAM] WebSocket connected");
connected = true;
} else {
LOG_WARN("[CAM] WebSocket connect failed, will retry after frame consume");
// Day 11: 不再 continue而是继续采集帧来清空 VBV
}
}
// 采集一帧
uint8_t* jpeg_data = nullptr;
size_t jpeg_size = 0;
if (camera.capture_frame(&jpeg_data, &jpeg_size)) {
consecutive_failures = 0; // 成功时重置计数
// Day 11: 只在连接时发送,否则丢弃帧以保持 VBV 流畅
if (connected) {
if (!ws_cam.send_binary(jpeg_data, jpeg_size)) {
LOG_ERROR("[CAM] Send failed");
ws_cam.disconnect();
}
} else {
// 未连接时消费帧但不发送,间隔 1 秒重试连接
usleep(1000000);
}
camera.release_frame(jpeg_data);
} else {
consecutive_failures++;
LOG_WARN("[CAM] Capture failed (%d/%d)", consecutive_failures, MAX_FAILURES);
// Day 11: 连续失败超过阈值,尝试重新初始化
if (consecutive_failures >= MAX_FAILURES) {
LOG_WARN("[CAM] Too many failures, reinitializing camera...");
camera.deinit();
usleep(1000000); // 等待 1 秒
if (!camera.init()) {
LOG_ERROR("[CAM] Reinit failed, fatal error!");
break; // 无法恢复,退出线程
}
LOG_INFO("[CAM] Camera reinitialized successfully");
consecutive_failures = 0;
}
usleep(100000); // 100ms 等待
}
// 处理服务器消息 (SET:FRAMESIZE=VGA 等)
ws_cam.poll_messages([&camera](const std::string& msg) {
if (msg.find("SET:FRAMESIZE=") == 0) {
std::string size = msg.substr(14);
camera.set_framesize(size);
} else if (msg.find("SET:QUALITY=") == 0) {
int quality = std::stoi(msg.substr(12));
camera.set_quality(quality);
}
});
}
LOG_INFO("[CAM] Thread stopped");
}
// ===== 音频采集线程 =====
void audio_capture_thread() {
LOG_INFO("[AUD-CAP] Thread started");
AudioCapture mic("hw:0,0", 16000, 1); // 16kHz, mono - 麦克风
if (!mic.init()) {
LOG_ERROR("[AUD-CAP] Init failed");
return;
}
// Day 21 改进: 移除客户端 VAD改为服务器端 Silero VAD
// 客户端持续发送所有音频,服务器负责语音检测
LOG_INFO("[AUD-CAP] Server-side VAD mode (continuous audio streaming)");
// Day 14 修正: 恢复 WebSocket TTS 播放(经验证有效)
// 初始化扬声器 (hw:1,0 - I2S 接口连接 MAX98357A)
AudioPlayer speaker("hw:1,0", 16000, 1); // 16kHz, mono
bool speaker_enabled = false;
if (speaker.init()) {
LOG_INFO("[AUD-PLAY] Speaker initialized on hw:1,0");
speaker_enabled = true;
} else {
LOG_WARN("[AUD-PLAY] Speaker init failed, audio playback disabled");
}
WSClient ws_aud(SERVER_HOST, SERVER_PORT, AUD_WS_PATH);
// Day 11: 添加诊断计数器
int audio_send_count = 0;
int audio_bytes_total = 0;
int audio_read_attempts = 0;
int audio_zero_reads = 0;
// TTS 预缓冲区 - Day 22 优化: 降低延迟
// 原来2秒预缓冲导致外出时语音延迟太大改为0.5秒
std::vector<int16_t> tts_buffer;
const size_t PRE_BUFFER_FRAMES = 8000; // 0.5 秒的帧数 (16kHz) - 从32000降低
const size_t MIN_PLAY_FRAMES = 1600; // 最小播放帧数 (0.1s) - 从4800降低
bool is_buffering = true;
int tts_recv_count = 0;
// Day 21 优化:指数退避重连策略
int reconnect_delay = 1; // 初始 1 秒
const int MAX_RECONNECT_DELAY = 16; // 最大 16 秒
while (g_running) {
if (!ws_aud.is_connected()) {
if (ws_aud.connect()) {
LOG_INFO("[AUD-CAP] WebSocket connected");
ws_aud.send_text("START");
// 重置 TTS 缓冲状态
tts_buffer.clear();
is_buffering = true;
// 连接成功,重置退避时间
reconnect_delay = 1;
} else {
LOG_WARN("[AUD-CAP] Connect failed, retry in %ds", reconnect_delay);
sleep(reconnect_delay);
// 指数退避:每次失败后延迟翻倍,最大 16 秒
reconnect_delay = std::min(reconnect_delay * 2, MAX_RECONNECT_DELAY);
continue;
}
}
// Day 22 优化: 使用20ms音频包(320 samples)与服务器ASR期望一致
// 20ms @ 16kHz = 320 samples = 640 bytes
int16_t buffer[320];
snd_pcm_sframes_t frames_read = mic.read(buffer, 320);
audio_read_attempts++;
// 每 100 次读取输出诊断
if (audio_read_attempts % 100 == 0) {
LOG_INFO("[AUD-CAP] Stats: attempts=%d, sent=%d, zero_reads=%d",
audio_read_attempts, audio_send_count, audio_zero_reads);
}
// 检查致命错误(设备重新初始化失败)
if (frames_read < 0) {
LOG_ERROR("[AUD-CAP] Fatal error, exiting thread");
break;
}
if (frames_read > 0) {
// Day 21 改进: 移除客户端 VAD改为持续发送所有音频
// 服务器端使用 Silero VAD 进行语音检测(更准确)
int bytes_to_send = frames_read * 2;
if (ws_aud.send_binary((uint8_t*)buffer, bytes_to_send)) {
audio_send_count++;
audio_bytes_total += bytes_to_send;
// 第一次发送时打印日志
if (audio_send_count == 1) {
LOG_INFO("[AUD-CAP] First audio packet sent! (%d bytes)", bytes_to_send);
}
// 每 100 次发送输出一次诊断日志
if (audio_send_count % 100 == 0) {
LOG_INFO("[AUD-CAP] Sent %d packets, %d bytes total",
audio_send_count, audio_bytes_total);
}
} else {
LOG_WARN("[AUD-CAP] Send failed");
}
} else {
audio_zero_reads++;
// 非阻塞模式:等待 20ms
usleep(20000); // 20ms
}
// 处理服务器消息 (文本命令和二进制音频数据)
ws_aud.poll_messages([&](const std::string& msg) {
// Day 21: 正确处理 RESET/RESTART 命令,重新启动 ASR 会话
if (msg == "RESTART" || msg == "RESET") {
LOG_INFO("[AUD-CAP] Received %s command, restarting ASR...", msg.c_str());
ws_aud.send_text("START");
// 重置 TTS 缓冲状态
tts_buffer.clear();
is_buffering = true;
tts_recv_count = 0;
}
});
// Day 14 修正: 恢复 WebSocket TTS 播放(服务器返回 16kHz PCM
if (speaker_enabled) {
ws_aud.poll_binary_messages([&](const uint8_t* data, size_t size) {
// 服务器返回的是 16kHz, mono, S16_LE PCM 数据
if (size % 2 == 0) {
size_t frames = size / 2;
tts_recv_count++;
// 将新数据追加到缓冲区
const int16_t* samples = (const int16_t*)data;
tts_buffer.insert(tts_buffer.end(), samples, samples + frames);
// 第一次收到时输出日志
if (tts_recv_count == 1) {
LOG_INFO("[AUD-PLAY] 🔊 First TTS chunk: %zu bytes", size);
}
// 检查是否应该开始播放
if (is_buffering && tts_buffer.size() >= PRE_BUFFER_FRAMES) {
LOG_INFO("[AUD-PLAY] Pre-buffer full (%zu frames), starting playback",
tts_buffer.size());
is_buffering = false;
}
}
});
// 播放缓冲区中的数据
if (!is_buffering && tts_buffer.size() >= MIN_PLAY_FRAMES) {
// 每次播放 1600 帧 (100ms)
size_t play_frames = std::min(tts_buffer.size(), (size_t)1600);
snd_pcm_sframes_t written = speaker.write(tts_buffer.data(), play_frames);
if (written > 0) {
tts_buffer.erase(tts_buffer.begin(), tts_buffer.begin() + written);
} else if (written == -EAGAIN) {
usleep(5000); // 5ms
}
} else if (!is_buffering && tts_buffer.empty()) {
// 缓冲区空了,重新进入缓冲状态
is_buffering = true;
tts_recv_count = 0;
}
}
}
LOG_INFO("[AUD-CAP] Thread stopped");
}
// ===== IMU 线程 =====
void imu_thread() {
LOG_INFO("[IMU] Thread started");
ICM42688 imu("/dev/spidev0.0");
if (!imu.init()) {
LOG_ERROR("[IMU] Init failed (check Device Tree SPI config)");
return;
}
UDPSender udp(SERVER_HOST, UDP_PORT);
while (g_running) {
float temp_c, ax, ay, az, gx, gy, gz;
if (imu.read_sensors(temp_c, ax, ay, az, gx, gy, gz)) {
// 构造 JSON
char json[256];
snprintf(json, sizeof(json),
"{\"ts\":%lu,\"temp_c\":%.2f,"
"\"accel\":{\"x\":%.3f,\"y\":%.3f,\"z\":%.3f},"
"\"gyro\":{\"x\":%.3f,\"y\":%.3f,\"z\":%.3f}}",
(unsigned long)(time(nullptr) * 1000), temp_c,
ax, ay, az, gx, gy, gz);
udp.send(json, strlen(json));
}
usleep(100000); // 10 Hz (100ms) - 降低采样率减少对音频的干扰
}
LOG_INFO("[IMU] Thread stopped");
}
// ===== 主函数 =====
int main(int argc, char** argv) {
(void)argc; // 未使用
(void)argv;
// 初始化日志系统
Logger::init(LOG_LEVEL_INFO);
LOG_INFO("========================================");
LOG_INFO("AvaotaF1 Client Starting...");
LOG_INFO("Server: %s:%d", SERVER_HOST, SERVER_PORT);
LOG_INFO("========================================");
// 注册信号处理
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// 创建线程
std::thread t_camera(camera_thread);
std::thread t_mic(audio_capture_thread);
// Day 14 修正: 移除 HTTP TTS 线程,改回 WebSocket TTS 播放(在 audio_capture_thread 中处理)
std::thread t_imu(imu_thread);
// 主线程监控
while (g_running) {
sleep(5);
LOG_INFO("[MAIN] Heartbeat - all threads running");
}
// 等待线程退出
LOG_INFO("Waiting for threads to exit...");
t_camera.join();
t_mic.join();
t_imu.join();
LOG_INFO("========================================");
LOG_INFO("AvaotaF1 Client Stopped");
LOG_INFO("========================================");
return 0;
}

View File

@@ -0,0 +1,70 @@
/**
* @file udp_sender.cpp
* @brief UDP 发送器实现
*/
#include "udp_sender.h"
#include "../utils/logger.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
UDPSender::UDPSender(const std::string& host, int port)
: m_host(host), m_port(port), m_socket_fd(-1), m_sockaddr(nullptr) {
// 创建 UDP socket
m_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (m_socket_fd < 0) {
LOG_ERROR("[UDP] Failed to create socket");
return;
}
// 配置目标地址
struct sockaddr_in* addr = new struct sockaddr_in;
memset(addr, 0, sizeof(*addr));
addr->sin_family = AF_INET;
addr->sin_port = htons(m_port);
if (inet_pton(AF_INET, m_host.c_str(), &addr->sin_addr) <= 0) {
LOG_ERROR("[UDP] Invalid address: %s", m_host.c_str());
close(m_socket_fd);
m_socket_fd = -1;
delete addr;
return;
}
m_sockaddr = addr;
LOG_INFO("[UDP] Initialized to %s:%d", m_host.c_str(), m_port);
}
UDPSender::~UDPSender() {
if (m_socket_fd >= 0) {
close(m_socket_fd);
}
if (m_sockaddr) {
delete (struct sockaddr_in*)m_sockaddr;
}
}
int UDPSender::send(const void* data, size_t size) {
if (m_socket_fd < 0 || !m_sockaddr) {
return -1;
}
struct sockaddr_in* addr = (struct sockaddr_in*)m_sockaddr;
ssize_t sent = sendto(m_socket_fd, data, size, 0,
(struct sockaddr*)addr, sizeof(*addr));
if (sent < 0) {
LOG_ERROR("[UDP] Send failed");
return -1;
}
return (int)sent;
}

View File

@@ -0,0 +1,37 @@
/**
* @file udp_sender.h
* @brief UDP 发送器 - 用于 IMU 数据上报
*/
#ifndef UDP_SENDER_H
#define UDP_SENDER_H
#include <string>
#include <cstdint>
class UDPSender {
public:
/**
* @brief 构造函数
* @param host 目标主机地址
* @param port 目标端口
*/
UDPSender(const std::string& host, int port);
~UDPSender();
/**
* @brief 发送数据
* @param data 数据指针
* @param size 数据大小
* @return 发送的字节数,-1 表示失败
*/
int send(const void* data, size_t size);
private:
std::string m_host;
int m_port;
int m_socket_fd;
void* m_sockaddr; // struct sockaddr_in* (避免头文件依赖)
};
#endif // UDP_SENDER_H

View File

@@ -0,0 +1,392 @@
/**
* @file ws_client.cpp
* @brief WebSocket 客户端实现 - 轻量级版本
*/
#include "ws_client.h"
#include "../utils/logger.h"
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#include <openssl/sha.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>
// WebSocket 魔术字符串(用于握手验证,当前未使用)
// static const char* WS_MAGIC_STRING = "258EAFAA-5914-47DA-95CA-C5AB0DC85B11";
WSClient::WSClient(const std::string& host, int port, const std::string& path)
: m_host(host), m_port(port), m_path(path),
m_sockfd(-1), m_connected(false), m_running(false) {
}
WSClient::~WSClient() {
disconnect();
}
bool WSClient::connect() {
if (m_connected) return true;
// 创建 socket
m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (m_sockfd < 0) {
LOG_ERROR("[WS] Failed to create socket: %s", strerror(errno));
return false;
}
// 解析主机地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(m_port);
// 尝试直接解析 IP
if (inet_pton(AF_INET, m_host.c_str(), &server_addr.sin_addr) <= 0) {
// 不是IP地址尝试 DNS 解析
struct hostent* he = gethostbyname(m_host.c_str());
if (!he) {
LOG_ERROR("[WS] Failed to resolve host: %s", m_host.c_str());
close(m_sockfd);
m_sockfd = -1;
return false;
}
memcpy(&server_addr.sin_addr, he->h_addr_list[0], he->h_length);
}
// 连接到服务器
if (::connect(m_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
LOG_ERROR("[WS] Failed to connect to %s:%d: %s",
m_host.c_str(), m_port, strerror(errno));
close(m_sockfd);
m_sockfd = -1;
return false;
}
// 增大发送缓冲区到 256KB默认通常只有 16KB
int sndbuf = 256 * 1024;
if (setsockopt(m_sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)) < 0) {
LOG_WARN("[WS] Failed to set send buffer size: %s", strerror(errno));
}
// 增大接收缓冲区到 256KB
int rcvbuf = 256 * 1024;
if (setsockopt(m_sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)) < 0) {
LOG_WARN("[WS] Failed to set recv buffer size: %s", strerror(errno));
}
LOG_INFO("[WS] TCP connected to %s:%d (buffers: 256KB)", m_host.c_str(), m_port);
// 执行 WebSocket 握手
if (!perform_handshake()) {
close(m_sockfd);
m_sockfd = -1;
return false;
}
m_connected = true;
m_running = true;
// 修复: 在创建新线程前,必须确保旧线程已被 join否则会触发 std::terminate
if (m_recv_thread.joinable()) {
m_recv_thread.join();
}
// 启动接收线程
m_recv_thread = std::thread(&WSClient::recv_loop, this);
LOG_INFO("[WS] WebSocket connected to ws://%s:%d%s",
m_host.c_str(), m_port, m_path.c_str());
return true;
}
void WSClient::disconnect() {
if (!m_connected && m_sockfd < 0) return;
m_running = false;
m_connected = false;
// 发送关闭帧
if (m_sockfd >= 0) {
uint8_t close_frame[] = {0x88, 0x00}; // FIN + CLOSE, 无payload
send(m_sockfd, close_frame, sizeof(close_frame), 0);
// 关闭 socket 读写以中断任何阻塞的 recv()/send()
shutdown(m_sockfd, SHUT_RDWR);
}
// 等待接收线程退出
if (m_recv_thread.joinable()) {
m_recv_thread.join();
}
if (m_sockfd >= 0) {
close(m_sockfd);
m_sockfd = -1;
}
LOG_INFO("[WS] Disconnected");
}
bool WSClient::send_text(const std::string& text) {
if (!m_connected) return false;
return send_frame((const uint8_t*)text.c_str(), text.length(), OP_TEXT);
}
bool WSClient::send_binary(const uint8_t* data, size_t size) {
if (!m_connected) return false;
// Day 12: 检查是否需要发送心跳
time_t now = time(nullptr);
if (m_last_send_time > 0 && (now - m_last_send_time) > HEARTBEAT_INTERVAL) {
send_ping();
}
return send_frame(data, size, OP_BINARY);
}
void WSClient::poll_messages(MessageCallback callback) {
std::lock_guard<std::mutex> lock(m_recv_mutex);
while (!m_recv_queue.empty()) {
callback(m_recv_queue.front());
m_recv_queue.pop();
}
}
void WSClient::poll_binary_messages(std::function<void(const uint8_t*, size_t)> callback) {
std::lock_guard<std::mutex> lock(m_binary_mutex);
while (!m_binary_queue.empty()) {
const std::vector<uint8_t>& data = m_binary_queue.front();
callback(data.data(), data.size());
m_binary_queue.pop();
}
}
// ===== 内部实现 =====
bool WSClient::perform_handshake() {
// 生成随机 Sec-WebSocket-Key (base64编码的16字节随机数)
uint8_t random_bytes[16];
for (int i = 0; i < 16; ++i) {
random_bytes[i] = rand() % 256;
}
// Base64 编码
BIO *bio, *b64;
BUF_MEM *bufferPtr;
b64 = BIO_new(BIO_f_base64());
bio = BIO_new(BIO_s_mem());
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
BIO_write(bio, random_bytes, 16);
BIO_flush(bio);
BIO_get_mem_ptr(bio, &bufferPtr);
std::string ws_key(bufferPtr->data, bufferPtr->length);
BIO_free_all(bio);
// 构造 HTTP 升级请求
char request[1024];
snprintf(request, sizeof(request),
"GET %s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Key: %s\r\n"
"Sec-WebSocket-Version: 13\r\n"
"\r\n",
m_path.c_str(), m_host.c_str(), m_port, ws_key.c_str());
// 发送请求
if (send(m_sockfd, request, strlen(request), 0) < 0) {
LOG_ERROR("[WS] Failed to send handshake request");
return false;
}
// 接收响应
char response[2048];
int n = recv(m_sockfd, response, sizeof(response) - 1, 0);
if (n <= 0) {
LOG_ERROR("[WS] Failed to receive handshake response");
return false;
}
response[n] = '\0';
// 简单验证响应(检查 "101 Switching Protocols"
if (strstr(response, "101") == NULL ||
strstr(response, "Switching Protocols") == NULL) {
LOG_ERROR("[WS] Handshake failed: %s", response);
return false;
}
LOG_DEBUG("[WS] Handshake successful");
return true;
}
void WSClient::recv_loop() {
LOG_INFO("[WS] Receive loop started");
while (m_running && m_connected) {
std::vector<uint8_t> payload;
uint8_t opcode;
if (recv_frame(payload, opcode)) {
if (opcode == OP_TEXT) {
// 文本消息
std::string msg((char*)payload.data(), payload.size());
std::lock_guard<std::mutex> lock(m_recv_mutex);
m_recv_queue.push(msg);
LOG_INFO("[WS] 📩 Received text: %s", msg.c_str());
} else if (opcode == OP_BINARY) {
// 二进制消息 - 保存到队列
std::lock_guard<std::mutex> lock(m_binary_mutex);
m_binary_queue.push(payload);
// Day 12: 提升日志级别,确认服务器是否发送 TTS 音频
LOG_INFO("[WS] 🔔 Received binary: %zu bytes (queue: %zu)", payload.size(), m_binary_queue.size());
} else if (opcode == OP_PING) {
// 回复 PONG
send_frame(payload.data(), payload.size(), OP_PONG);
} else if (opcode == OP_CLOSE) {
LOG_WARN("[WS] Server closed connection");
m_connected = false;
break;
}
} else {
if (m_connected) {
LOG_ERROR("[WS] Failed to receive frame, disconnecting");
m_connected = false;
}
break;
}
}
LOG_INFO("[WS] Receive loop stopped");
}
bool WSClient::send_frame(const uint8_t* data, size_t len, uint8_t opcode) {
if (m_sockfd < 0 || !m_connected) return false;
std::vector<uint8_t> frame;
// 第1字节: FIN=1, opcode
frame.push_back(0x80 | opcode);
// 第2字节及后续: MASK=1, payload长度
uint8_t mask_key[4];
generate_mask_key(mask_key);
if (len < 126) {
frame.push_back(0x80 | (uint8_t)len);
} else if (len < 65536) {
frame.push_back(0x80 | 126);
frame.push_back((len >> 8) & 0xFF);
frame.push_back(len & 0xFF);
} else {
frame.push_back(0x80 | 127);
for (int i = 7; i >= 0; --i) {
frame.push_back((len >> (i * 8)) & 0xFF);
}
}
// Mask key
for (int i = 0; i < 4; ++i) {
frame.push_back(mask_key[i]);
}
// Masked payload
for (size_t i = 0; i < len; ++i) {
frame.push_back(data[i] ^ mask_key[i % 4]);
}
// 发送帧
ssize_t sent = send(m_sockfd, frame.data(), frame.size(), 0);
if (sent < 0 || (size_t)sent != frame.size()) {
LOG_ERROR("[WS] Failed to send frame");
return false;
}
// Day 12: 更新最后发送时间
m_last_send_time = time(nullptr);
return true;
}
bool WSClient::recv_frame(std::vector<uint8_t>& payload, uint8_t& opcode) {
// 读取前2字节
uint8_t header[2];
if (recv(m_sockfd, header, 2, MSG_WAITALL) != 2) {
return false;
}
// bool fin = (header[0] & 0x80) != 0; // 当前未使用
opcode = header[0] & 0x0F;
bool masked = (header[1] & 0x80) != 0;
uint64_t payload_len = header[1] & 0x7F;
// 读取扩展长度
if (payload_len == 126) {
uint8_t len_bytes[2];
if (recv(m_sockfd, len_bytes, 2, MSG_WAITALL) != 2) {
return false;
}
payload_len = ((uint64_t)len_bytes[0] << 8) | len_bytes[1];
} else if (payload_len == 127) {
uint8_t len_bytes[8];
if (recv(m_sockfd, len_bytes, 8, MSG_WAITALL) != 8) {
return false;
}
payload_len = 0;
for (int i = 0; i < 8; ++i) {
payload_len = (payload_len << 8) | len_bytes[i];
}
}
// 读取 mask key服务器不应该发送masked数据
uint8_t mask_key[4] = {0};
if (masked) {
if (recv(m_sockfd, mask_key, 4, MSG_WAITALL) != 4) {
return false;
}
}
// 读取 payload
payload.resize(payload_len);
if (payload_len > 0) {
ssize_t received = recv(m_sockfd, payload.data(), payload_len, MSG_WAITALL);
if (received != (ssize_t)payload_len) {
return false;
}
// 解码如果有mask
if (masked) {
for (size_t i = 0; i < payload_len; ++i) {
payload[i] ^= mask_key[i % 4];
}
}
}
return true;
}
void WSClient::generate_mask_key(uint8_t* mask) {
for (int i = 0; i < 4; ++i) {
mask[i] = rand() % 256;
}
}
// Day 12: 心跳 PING 帧发送
void WSClient::send_ping() {
if (m_sockfd >= 0 && m_connected) {
send_frame(nullptr, 0, OP_PING);
LOG_INFO("[WS] 💓 Sent PING heartbeat");
}
}

View File

@@ -0,0 +1,130 @@
/**
* @file ws_client.h
* @brief WebSocket 客户端封装 - 轻量级实现(无 libuwsc 依赖)
*
* 功能:
* - 连接到 WebSocket 服务器
* - 发送文本/二进制消息
* - 接收消息回调
* - 线程安全的消息队列
*/
#ifndef WS_CLIENT_H
#define WS_CLIENT_H
#include <string>
#include <functional>
#include <atomic>
#include <thread>
#include <mutex>
#include <queue>
#include <vector>
#include <cstdint>
class WSClient {
public:
using MessageCallback = std::function<void(const std::string&)>;
/**
* @brief 构造函数
* @param host 服务器地址
* @param port 服务器端口
* @param path WebSocket 路径 (如 "/ws/camera")
*/
WSClient(const std::string& host, int port, const std::string& path);
~WSClient();
/**
* @brief 连接到服务器
* @return true 成功, false 失败
*/
bool connect();
/**
* @brief 断开连接
*/
void disconnect();
/**
* @brief 检查是否已连接
*/
bool is_connected() const { return m_connected; }
/**
* @brief 发送文本消息
* @param text 文本内容
* @return true 成功, false 失败
*/
bool send_text(const std::string& text);
/**
* @brief 发送二进制消息
* @param data 二进制数据
* @param size 数据大小
* @return true 成功, false 失败
*/
bool send_binary(const uint8_t* data, size_t size);
/**
* @brief 轮询消息 (处理接收到的消息)
* @param callback 消息处理回调函数
*/
void poll_messages(MessageCallback callback);
/**
* @brief 轮询二进制数据 (处理接收到的二进制帧)
* @param callback 二进制数据处理回调函数 (data, size)
*/
void poll_binary_messages(std::function<void(const uint8_t*, size_t)> callback);
private:
std::string m_host;
int m_port;
std::string m_path;
int m_sockfd;
std::atomic<bool> m_connected;
std::atomic<bool> m_running;
std::thread m_recv_thread;
// 发送队列
struct Message {
std::vector<uint8_t> data;
bool is_binary;
};
std::queue<Message> m_send_queue;
std::mutex m_send_mutex;
// 接收队列 (文本消息)
std::queue<std::string> m_recv_queue;
std::mutex m_recv_mutex;
// 接收队列 (二进制数据)
std::queue<std::vector<uint8_t>> m_binary_queue;
std::mutex m_binary_mutex;
// 内部实现
bool perform_handshake();
void recv_loop();
bool send_frame(const uint8_t* data, size_t len, uint8_t opcode);
bool recv_frame(std::vector<uint8_t>& payload, uint8_t& opcode);
void generate_mask_key(uint8_t* mask);
// Day 12: 心跳机制 (使用普通 time_t 避免 32 位平台原子操作问题)
time_t m_last_send_time = 0; // 上次发送时间
static const int HEARTBEAT_INTERVAL = 25; // 心跳间隔(秒)
void send_ping(); // 发送 PING 帧
// WebSocket 操作码
enum Opcode {
OP_CONTINUATION = 0x0,
OP_TEXT = 0x1,
OP_BINARY = 0x2,
OP_CLOSE = 0x8,
OP_PING = 0x9,
OP_PONG = 0xA
};
};
#endif // WS_CLIENT_H

View File

@@ -0,0 +1,45 @@
/**
* @file logger.cpp
* @brief 日志系统实现
*/
#include "logger.h"
// 静态成员初始化
LogLevel Logger::s_log_level = LOG_LEVEL_INFO;
// ==========================================
// 用于解决 eyesee-mpp 库的链接依赖 (stub)
// 替代 liblog/libglog避免动态链接错误
// ==========================================
#include <cstdarg>
#include <cstdio>
extern "C" {
// 模拟 log_printf
void log_printf(int level, const char* fmt, ...) {
// 简单映射一下级别 (eyesee-mpp 定义可能不同,这不重要)
if (level < 2) return; // 忽略低级别日志
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
// fprintf(stderr, "\n");
}
// 某些库可能还需要这些符号
// void log_set_level(int level) { (void)level; } // libcdx_base 已定义
// int log_get_level() { return 1; } // libcdx_base 已定义
void alog(int level, const char* tag, const char* fmt, ...) {
(void)level; (void)tag; (void)fmt;
}
void SLOGD(const char* fmt, ...) { (void)fmt; }
void SLOGE(const char* fmt, ...) { (void)fmt; }
void SLOGW(const char* fmt, ...) { (void)fmt; }
void SLOGI(const char* fmt, ...) { (void)fmt; }
void SLOGV(const char* fmt, ...) { (void)fmt; }
} // extern "C"

View File

@@ -0,0 +1,61 @@
/**
* @file logger.h
* @brief 简易日志系统
*/
#ifndef LOGGER_H
#define LOGGER_H
#include <cstdio>
#include <ctime>
#include <cstdarg>
enum LogLevel {
LOG_LEVEL_DEBUG = 0,
LOG_LEVEL_INFO = 1,
LOG_LEVEL_WARN = 2,
LOG_LEVEL_ERROR = 3
};
class Logger {
public:
static void init(LogLevel level) {
s_log_level = level;
}
static void log(LogLevel level, const char* file, int line, const char* fmt, ...) {
if (level < s_log_level) return;
// 时间戳
time_t now = time(nullptr);
struct tm* t = localtime(&now);
char timestamp[32];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", t);
// 日志级别
const char* level_str[] = {"DEBUG", "INFO", "WARN", "ERROR"};
// 输出前缀
fprintf(stderr, "[%s] [%s] ", timestamp, level_str[level]);
// 输出消息
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fprintf(stderr, " (%s:%d)\n", file, line);
fflush(stderr);
}
private:
static LogLevel s_log_level;
};
// 宏定义
#define LOG_DEBUG(fmt, ...) Logger::log(LOG_LEVEL_DEBUG, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) Logger::log(LOG_LEVEL_INFO, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) Logger::log(LOG_LEVEL_WARN, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) Logger::log(LOG_LEVEL_ERROR, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#endif // LOGGER_H

BIN
docs/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

BIN
docs/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

1366
docs/AvaotaF1.md Normal file

File diff suppressed because one or more lines are too long

21
docs/引脚.md Normal file
View File

@@ -0,0 +1,21 @@
### 扬声器MAX98357A
VIN-3V3
GND-GND
SD-3V3
GAIN-GND
DIN-PD15
BCLK-PD12
LRC-PD13
### IMU
VCC-3V3
GND-GND
AD0-PD4
SDA-PD2
SCL-PD3
CS-PD5