Init: 导入AvaotaF1客户端源码
This commit is contained in:
68
.gitignore
vendored
68
.gitignore
vendored
@@ -1,34 +1,34 @@
|
||||
# ---> C++
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# ---> C++
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
|
||||
1068
Device_Tree/board.dts
Normal file
1068
Device_Tree/board.dts
Normal file
File diff suppressed because it is too large
Load Diff
95
ESP32S3/ICM42688.cpp
Normal file
95
ESP32S3/ICM42688.cpp
Normal 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
85
ESP32S3/ICM42688.h
Normal 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
60
ESP32S3/camera_pins.h
Normal 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
1014
ESP32S3/compile.ino
Normal file
File diff suppressed because it is too large
Load Diff
348
README.md
348
README.md
@@ -1,2 +1,346 @@
|
||||
# NaviGlassFirmware
|
||||
|
||||
# Avaota F1 开发与集成
|
||||
|
||||
> 面向仓库根目录的总览文档,整合 Day1–Day9 日志、任务清单与完成总结。
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目简介
|
||||
|
||||
本项目面向 **Avaota F1 AI 机器人**,基于 **全志 V821 / 32-bit RISC‑V** SoC,目标是在 Tina Linux 固件上实现一套「可量产」的机器人终端固件,集成:
|
||||
|
||||
- 音频采集(板载模拟麦克风)与播放(I2S + MAX98357A)
|
||||
- IMU 六轴姿态传感(ICM‑42688‑P)
|
||||
- GC2083 MIPI 摄像头 JPEG 图像采集
|
||||
- UDP / HTTP / WebSocket 网络通讯
|
||||
- 多线程主程序与静态链接交叉编译流程
|
||||
|
||||
文档与代码组织按「Day1–Day9 开发日志 + 任务清单 + 完成总结」推进,可作为以后移植到其他 Tina Linux / 全志平台的模板工程。
|
||||
|
||||
目前对话的位置是本地Windows 11系统的主机,用于开发的环境是局域网中的Ubuntu服务器。
|
||||
|
||||
---
|
||||
|
||||
## 2. 硬件与开发环境
|
||||
|
||||
### 2.1 目标硬件(板端)
|
||||
|
||||
- **SoC**:全志 V821,32‑bit RISC‑V 架构
|
||||
- **板载外设**
|
||||
- 模拟麦克风 → 内置 Audio Codec(audiocodec)
|
||||
- I2S 数字功放:MAX98357A(BCLK/LRCK/DOUT:PD12/PD13/PD15)
|
||||
- 摄像头:GC2083,MIPI‑CSI2,典型输出 1280×720 @ 20fps
|
||||
- IMU:ICM‑42688‑P(GPIO 模拟 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/ # ICM‑42688 SPI 驱动与测试
|
||||
│ ├── network/ # UDP / HTTP / WebSocket 客户端
|
||||
│ ├── utils/
|
||||
│ ├── main.cpp # 主程序入口(多线程集成)
|
||||
│ ├── main_test.cpp # 本地硬件自检程序
|
||||
│ ├── Makefile # 交叉编译配置
|
||||
│ └── build_*.sh # 构建脚本(main/test/phaseX)
|
||||
├── build_main.sh
|
||||
├── logs.md # 编译时的日志(实时填写)
|
||||
└── README.md # 本文档
|
||||
```
|
||||
|
||||
你可以直接把 Day1–Day7、任务清单与完成总结放到 `../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 传感器(ICM‑42688‑P)
|
||||
|
||||
- 最终采用 **GPIO 模拟 SPI**:
|
||||
- SCLK: PD3
|
||||
- MOSI: PD2
|
||||
- MISO: PD4
|
||||
- CS : PD5
|
||||
- SPI Mode0,软件 bit‑bang,速率约 500 kHz(可按需提升)
|
||||
- 主要特性:
|
||||
- WHO_AM_I = 0x47 识别验证
|
||||
- 加速度计:±16g,1 kHz ODR
|
||||
- 陀螺仪:±2000 °/s,1 kHz ODR
|
||||
- 温度通道:用于环境监控与漂移补偿
|
||||
- 静止状态验证:
|
||||
- 合加速度 ≈ 9.8 m/s²
|
||||
- 陀螺仪接近 0 °/s
|
||||
- 上层封装:
|
||||
- `ICM42688` 类提供采样与单位转换
|
||||
- 独立 `test_imu` 自检程序
|
||||
- 在主程序中通过 UDP 周期性上报 JSON/结构体数据
|
||||
|
||||
### 4.3 摄像头系统(GC2083 + MPP)
|
||||
|
||||
- 使用 Allwinner Eyesee‑MPP 框架:SYS → VI → ISP → VENC
|
||||
- 流水线:
|
||||
- VI(接 MIPI 摄像头)
|
||||
- ISP(自动曝光、白平衡、降噪)
|
||||
- VENC(JPEG 编码)
|
||||
- 关键配置:
|
||||
- 分辨率:1280×720 @ 20fps(可调)
|
||||
- JPEG 质量:80(在质量与码率之间折中)
|
||||
- VI Buffer:5 帧
|
||||
- VBV Buffer:4 MB,避免 `VBV FULL` 错误
|
||||
- `Camera` 类职责:
|
||||
- 完成 MPP 初始化 / 绑定 / 销毁
|
||||
- 按需抓拍单帧 JPEG(用于 WebSocket 发送或本地保存)
|
||||
- 提供阻塞式 `capture_frame()` 接口与重试机制
|
||||
- 测试程序:
|
||||
- `test_camera` 抓拍多张 JPEG 保存到 SD 卡
|
||||
- 实测成功率 100%,文件大小 20–80 KB 之间,画面曝光正常
|
||||
|
||||
### 4.4 网络通讯
|
||||
|
||||
**UDP**
|
||||
|
||||
- 轻量 IMU 数据上报通道
|
||||
- 使用 POSIX socket,静态链接,无额外依赖
|
||||
- 典型用法:
|
||||
- `UDPSender` 初始化目标 IP/Port
|
||||
- 周期性发送 IMU/状态数据
|
||||
|
||||
**HTTP(libcurl 或轻量封装)**
|
||||
|
||||
- 主要用途:
|
||||
- 从服务器拉取 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 bit‑bang
|
||||
- 对功耗与休眠策略进行优化
|
||||
- 引入简单看门狗机制,防止长期运行卡死
|
||||
|
||||
---
|
||||
|
||||
## 9. 如何使用本 README
|
||||
|
||||
- **新成员上手**:从本 README 入手,结合 `../Docs/DevLogs/DayX.md` 逐步了解每个子系统的设计与坑点。
|
||||
- **以后复用到新项目**:可以直接复制「编译流程」「外设接线 + DTS 配置」「主程序多线程架构」这几部分,稍作修改即可移植到其他全志 / RISC‑V 设备。
|
||||
- **代码导航入口**:按模块查找 `src/audio`, `src/imu`, `src/camera`, `src/network` 目录,结合对应的 DayX 日志阅读。
|
||||
|
||||
祝使用愉快 🎉,也欢迎在后续开发阶段继续补充和更新本 README。
|
||||
|
||||
375
avaota_app_demo/MUSL_COMPILE.md
Normal file
375
avaota_app_demo/MUSL_COMPILE.md
Normal 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
168
avaota_app_demo/README.md
Normal 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` 正常显示依赖
|
||||
- [ ] 程序成功运行
|
||||
|
||||
---
|
||||
|
||||
**祝编译顺利!** 🚀
|
||||
113
avaota_app_demo/build_main.sh
Normal file
113
avaota_app_demo/build_main.sh
Normal 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
|
||||
267
avaota_app_demo/src/Makefile
Normal file
267
avaota_app_demo/src/Makefile
Normal 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"
|
||||
357
avaota_app_demo/src/audio/audio_capture.cpp
Normal file
357
avaota_app_demo/src/audio/audio_capture.cpp
Normal 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;
|
||||
}
|
||||
49
avaota_app_demo/src/audio/audio_capture.h
Normal file
49
avaota_app_demo/src/audio/audio_capture.h
Normal 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
|
||||
327
avaota_app_demo/src/audio/audio_player.cpp
Normal file
327
avaota_app_demo/src/audio/audio_player.cpp
Normal 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;
|
||||
}
|
||||
49
avaota_app_demo/src/audio/audio_player.h
Normal file
49
avaota_app_demo/src/audio/audio_player.h
Normal 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
|
||||
425
avaota_app_demo/src/camera/camera.cpp
Normal file
425
avaota_app_demo/src/camera/camera.cpp
Normal 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: 降低到2MB,640x480不需要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;
|
||||
}
|
||||
}
|
||||
100
avaota_app_demo/src/camera/camera.h
Normal file
100
avaota_app_demo/src/camera/camera.h
Normal 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
|
||||
349
avaota_app_demo/src/imu/icm42688.cpp
Normal file
349
avaota_app_demo/src/imu/icm42688.cpp
Normal 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;
|
||||
}
|
||||
202
avaota_app_demo/src/imu/icm42688.h
Normal file
202
avaota_app_demo/src/imu/icm42688.h
Normal 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
|
||||
377
avaota_app_demo/src/main.cpp
Normal file
377
avaota_app_demo/src/main.cpp
Normal 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;
|
||||
}
|
||||
70
avaota_app_demo/src/network/udp_sender.cpp
Normal file
70
avaota_app_demo/src/network/udp_sender.cpp
Normal 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;
|
||||
}
|
||||
37
avaota_app_demo/src/network/udp_sender.h
Normal file
37
avaota_app_demo/src/network/udp_sender.h
Normal 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
|
||||
392
avaota_app_demo/src/network/ws_client.cpp
Normal file
392
avaota_app_demo/src/network/ws_client.cpp
Normal 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");
|
||||
}
|
||||
}
|
||||
130
avaota_app_demo/src/network/ws_client.h
Normal file
130
avaota_app_demo/src/network/ws_client.h
Normal 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
|
||||
45
avaota_app_demo/src/utils/logger.cpp
Normal file
45
avaota_app_demo/src/utils/logger.cpp
Normal 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"
|
||||
61
avaota_app_demo/src/utils/logger.h
Normal file
61
avaota_app_demo/src/utils/logger.h
Normal 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
BIN
docs/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 528 KiB |
BIN
docs/2.png
Normal file
BIN
docs/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 585 KiB |
1366
docs/AvaotaF1.md
Normal file
1366
docs/AvaotaF1.md
Normal file
File diff suppressed because one or more lines are too long
21
docs/引脚.md
Normal file
21
docs/引脚.md
Normal 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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user