chore: migrate project into clean repository

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

12
arcs-sdk/docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
build
output
zh/components
zh/boards
zh/drivers
zh/_static
zh/samples
zh/modules
zh/demos
zh/tools
zh/CHANGELOG.md
_ext/__pycache__

View File

@@ -0,0 +1,246 @@
# 文档贡献指南
本指南说明如何为 ARCS SDK 贡献高质量的组件文档和示例文档。
## 📋 文档结构概览
### Components 组件文档
```
arcs-sdk/components/
├── <component_name>/ # 组件目录
│ ├── README.md # 组件文档(必需)
│ ├── CMakeLists.txt # 构建配置
│ ├── Kconfig # 配置选项
│ ├── include/ # 头文件
│ └── src/ # 源代码
```
### Samples 示例文档
```
arcs-sdk/samples/
├── <category>/ # 分类目录
│ ├── <sample_name>/ # 示例项目
│ │ ├── CMakeLists.txt # 构建配置
│ │ ├── sample.yaml # 测试配置(必需)
│ │ ├── README.md # 示例文档(推荐)
│ │ ├── prj.conf # 项目配置
│ │ ├── Kconfig # 配置选项
│ │ └── src/ # 源代码
│ └── index_zh.rst # 分类索引
```
## 🔧 组件文档编写指南
### 文档模板结构
每个组件的 `README.md` 应包含以下标准章节:
```markdown
# [组件名称] 组件
[组件简介 - 一句话概括组件功能]
## 📖 组件概述
### 主要功能
- **功能1**:详细说明
- **功能2**:详细说明
### 支持的硬件/协议
| 型号/协议 | 类型 | 特殊功能 |
|-----------|------|----------|
| 项目1 | 类型 | 说明 |
## 🚀 快速开始
### 1. 配置项启用
### 2. 硬件配置
### 3. 设备初始化
### 4. 基本操作
## 🔧 配置选项
## 📋 API 参考
### 核心数据结构
### 主要API函数
## ⚠️ 注意事项
```
### 编写规范
#### **标题和结构**
- 使用清晰的 Markdown 标题层次H1-H4
- H1 用于组件名称H2 用于主要章节H3 用于子章节
- 使用 emoji 图标增强可读性(📖 📋 🚀 🔧 ⚠️)
#### **代码示例**
- 所有代码块必须指定语言类型:`c``kconfig``bash`
- 提供完整的、可运行的代码示例
- 包含错误处理和边界条件检查
- 添加必要的注释说明
#### **配置说明**
- 详细说明所有 Kconfig 配置项
- 提供配置项的默认值和可选值
- 说明配置项之间的依赖关系
#### **API文档**
- 按功能分组描述API函数
- 包含函数参数、返回值说明
- 提供数据结构的完整定义
- 举例说明常见用法
#### **表格使用**
- 使用表格组织配置项、硬件支持等信息
- 保持表格简洁易读
- 包含必要的说明列
## 📝 示例文档编写指南
### 示例项目结构
每个示例项目必须包含:
#### **必需文件**
- `sample.yaml` - 测试配置文件
- `CMakeLists.txt` - 构建配置
- `src/main.c` - 主程序源码
#### **推荐文件**
- `README.md` - 示例说明文档
- `prj.conf` - 项目配置
- `Kconfig` - 自定义配置项
- `build.sh` - 构建脚本
### 示例文档模板
```markdown
# [示例名称]
[示例功能简介]
## 📖 示例说明
### 功能演示
- 演示功能1
- 演示功能2
### 硬件要求
- 硬件要求说明
## 🚀 快速开始
### 1. 构建项目
### 2. 烧录运行
### 3. 查看输出
## 🔧 配置说明
## 📋 代码解析
### 关键代码段
## 🔍 预期输出
## ⚠️ 注意事项
```
## 📚 文档索引管理
### 组件索引更新
`arcs-sdk/components/index_zh.rst` 中添加新组件:
```rst
.. _components:
组件
====
.. toctree::
:maxdepth: 1
display/README.md
touch/README.md
<new_component>/README.md # 新增组件
```
### 示例索引更新
在相应分类的 `index_zh.rst` 中添加新示例:
```rst
.. _samples_category:
[分类名称]
==========
.. toctree::
:maxdepth: 1
existing_sample/README.md
new_sample/README.md # 新增示例
```
## ✅ 质量检查清单
### 组件文档检查
- [ ] 包含完整的功能概述
- [ ] 提供清晰的快速开始指南
- [ ] 详细说明所有配置选项
- [ ] 包含完整的API参考
- [ ] 包含注意事项和限制说明
- [ ] 更新了组件索引文件
### 示例文档检查
- [ ] `sample.yaml` 配置正确
- [ ] 构建脚本正常工作
- [ ] 代码风格符合项目规范
- [ ] 包含清晰的使用说明
- [ ] 预期输出描述准确
- [ ] 更新了相应的索引文件
### 通用质量要求
- [ ] 中文表达规范,语句通顺
- [ ] Markdown 格式正确
- [ ] 链接和引用有效
- [ ] 图片和表格清晰
- [ ] 拼写和语法无误
## 🛠️ 本地测试
### 文档构建测试
```bash
cd arcs-sdk/docs
make zh # 构建中文文档
# 检查 output/zh/html/ 中的生成结果
```
### 示例构建测试
```bash
cd arcs-sdk/samples/<category>/<sample_name>
./build.sh # 构建示例
# 检查构建是否成功
```
## 📬 提交流程
1. **创建分支**:基于主分支创建功能分支
2. **编写文档**:按照本指南编写文档
3. **本地测试**:确保文档构建和示例编译正常
4. **质量检查**:使用检查清单验证文档质量
5. **提交PR**创建Pull Request并请求代码审查
## 🤝 寻求帮助
如有文档编写疑问,可以:
- 参考现有优质文档(如 `display` 组件)
- 查阅本项目的代码风格指南
- 在项目讨论区提问
- 联系维护团队获取支持
---
**感谢您为 ARCS SDK 文档做出的贡献!** 🙏

37
arcs-sdk/docs/Makefile Normal file
View File

@@ -0,0 +1,37 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
#SPHINXBUILD ?= sphinx-build
SPHINXBUILD ?= python3 -m sphinx.cmd.build
#CMAKE_RST_GEN ?= python3 ../scripts/generate_cmake_rst.py --skip_private --skip_undocumented --headline="SDK CMake API Reference" ../cmake
all: html
#cmake_intro:
# @$(CMAKE_RST_GEN) -o cmake_intro.rst
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
en: Makefile
@$(SPHINXBUILD) -M html "en" "output/en" $(SPHINXOPTS) $(O)
zh: Makefile
@$(SPHINXBUILD) -M html "zh" "output/zh" $(SPHINXOPTS) $(O)
html: zh
.PHONY: all zh html clean
clean:
@rm -rf output
@rm -rf zh/samples
@rm -rf zh/components
@rm -rf zh/_static
@rm -rf zh/drivers
@rm -rf zh/modules
@rm -rf zh/boards
@rm -rf zh/CHANGELOG.md
@rm -rf zh/tools
@rm -rf zh/demos

146
arcs-sdk/docs/README.md Normal file
View File

@@ -0,0 +1,146 @@
# 本地文档构建指南
本文档指导您如何在本地环境中构建和生成项目文档。
## 文档架构说明
本项目采用**Sphinx + Doxygen**混合架构生成多语言技术文档,支持中英文双语输出。
### 架构组成
```
docs/
├── zh/ # 中文文档源文件
│ ├── conf.py # Sphinx中文配置文件
│ ├── index.rst # 中文文档主页
│ ├── get_started.rst # 快速开始指南
│ ├── components/ # 组件文档
│ └── api_doc.md # API文档入口
├── doxygen/ # Doxygen配置与定制
│ ├── Doxyfile # Doxygen配置文件
│ ├── mainpage.md # API文档主页
│ └── custom/ # 自定义样式和模板
├── _ext/ # Sphinx扩展插件
│ ├── doxyrunner.py # Doxygen集成插件
│ └── external_content.py # 外部内容处理
├── assets/ # 静态资源文件
├── requirements.txt # Python依赖列表
├── Makefile # 构建配置
└── README.md # 本指南
```
### 文档生成流程
1. **Sphinx处理**处理RST/MD格式的用户文档
2. **Doxygen集成**自动提取代码注释生成API文档
3. **多语言支持**:通过`make en`/`make zh`生成对应语言版本
4. **输出统一**:最终在`output/`目录生成完整的HTML文档
### 技术特点
- **🌐 多语言支持**:中英文双语文档生成
- **📚 混合文档**结合手写文档和自动API文档
- **🔧 可扩展**:通过`_ext/`目录的插件系统支持自定义功能
- **📱 响应式**使用Read the Docs主题支持移动端阅读
## 环境准备
### 安装系统依赖
本文档构建依赖 Doxygen 工具来生成 API 文档,请先安装:
**Ubuntu/Debian:**
```bash
sudo apt-get install doxygen
```
**其他系统:**
请访问 [Doxygen 官网](https://www.doxygen.nl/download.html) 下载对应平台的安装包。
### 安装 Python 依赖
在开始构建文档之前,还需要安装必要的 Python 依赖包:
```bash
pip install --user -r requirements.txt
```
## 构建文档
### 生成文档
1. 切换到项目的 `docs` 目录:
```bash
cd docs
```
2. 执行构建命令:
**构建所有语言版本(推荐):**
```bash
make
```
**仅构建中文文档:**
```bash
make zh
```
**仅构建英文文档 (暂不支持)**
```bash
make en
```
**将 Sphinx 警告视为错误:**
```bash
make SPHINXOPTS="-W"
```
> 利用 `SPHINXOPTS` 变量向 `sphinx-build` 追加 `-W` 参数,任何警告都会使构建失败;可与 `make`、`make zh` 等目标同时使用,例如 `make zh SPHINXOPTS="-W"`。
**清理构建缓存:**
```bash
make clean
```
### 查看生成结果
文档构建完成后,生成的文档文件将保存在 `output` 目录中:
```
output/
├── zh/ # 中文文档
│ └── html/
│ └── index.html
└── en/ # 英文文档
└── html/
└── index.html
```
您可以通过浏览器打开对应语言目录下的 `index.html` 文件来查看文档内容。
### 故障排除
**常见问题:**
- **依赖缺失**:确保已安装 `requirements.txt` 中的所有依赖包
- **权限问题**:如果使用 `--user` 安装依赖后仍有问题,可尝试使用虚拟环境
- **构建失败**:使用 `make clean` 清理缓存后重新构建
## 📚 文档贡献
### 贡献指南
如果您想为 ARCS SDK 贡献文档,请参阅 [文档贡献指南](CONTRIBUTING.md),其中包含:
- **组件文档编写规范**:标准模板和编写要求
- **示例文档编写指南**:示例项目的文档化要求
- **文档索引管理**:如何正确添加新文档到索引
- **质量检查清单**:确保文档质量的检查要点
- **提交流程**:从编写到发布的完整流程
### 主要贡献领域
- **组件文档** (`components/`): 为SDK组件编写完整的使用文档
- **示例文档** (`samples/`): 为示例项目提供清晰的说明文档
- **API文档**: 通过代码注释改进自动生成的API文档

View File

@@ -0,0 +1,398 @@
# 源码链接自动生成功能使用说明
## 功能概述
该功能通过 Sphinx 扩展自动在示例文档页面顶部添加源码位置信息,解决了在线文档无法知道示例源码路径的问题。
### 效果展示
当用户访问示例文档时,会在页面顶部看到一个美观的源码位置提示框:
```
┌────────────────────────────────────────────────────────┐
│ 📁 源码位置: samples/drivers/devices/lisa_uart/send_sync_int │
│ [查看源码] │
└────────────────────────────────────────────────────────┘
```
点击"查看源码"按钮可以直接跳转到 GitHub/GitLab 仓库对应位置。
## 实现方案
### 1. 核心组件
- **Sphinx 扩展**: `docs/_ext/source_link.py`
- 自动检测示例文档
- 提取源码路径
- 注入链接节点到文档树
- **CSS 样式**: `docs/assets/source-link.css`
- 提供美观的视觉效果
- 支持响应式设计
- 支持深色模式
- **配置文件**: `docs/zh/conf.py`
- 集成扩展
- 配置仓库地址和匹配规则
### 2. 工作流程
```
┌──────────────┐
│ 示例 README.md│
└──────┬───────┘
│ 1. external_content 复制到 docs/zh/
┌──────────────┐
│ Sphinx 构建 │
└──────┬───────┘
│ 2. source_link 扩展检测并处理
┌──────────────┐
│ doctree 注入 │
│ 源码链接节点 │
└──────┬───────┘
│ 3. HTML 生成
┌──────────────┐
│ 最终文档页面 │
│ (含源码链接) │
└──────────────┘
```
## 配置说明
### 基本配置
`docs/zh/conf.py` 中已添加以下配置:
```python
# 添加扩展
extensions = [
# ... 其他扩展
"source_link" # 自动添加源码链接
]
# 源码链接配置
source_link_base_url = os.environ.get(
"SOURCE_LINK_BASE_URL",
"" # 留空则只显示本地路径
)
source_link_patterns = [
"samples/**/*.md",
"samples/**/*.rst",
"demos/**/*.md",
"demos/**/*.rst",
]
source_link_show_local_path = True
source_link_label = "📁 源码位置"
```
### 配置选项详解
#### `source_link_base_url`
- **用途**: 配置 GitHub/GitLab 仓库地址
- **格式**: `https://仓库地址/-/tree/分支名``https://仓库地址/tree/分支名`
- **示例**:
- GitLab: `https://cloud.listenai.com/CSKG836746/arcs-sdk/public/arcs-sdk/-/tree/master`
- GitHub: `https://github.com/listenai/arcs-sdk/tree/master`
- **说明**:
- 如果设置,会在本地路径旁显示"查看源码"按钮
- 如果不设置,只显示本地路径
- 支持通过环境变量 `SOURCE_LINK_BASE_URL` 动态配置
- 分支名会根据文档版本自动替换(见 `source_link_version_branches`)
#### `source_link_version_branches`
- **用途**: 文档版本到 Git 分支的映射
- **格式**: 字典,键为文档版本,值为对应的 Git 分支名
- **示例**:
```python
source_link_version_branches = {
'latest': 'master',
'v0.1.0': 'release/v0.1.0',
'v0.1.1': 'release/v0.1.1',
}
```
- **说明**:
- 自动根据文档版本切换源码链接指向的分支
- 如果版本不在映射中,默认规则:
- `latest` → `master`
- `v0.1.0` → `release/v0.1.0` (自动添加 `release/` 前缀)
- 确保源码链接始终指向对应版本的代码
#### `source_link_patterns`
- **用途**: 指定哪些文档需要添加源码链接
- **格式**: 文件路径通配符列表
- **默认值**: `["samples/**/*.md", "samples/**/*.rst"]`
- **说明**:
- 支持通配符 `**` 匹配任意层级目录
- 可以添加更多模式,如 `demos/**/*.md`
#### `source_link_show_local_path`
- **用途**: 是否显示本地路径
- **类型**: Boolean
- **默认值**: `True`
- **说明**:
- `True`: 显示完整本地路径
- `False`: 只显示"查看源码"按钮(需要配置 base_url)
#### `source_link_label`
- **用途**: 自定义标签文本
- **类型**: String
- **默认值**: `"📁 源码位置"`
- **说明**: 可以改为其他文本,如 `"🔗 源代码"`, `"📂 Source"`
## 使用方法
### 方法 1: 版本感知 + 本地路径(推荐,当前配置)
适用于有多版本文档的情况,自动根据文档版本链接到对应分支。
**配置**:
```python
source_link_base_url = "https://cloud.listenai.com/CSKG836746/arcs-sdk/public/arcs-sdk/-/tree/master"
source_link_show_local_path = True
source_link_version_branches = {
'latest': 'master',
'v0.1.0': 'release/v0.1.0',
'v0.1.1': 'release/v0.1.1',
}
```
**效果**:
- **latest 版本文档**: `📁 源码位置: samples/... [查看源码]` → 链接到 `master` 分支
- **v0.1.0 版本文档**: `📁 源码位置: samples/... [查看源码]` → 链接到 `release/v0.1.0` 分支
- **v0.1.1 版本文档**: `📁 源码位置: samples/... [查看源码]` → 链接到 `release/v0.1.1` 分支
### 方法 2: 仅显示本地路径
适用于内部文档或没有公开仓库的情况。
**配置**:
```python
source_link_base_url = ""
source_link_show_local_path = True
```
**效果**:
```
📁 源码位置: samples/drivers/devices/lisa_uart/send_sync_int
```
### 方法 3: 固定分支链接
适用于只有单一版本或所有版本共用同一分支的情况。
**配置**:
```python
source_link_base_url = "https://github.com/listenai/arcs-sdk/tree/master"
source_link_show_local_path = True
# 不设置 source_link_version_branches
```
**效果**:
```
📁 源码位置: samples/drivers/devices/lisa_uart/send_sync_int [查看源码]
```
(所有版本都链接到 master 分支)
### 方法 4: 仅显示链接按钮
适用于希望极简显示的情况。
**配置**:
```python
source_link_base_url = "https://github.com/listenai/arcs-sdk/tree/master"
source_link_show_local_path = False
```
**效果**:
```
📁 源码位置: [查看源码]
```
## 构建和测试
### 本地测试
1. **设置仓库地址**(可选):
```bash
export SOURCE_LINK_BASE_URL="https://github.com/listenai/arcs-sdk/tree/master"
```
2. **构建文档**:
```bash
cd docs
make clean
make html
```
3. **查看效果**:
```bash
# 在浏览器中打开
firefox zh/_build/html/samples/drivers/devices/lisa_uart/send_sync_int/README.html
```
### CI/CD 集成
在构建脚本或 CI 配置中添加环境变量:
```yaml
# .github/workflows/docs.yml
- name: Build docs
env:
SOURCE_LINK_BASE_URL: "https://github.com/${{ github.repository }}/tree/${{ github.ref_name }}"
run: |
cd docs
make html
```
或 GitLab CI:
```yaml
# .gitlab-ci.yml
build-docs:
variables:
SOURCE_LINK_BASE_URL: "https://gitlab.com/$CI_PROJECT_PATH/-/tree/$CI_COMMIT_REF_NAME"
script:
- cd docs
- make html
```
## 自定义样式
如果需要修改源码链接框的外观,编辑 `docs/assets/source-link.css`:
### 预设的简洁样式
文件中包含了一个备选的简洁样式(已注释),如需使用:
1. 注释掉当前的渐变样式
2. 取消注释简洁样式部分
### 自定义颜色
修改 CSS 中的颜色变量:
```css
.source-link-box {
background: linear-gradient(135deg, #YOUR_COLOR_1 0%, #YOUR_COLOR_2 100%);
border-left-color: #YOUR_BORDER_COLOR;
}
```
## 扩展到其他文档类型
如果需要为其他类型的文档添加源码链接,只需在配置中添加对应的模式:
```python
source_link_patterns = [
"samples/**/*.md",
"samples/**/*.rst",
"demos/**/*.md",
"demos/**/*.rst",
"components/**/*.md", # 组件文档
"drivers/**/*.md", # 驱动文档
"boards/**/*.md", # 板级文档
]
```
## 故障排查
### 源码链接没有显示
1. **检查文档路径是否匹配模式**:
```python
# 确认 source_link_patterns 包含对应的路径模式
```
2. **检查扩展是否正确加载**:
```bash
# 查看构建日志中是否有 source_link 相关的错误
make html 2>&1 | grep source_link
```
3. **检查 CSS 是否加载**:
```bash
# 确认 HTML 中包含 source-link.css
grep "source-link.css" zh/_build/html/samples/**/README.html
```
### 链接路径不正确
1. **检查 SDK_BASE 环境变量**:
```python
# 在 conf.py 中确认
print(f"SDK_BASE: {SDK_BASE}")
```
2. **检查 external_content 配置**:
```python
# 确认文件被正确复制到 docs/zh/
```
### 样式显示异常
1. **清理构建缓存**:
```bash
cd docs
make clean
make html
```
2. **检查浏览器缓存**:
- 强制刷新页面 (Ctrl+Shift+R)
- 或清除浏览器缓存
## 优势总结
### ✅ 自动化
- 无需手动修改每个 README.md
- 构建时自动处理
- 维护成本低
### ✅ 通用性
- 适用于所有示例文档
- 支持多种仓库(GitHub/GitLab/Gitee)
- 可扩展到其他文档类型
### ✅ 可配置
- 灵活的显示选项
- 支持环境变量配置
- 易于集成到 CI/CD
### ✅ 用户友好
- 美观的视觉效果
- 一键跳转到源码
- 响应式设计,支持移动端
## 后续优化建议
1. **添加多语言支持**:
```python
source_link_label_zh = "📁 源码位置"
source_link_label_en = "📁 Source Location"
```
2. **支持多仓库**:
```python
source_link_repos = {
"samples": "https://github.com/org/samples",
"demos": "https://github.com/org/demos",
}
```
3. **添加编辑链接**:
在源码链接旁添加"编辑此页"按钮,方便贡献者直接编辑文档。
4. **集成版本信息**:
根据文档版本自动切换 GitHub 分支或 tag。
## 技术支持
如有问题或建议,请联系文档团队或提交 Issue。

View File

@@ -0,0 +1,399 @@
"""
Doxyrunner Sphinx Plugin
########################
Copyright (c) 2021 Nordic Semiconductor ASA
SPDX-License-Identifier: Apache-2.0
Introduction
============
This Sphinx plugin can be used to run Doxygen build as part of the Sphinx build
process. It is meant to be used with other plugins such as ``breathe`` in order
to improve the user experience. The principal features offered by this plugin
are:
- Doxygen build is run before Sphinx reads input files
- Doxyfile can be optionally pre-processed so that variables can be inserted
- Changes in the Doxygen input files are tracked so that Doxygen build is only
run if necessary.
- Synchronizes Doxygen XML output so that even if Doxygen is run only changed,
deleted or added files are modified.
References:
- https://github.com/michaeljones/breathe/issues/420
Configuration options
=====================
- ``doxyrunner_doxygen``: Path to the Doxygen binary.
- ``doxyrunner_doxyfile``: Path to Doxyfile.
- ``doxyrunner_outdir``: Doxygen build output directory (inserted to
``OUTPUT_DIRECTORY``)
- ``doxyrunner_outdir_var``: Variable representing the Doxygen build output
directory, as used by ``OUTPUT_DIRECTORY``. This can be useful if other
Doxygen variables reference to the output directory.
- ``doxyrunner_fmt``: Flag to indicate if Doxyfile should be formatted.
- ``doxyrunner_fmt_vars``: Format variables dictionary (name: value).
- ``doxyrunner_fmt_pattern``: Format pattern.
- ``doxyrunner_silent``: If Doxygen output should be logged or not. Note that
this option may not have any effect if ``QUIET`` is set to ``YES``.
"""
import filecmp
import hashlib
from pathlib import Path
import re
import shlex
import shutil
from subprocess import Popen, PIPE, STDOUT
import tempfile
from typing import List, Dict, Optional, Any
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.util import logging
__version__ = "0.1.0"
logger = logging.getLogger(__name__)
def hash_file(file: Path) -> str:
"""Compute the hash (SHA256) of a file in text mode.
Args:
file: File to be hashed.
Returns:
Hash.
"""
with open(file, encoding="utf-8") as f:
sha256 = hashlib.sha256(f.read().encode("utf-8"))
return sha256.hexdigest()
def get_doxygen_option(doxyfile: str, option: str) -> List[str]:
"""Obtain the value of a Doxygen option.
Args:
doxyfile: Content of the Doxyfile.
option: Option to be retrieved.
Notes:
Does not support appended values.
Returns:
Option values.
"""
option_re = re.compile(r"^\s*([A-Z0-9_]+)\s*=\s*(.*)$")
multiline_re = re.compile(r"^\s*(.*)$")
values = []
found = False
finished = False
for line in doxyfile.splitlines():
if not found:
m = option_re.match(line)
if not m or m.group(1) != option:
continue
found = True
value = m.group(2)
else:
m = multiline_re.match(line)
if not m:
raise ValueError(f"Unexpected line content: {line}")
value = m.group(1)
# check if it is a multiline value
finished = not value.endswith("\\")
# strip backslash
if not finished:
value = value[:-1]
# split values
values += shlex.split(value.replace("\\", "\\\\"))
if finished:
break
return values
def process_doxyfile(
doxyfile: str,
outdir: Path,
silent: bool,
fmt: bool = False,
fmt_pattern: Optional[str] = None,
fmt_vars: Optional[Dict[str, str]] = None,
outdir_var: Optional[str] = None,
) -> str:
"""Process Doxyfile.
Notes:
OUTPUT_DIRECTORY, WARN_FORMAT and QUIET are overridden to satisfy
extension operation needs.
Args:
doxyfile: Path to the Doxyfile.
outdir: Output directory of the Doxygen build.
silent: If Doxygen should be run in quiet mode or not.
fmt: If Doxyfile should be formatted.
fmt_pattern: Format pattern.
fmt_vars: Format variables.
outdir_var: Variable representing output directory.
Returns:
Processed Doxyfile content.
"""
with open(doxyfile) as f:
content = f.read()
content = re.sub(
r"^\s*OUTPUT_DIRECTORY\s*=.*$",
f"OUTPUT_DIRECTORY={outdir.as_posix()}",
content,
flags=re.MULTILINE,
)
content = re.sub(
r"^\s*WARN_FORMAT\s*=.*$",
'WARN_FORMAT="$file:$line: $text"',
content,
flags=re.MULTILINE,
)
content = re.sub(
r"^\s*QUIET\s*=.*$",
"QUIET=" + "YES" if silent else "NO",
content,
flags=re.MULTILINE,
)
if fmt:
if not fmt_pattern or not fmt_vars:
raise ValueError("Invalid formatting pattern or variables")
if outdir_var:
fmt_vars = fmt_vars.copy()
fmt_vars[outdir_var] = outdir.as_posix()
for var, value in fmt_vars.items():
content = content.replace(fmt_pattern.format(var), value)
return content
def doxygen_input_has_changed(env: BuildEnvironment, doxyfile: str) -> bool:
"""Check if Doxygen input files have changed.
Args:
env: Sphinx build environment instance.
doxyfile: Doxyfile content.
Returns:
True if changed, False otherwise.
"""
# obtain Doxygen input files and patterns
input_files = get_doxygen_option(doxyfile, "INPUT")
if not input:
raise ValueError("No INPUT set in Doxyfile")
file_patterns = get_doxygen_option(doxyfile, "FILE_PATTERNS")
if not file_patterns:
raise ValueError("No FILE_PATTERNS set in Doxyfile")
# build a set with input files hash
cache = set()
for file in input_files:
path = Path(file)
if path.is_file():
cache.add(hash_file(path))
else:
for pattern in file_patterns:
for p_file in path.glob("**/" + pattern):
cache.add(hash_file(p_file))
# check if any file has changed
if hasattr(env, "doxyrunner_cache") and env.doxyrunner_cache == cache:
return False
# store current state
env.doxyrunner_cache = cache
return True
def process_doxygen_output(line: str, silent: bool) -> None:
"""Process a line of Doxygen program output.
This function will map Doxygen output to the Sphinx logger output. Errors
and warnings will be converted to Sphinx errors and warnings. Other
messages, if not silent, will be mapped to the info logger channel.
Args:
line: Doxygen program line.
silent: True if regular messages should be logged, False otherwise.
"""
m = re.match(r"(.*):(\d+): ([a-z]+): (.*)", line)
if m:
type = m.group(3)
message = f"{m.group(1)}:{m.group(2)}: {m.group(4)}"
if type == "error":
logger.error(message)
elif type == "warning":
logger.warning(message)
else:
logger.info(message)
elif not silent:
logger.info(line)
def run_doxygen(doxygen: str, doxyfile: str, silent: bool = False) -> None:
"""Run Doxygen build.
Args:
doxygen: Path to Doxygen binary.
doxyfile: Doxyfile content.
silent: If Doxygen output should be logged or not.
"""
f_doxyfile = tempfile.NamedTemporaryFile("w", delete=False)
f_doxyfile.write(doxyfile)
f_doxyfile.close()
p = Popen([doxygen, f_doxyfile.name], stdout=PIPE, stderr=STDOUT, encoding="utf-8")
while True:
line = p.stdout.readline() # type: ignore
if line:
process_doxygen_output(line.rstrip(), silent)
if p.poll() is not None:
break
Path(f_doxyfile.name).unlink()
if p.returncode:
raise IOError(f"Doxygen process returned non-zero ({p.returncode})")
def sync_doxygen(doxyfile: str, new: Path, prev: Path) -> None:
"""Synchronize Doxygen output with a previous build.
This function makes sure that only new, deleted or changed files are
actually modified in the Doxygen XML output. Latest HTML content is just
moved.
Args:
doxyfile: Contents of the Doxyfile.
new: Newest Doxygen build output directory.
prev: Previous Doxygen build output directory.
"""
generate_html = get_doxygen_option(doxyfile, "GENERATE_HTML")
if generate_html[0] == "YES":
html_output = get_doxygen_option(doxyfile, "HTML_OUTPUT")
if not html_output:
raise ValueError("No HTML_OUTPUT set in Doxyfile")
new_htmldir = new / html_output[0]
prev_htmldir = prev / html_output[0]
if prev_htmldir.exists():
shutil.rmtree(prev_htmldir)
new_htmldir.rename(prev_htmldir)
xml_output = get_doxygen_option(doxyfile, "XML_OUTPUT")
if not xml_output:
raise ValueError("No XML_OUTPUT set in Doxyfile")
new_xmldir = new / xml_output[0]
prev_xmldir = prev / xml_output[0]
if prev_xmldir.exists():
dcmp = filecmp.dircmp(new_xmldir, prev_xmldir)
for file in dcmp.right_only:
(Path(dcmp.right) / file).unlink()
for file in dcmp.left_only + dcmp.diff_files:
shutil.copy(Path(dcmp.left) / file, Path(dcmp.right) / file)
shutil.rmtree(new_xmldir)
else:
new_xmldir.rename(prev_xmldir)
def doxygen_build(app: Sphinx) -> None:
"""Doxyrunner entry point.
Args:
app: Sphinx application instance.
"""
if app.config.doxyrunner_outdir:
outdir = Path(app.config.doxyrunner_outdir)
else:
outdir = Path(app.outdir) / "_doxygen"
outdir.mkdir(exist_ok=True)
tmp_outdir = outdir / "tmp"
logger.info("Preparing Doxyfile...")
doxyfile = process_doxyfile(
app.config.doxyrunner_doxyfile,
tmp_outdir,
app.config.doxyrunner_silent,
app.config.doxyrunner_fmt,
app.config.doxyrunner_fmt_pattern,
app.config.doxyrunner_fmt_vars,
app.config.doxyrunner_outdir_var,
)
logger.info("Checking if Doxygen needs to be run...")
changed = doxygen_input_has_changed(app.env, doxyfile)
if not changed:
logger.info("Doxygen build will be skipped (no changes)!")
return
logger.info("Running Doxygen...")
run_doxygen(
app.config.doxyrunner_doxygen,
doxyfile,
app.config.doxyrunner_silent,
)
logger.info("Syncing Doxygen output...")
sync_doxygen(doxyfile, tmp_outdir, outdir)
shutil.rmtree(tmp_outdir)
def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value("doxyrunner_doxygen", "doxygen", "env")
app.add_config_value("doxyrunner_doxyfile", None, "env")
app.add_config_value("doxyrunner_outdir", None, "env")
app.add_config_value("doxyrunner_outdir_var", None, "env")
app.add_config_value("doxyrunner_fmt", False, "env")
app.add_config_value("doxyrunner_fmt_vars", {}, "env")
app.add_config_value("doxyrunner_fmt_pattern", "@{}@", "env")
app.add_config_value("doxyrunner_silent", True, "")
app.connect("builder-inited", doxygen_build)
return {
"version": __version__,
"parallel_read_safe": True,
"parallel_write_safe": True,
}

View File

@@ -0,0 +1,194 @@
"""
External content
################
Copyright (c) 2021 Nordic Semiconductor ASA
SPDX-License-Identifier: Apache-2.0
Introduction
============
This extension allows to import sources from directories out of the Sphinx
source directory. They are copied to the source directory before starting the
build. Note that the copy is *smart*, that is, only updated files are actually
copied. Therefore, incremental builds detect changes correctly and behave as
expected.
Paths for external content included via e.g. figure, literalinclude, etc.
are adjusted as needed.
Configuration options
=====================
- ``external_content_contents``: A list of external contents. Each entry is
a tuple with two fields: the external base directory and a file glob pattern.
- ``external_content_directives``: A list of directives that should be analyzed
and their paths adjusted if necessary. Defaults to ``DEFAULT_DIRECTIVES``.
- ``external_content_keep``: A list of file globs (relative to the destination
directory) that should be kept even if they do not exist in the source
directory. This option can be useful for auto-generated files in the
destination directory.
"""
import filecmp
import os
from pathlib import Path
import re
import shutil
import tempfile
from typing import Dict, Any, List, Optional
from sphinx.application import Sphinx
__version__ = "0.1.0"
DEFAULT_DIRECTIVES = ("figure", "image", "include", "literalinclude")
"""Default directives for included content."""
def adjust_includes(
fname: Path,
basepath: Path,
directives: List[str],
encoding: str,
dstpath: Optional[Path] = None,
) -> None:
"""Adjust included content paths.
Args:
fname: File to be processed.
basepath: Base path to be used to resolve content location.
directives: Directives to be parsed and adjusted.
encoding: Sources encoding.
dstpath: Destination path for fname if its path is not the actual destination.
"""
if fname.suffix != ".rst":
return
dstpath = dstpath or fname.parent
def _adjust(m):
directive, fpath = m.groups()
# ignore absolute paths
if fpath.startswith("/"):
fpath_adj = fpath
else:
fpath_adj = Path(os.path.relpath(basepath / fpath, dstpath)).as_posix()
return f".. {directive}:: {fpath_adj}"
with open(fname, "r+", encoding=encoding) as f:
content = f.read()
content_adj, modified = re.subn(
r"\.\. (" + "|".join(directives) + r")::\s*([^`\n]+)", _adjust, content
)
if modified:
f.seek(0)
f.write(content_adj)
f.truncate()
def sync_contents(app: Sphinx) -> None:
"""Synchronize external contents.
Args:
app: Sphinx application instance.
"""
srcdir = Path(app.srcdir).resolve()
to_copy = []
to_delete = set(f for f in srcdir.glob("**/*") if not f.is_dir())
to_keep = set(
f
for k in app.config.external_content_keep
for f in srcdir.glob(k)
if not f.is_dir()
)
# Get exclude patterns from config
exclude_patterns = getattr(app.config, 'external_content_exclude', [])
for content in app.config.external_content_contents:
prefix_src, glob = content
# Build exclude set using glob patterns for this content base path
exclude_set = set()
for pattern in exclude_patterns:
# Use glob to find all files matching the exclude pattern from this base path
for excluded_file in prefix_src.glob(pattern):
if not excluded_file.is_dir():
exclude_set.add(excluded_file.resolve())
for src in prefix_src.glob(glob):
# Check if src is in the exclude set
if src.resolve() in exclude_set:
continue
if src.is_dir():
to_copy.extend(
[(f, prefix_src) for f in src.glob("**/*")
if not f.is_dir() and f.resolve() not in exclude_set]
)
else:
to_copy.append((src, prefix_src))
for entry in to_copy:
src, prefix_src = entry
dst = (srcdir / src.relative_to(prefix_src)).resolve()
if dst in to_delete:
to_delete.remove(dst)
if not dst.parent.exists():
dst.parent.mkdir(parents=True)
# just copy if it does not exist
if not dst.exists():
shutil.copy(src, dst)
adjust_includes(
dst,
src.parent,
app.config.external_content_directives,
app.config.source_encoding,
)
# if origin file is modified only copy if different
elif src.stat().st_mtime > dst.stat().st_mtime:
with tempfile.TemporaryDirectory() as td:
# adjust origin includes before comparing
src_adjusted = Path(td) / src.name
shutil.copy(src, src_adjusted)
adjust_includes(
src_adjusted,
src.parent,
app.config.external_content_directives,
app.config.source_encoding,
dstpath=dst.parent,
)
if not filecmp.cmp(src_adjusted, dst):
dst.unlink()
shutil.move(os.fspath(src_adjusted), os.fspath(dst))
# remove any previously copied file not present in the origin folder,
# excepting those marked to be kept.
for file in to_delete - to_keep:
file.unlink()
def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value("external_content_contents", [], "env")
app.add_config_value("external_content_directives", DEFAULT_DIRECTIVES, "env")
app.add_config_value("external_content_keep", [], "")
app.add_config_value("external_content_exclude", [], "env")
app.connect("builder-inited", sync_contents)
return {
"version": __version__,
"parallel_read_safe": True,
"parallel_write_safe": True,
}

View File

@@ -0,0 +1,259 @@
"""
Source Link Extension
######################
This Sphinx extension automatically adds source code repository links to documentation pages.
It's especially useful for sample documentation, allowing readers to easily locate the source
code in the repository.
Features
========
- Automatically injects a "View Source Code" link at the top of specified documents
- Supports GitHub, GitLab, and local path displays
- Configurable patterns to match which documents should get source links
- Minimal configuration required
Configuration options
=====================
- ``source_link_base_url``: Base URL for the source repository (e.g.,
'https://github.com/your-org/repo/tree/master'). If not set, only shows local paths.
- ``source_link_patterns``: List of glob patterns for files that should have source links
(defaults to ['samples/**/*.md', 'samples/**/*.rst'])
- ``source_link_show_local_path``: Whether to show the local file path (default: True)
- ``source_link_label``: Label text for the link (default: '📁 源码位置')
"""
import os
from pathlib import Path
from typing import Dict, Any, List
from docutils import nodes
from docutils.parsers.rst import Directive
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective
__version__ = "0.1.0"
def get_source_path(app: Sphinx, docname: str) -> str:
"""
Get the source file path relative to SDK_BASE.
Args:
app: Sphinx application instance
docname: Document name (without extension)
Returns:
Relative path from SDK_BASE to the source file, or empty string if not applicable
"""
srcdir = Path(app.srcdir).resolve()
sdk_base = Path(os.environ.get("SDK_BASE", srcdir.parent)).resolve()
# Try to find the actual source file (.md or .rst)
for ext in ['.md', '.rst']:
source_file = srcdir / f"{docname}{ext}"
if source_file.exists():
try:
rel_path = source_file.relative_to(srcdir)
# Get directory path (remove filename if it's README.md or similar)
if source_file.name.lower() in ['readme.md', 'readme.rst', 'index.md', 'index.rst']:
return str(rel_path.parent)
else:
return str(rel_path.parent)
except ValueError:
pass
return ""
def should_add_source_link(app: Sphinx, docname: str) -> bool:
"""
Check if a document should have a source link based on configured patterns.
Args:
app: Sphinx application instance
docname: Document name
Returns:
True if the document matches any of the configured patterns
"""
patterns = app.config.source_link_patterns
for pattern in patterns:
# Convert glob pattern to simple matching (could be enhanced with fnmatch)
if pattern.endswith('**/*.md') or pattern.endswith('**/*.rst'):
prefix = pattern.rsplit('/', 1)[0].replace('**', '')
if docname.startswith(prefix):
return True
elif pattern in docname:
return True
return False
def get_version_branch(app: Sphinx) -> str:
"""
Get the Git branch name based on the current documentation version.
Args:
app: Sphinx application instance
Returns:
Branch name to use in repository URLs
"""
# Get current version from html_context
current_version = app.config.html_context.get('current_version', 'latest')
# Get version-to-branch mapping from config
version_branch_map = getattr(app.config, 'source_link_version_branches', {})
# Return mapped branch, or construct default branch name
if current_version in version_branch_map:
return version_branch_map[current_version]
elif current_version == 'latest':
return 'master'
elif current_version.startswith('v'):
# Convert v0.1.0 to release/v0.1.0
return f"release/{current_version}"
else:
return 'master'
def create_source_link_node(app: Sphinx, source_path: str) -> nodes.container:
"""
Create a docutils node containing the source link information.
Args:
app: Sphinx application instance
source_path: Relative path to the source directory
Returns:
A docutils container node with the source link
"""
container = nodes.container()
container['classes'].append('source-link-box')
paragraph = nodes.paragraph()
# Add emoji/icon
icon = nodes.inline(text=app.config.source_link_label.split()[0] + ' ')
icon['classes'].append('source-link-icon')
paragraph += icon
# Add label
label_text = ' '.join(app.config.source_link_label.split()[1:]) + ': '
label = nodes.strong(text=label_text)
paragraph += label
# Add local path
if app.config.source_link_show_local_path:
path_text = nodes.literal(text=source_path)
path_text['classes'].append('source-link-path')
paragraph += path_text
# Add repository link if configured
if app.config.source_link_base_url:
if app.config.source_link_show_local_path:
paragraph += nodes.Text(' ')
# Get version-specific branch
branch = get_version_branch(app)
# Create clickable link with version-specific branch
base_url = app.config.source_link_base_url.rstrip('/')
# Replace the branch/tag in the URL
# Handle both GitHub style (/tree/BRANCH) and GitLab style (/-/tree/BRANCH)
if '/-/tree/' in base_url:
# GitLab style
base_parts = base_url.rsplit('/-/tree/', 1)
ref_url = f"{base_parts[0]}/-/tree/{branch}/{source_path}"
elif '/tree/' in base_url:
# GitHub style
base_parts = base_url.rsplit('/tree/', 1)
ref_url = f"{base_parts[0]}/tree/{branch}/{source_path}"
else:
# Fallback: append branch and path
ref_url = f"{base_url}/{branch}/{source_path}"
reference = nodes.reference('', '查看源码', refuri=ref_url)
reference['classes'].append('source-link-button')
paragraph += reference
container += paragraph
return container
def inject_source_link(app: Sphinx, doctree, docname: str) -> None:
"""
Inject source link at the beginning of the document.
Args:
app: Sphinx application instance
doctree: Document tree
docname: Document name
"""
if not should_add_source_link(app, docname):
return
source_path = get_source_path(app, docname)
if not source_path:
return
# Create the source link node
source_link_node = create_source_link_node(app, source_path)
# Insert at the beginning of the document
# Find the first section or insert at the very beginning
if len(doctree.children) > 0:
# Insert after the title but before the first section content
for i, child in enumerate(doctree.children):
if isinstance(child, nodes.section) and len(child.children) > 0:
# Insert after the section title
child.insert(1, source_link_node)
break
else:
# No section found, insert at the beginning
doctree.insert(0, source_link_node)
def add_source_link_css(app: Sphinx, config) -> None:
"""
Add CSS for source link styling.
Args:
app: Sphinx application instance
config: Sphinx config
"""
# CSS will be added via static files
app.add_css_file('source-link.css')
def setup(app: Sphinx) -> Dict[str, Any]:
"""
Setup function for the Sphinx extension.
Args:
app: Sphinx application instance
Returns:
Extension metadata
"""
# Add configuration values
app.add_config_value("source_link_base_url", "", "html")
app.add_config_value("source_link_patterns", ["samples/**/*.md", "samples/**/*.rst"], "html")
app.add_config_value("source_link_show_local_path", True, "html")
app.add_config_value("source_link_label", "📁 源码位置", "html")
app.add_config_value("source_link_version_branches", {}, "html")
# Connect event handlers
app.connect("doctree-resolved", inject_source_link)
app.connect("config-inited", add_source_link_css)
return {
"version": __version__,
"parallel_read_safe": True,
"parallel_write_safe": True,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,272 @@
/* 反馈按钮悬浮样式 */
.feedback-button {
position: fixed;
right: 30px;
bottom: 100px;
z-index: 1000;
background-color: #2980b9;
color: white;
border: none;
border-radius: 50px;
padding: 12px 24px;
font-size: 14px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif;
}
.feedback-button:hover {
background-color: #3498db;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
}
/* 反馈弹窗遮罩 */
.feedback-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2000;
animation: fadeIn 0.3s ease;
}
.feedback-modal-overlay.active {
display: flex;
justify-content: center;
align-items: center;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 反馈弹窗主体 */
.feedback-modal {
background-color: white;
border-radius: 12px;
padding: 30px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
position: relative;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* 关闭按钮 */
.feedback-modal-close {
position: absolute;
top: 15px;
right: 15px;
background: none;
border: none;
font-size: 24px;
color: #999;
cursor: pointer;
padding: 5px 10px;
line-height: 1;
transition: color 0.2s;
}
.feedback-modal-close:hover {
color: #333;
}
/* 弹窗标题 */
.feedback-modal h2 {
margin: 0 0 20px 0;
font-size: 20px;
color: #333;
font-weight: 600;
}
/* 表单样式 */
.feedback-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.feedback-form-group {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 20px;
}
.feedback-form-group label {
font-size: 14px;
font-weight: 500;
color: #555;
}
.feedback-form-group textarea,
.feedback-form-group input[type="text"],
.feedback-form-group input[type="email"] {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif;
transition: border-color 0.2s;
box-sizing: border-box;
}
.feedback-form-group textarea {
min-height: 120px;
resize: vertical;
}
/* contenteditable 编辑器样式 */
.feedback-editor {
min-height: 150px;
max-height: 400px;
overflow-y: auto;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
background-color: white;
transition: border-color 0.2s;
line-height: 1.6;
word-wrap: break-word;
}
.feedback-editor:focus {
outline: none;
border-color: #2980b9;
}
.feedback-editor:empty:before {
content: attr(data-placeholder);
color: #999;
white-space: pre-wrap;
}
.feedback-editor img {
max-width: 100%;
height: auto;
display: block;
margin: 10px 0;
border-radius: 4px;
border: 1px solid #e1e8ed;
cursor: pointer;
}
.feedback-editor img:hover {
border-color: #2980b9;
box-shadow: 0 2px 8px rgba(41, 128, 185, 0.2);
}
.feedback-form-group input:focus,
.feedback-form-group textarea:focus {
outline: none;
border-color: #2980b9;
}
/* 联系信息部分 */
.feedback-contact-info {
margin-top: 10px;
}
.feedback-contact-info p {
margin: 0 0 15px 0;
font-size: 14px;
color: #333;
border-bottom: 1px solid #eee;
}
.feedback-contact-fields {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
/* 提交按钮 */
.feedback-submit-btn {
align-self: flex-end;
background-color: #2980b9;
color: white;
border: none;
border-radius: 6px;
padding: 12px 40px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 10px;
}
.feedback-submit-btn:hover {
background-color: #3498db;
}
.feedback-submit-btn:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
/* 提示信息 */
.feedback-message {
padding: 12px;
border-radius: 6px;
font-size: 14px;
margin-bottom: 15px;
display: none;
}
.feedback-message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
display: block;
}
.feedback-message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
display: block;
}
/* 响应式设计 */
@media (max-width: 768px) {
.feedback-button {
right: 20px;
bottom: 80px;
padding: 10px 20px;
font-size: 13px;
}
.feedback-modal {
padding: 20px;
width: 95%;
}
.feedback-contact-fields {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,511 @@
/**
* ARCS SDK 文档反馈组件
* 用于收集用户反馈并提交到 SeaTable
*/
(function() {
'use strict';
// SeaTable 表单提交配置(可通过 window.SEATABLE_FEEDBACK_CONFIG 覆盖)
const DEFAULT_CONFIG = {
formId: '6bb54f8a-2740-4bf5-a63e-836c37ec9406',
// 使用相对路径,通过 Nginx 代理到 SeaTable
formSubmitUrl: '/arcs-docs/api/v2.1/form-submit/6bb54f8a-2740-4bf5-a63e-836c37ec9406/',
uploadLinkUrl: '/arcs-docs/api/v2.1/forms/6bb54f8a-2740-4bf5-a63e-836c37ec9406/upload-link/?upload_type=image',
tableId: '0000',
// 保留完整URL作为备用
directFormSubmitUrl: 'https://inner-table.listenai.com/api/v2.1/form-submit/6bb54f8a-2740-4bf5-a63e-836c37ec9406/',
directUploadLinkUrl: 'https://inner-table.listenai.com/api/v2.1/forms/6bb54f8a-2740-4bf5-a63e-836c37ec9406/upload-link/?upload_type=image'
};
// 合并外部配置
const SEATABLE_CONFIG = Object.assign({}, DEFAULT_CONFIG, window.SEATABLE_FEEDBACK_CONFIG || {});
// 获取当前文档信息
function getDocumentInfo() {
// 获取文档版本
const versionLabel = document.querySelector('.current-version-label');
const version = versionLabel ? versionLabel.textContent.trim() : 'latest';
// 获取文档路径/面包屑导航
let pagePath = '';
const breadcrumbs = document.querySelectorAll('.wy-breadcrumbs li');
if (breadcrumbs.length > 0) {
const pathParts = [];
breadcrumbs.forEach((crumb, index) => {
// 跳过最后的 "Edit on GitHub" 等链接
if (index < breadcrumbs.length - 1) {
const text = crumb.textContent.trim();
if (text && text !== '»') {
pathParts.push(text);
}
}
});
pagePath = pathParts.join(' / ');
}
// 如果没有面包屑,尝试从标题获取
if (!pagePath) {
const title = document.querySelector('h1');
pagePath = title ? title.textContent.trim() : document.title;
}
// 获取完整 URL
const pageUrl = window.location.href;
return {
version: version,
pagePath: pagePath,
pageUrl: pageUrl
};
}
// 上传图片到 SeaTable
async function uploadImagesToSeaTable(images) {
const uploadedUrls = [];
for (const imageData of images) {
try {
console.log('开始上传图片:', imageData.file.name);
// 尝试上传单张图片
const imageUrl = await uploadSingleImage(imageData.file);
if (imageUrl) {
uploadedUrls.push(imageUrl);
console.log('✓ 图片上传成功:', imageUrl);
} else {
// 上传失败,记录图片信息作为占位符
const fileInfo = `图片: ${imageData.file.name} (${(imageData.file.size / 1024).toFixed(1)}KB)`;
uploadedUrls.push(fileInfo);
console.log('✗ 图片上传失败,记录文件信息:', fileInfo);
}
} catch (error) {
console.error('图片上传异常:', error);
uploadedUrls.push(`图片: ${imageData.file.name}`);
}
}
return uploadedUrls;
}
// 上传单张图片
async function uploadSingleImage(file) {
try {
console.log('→ 步骤1: 获取上传凭证');
// 步骤1: GET 请求获取上传凭证(简化请求避免 CORS preflight
const credentialResponse = await fetch(SEATABLE_CONFIG.uploadLinkUrl, {
method: 'GET',
credentials: 'include', // 包含 cookies
mode: 'cors' // 明确指定 CORS 模式
});
if (!credentialResponse.ok) {
console.error('获取上传凭证失败:', credentialResponse.status);
return null;
}
const credential = await credentialResponse.json();
console.log('获取到凭证:', credential);
if (!credential.upload_link || !credential.parent_path) {
console.error('凭证数据不完整');
return null;
}
// 步骤2: 上传图片到 upload_link
console.log('→ 步骤2: 上传图片文件');
const uploadFormData = new FormData();
uploadFormData.append('parent_dir', credential.parent_path);
uploadFormData.append('file', file);
// 如果 upload_link 是完整URL需要转换为代理路径
let uploadUrl = credential.upload_link;
if (uploadUrl.startsWith('https://inner-table.listenai.com')) {
uploadUrl = uploadUrl.replace('https://inner-table.listenai.com', '/arcs-docs');
}
const uploadResponse = await fetch(uploadUrl + '?ret-json=1', {
method: 'POST',
body: uploadFormData,
credentials: 'include',
mode: 'cors'
});
if (!uploadResponse.ok) {
console.error('上传图片失败:', uploadResponse.status);
return null;
}
const uploadResult = await uploadResponse.json();
console.log('上传结果:', uploadResult);
// uploadResult 是一个数组
if (!Array.isArray(uploadResult) || uploadResult.length === 0) {
console.error('上传结果格式错误');
return null;
}
const uploadedFile = uploadResult[0];
const fileName = uploadedFile.name;
// 步骤3: 构建最终图片URL
// 注意提交给SeaTable的URL必须是完整URL这样SeaTable后台才能正确显示图片
const imageUrl = `https://inner-table.listenai.com/workspace/24${credential.parent_path}/${fileName}`;
console.log('✓ 图片URL:', imageUrl);
return imageUrl;
} catch (error) {
console.error('上传图片过程出错:', error);
// 如果是跨域错误,提示用户
if (error.name === 'TypeError' && error.message.includes('fetch')) {
console.warn('⚠️ 图片上传遇到跨域限制');
console.warn('💡 提示:文档需要部署在 inner-table.listenai.com 同域下才能上传图片');
}
return null;
}
}
// 提交反馈到 SeaTable
async function submitFeedback(feedbackData) {
const docInfo = getDocumentInfo();
// 准备个人信息字段(合并姓名和邮箱)
let personalInfo = '';
if (feedbackData.name && feedbackData.email) {
personalInfo = `${feedbackData.name}${feedbackData.email}`;
} else if (feedbackData.name) {
personalInfo = feedbackData.name;
} else if (feedbackData.email) {
personalInfo = feedbackData.email;
}
// 上传图片并获取URL
let imageUrls = [];
if (feedbackData.images && feedbackData.images.length > 0) {
console.log('开始上传', feedbackData.images.length, '张图片...');
imageUrls = await uploadImagesToSeaTable(feedbackData.images);
}
// 准备提交的数据,按照 SeaTable 表单要求的格式
const now = new Date();
const submitDateTime = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0') + ' ' +
String(now.getHours()).padStart(2, '0') + ':' +
String(now.getMinutes()).padStart(2, '0') + ':' +
String(now.getSeconds()).padStart(2, '0');
const rowData = {
'文档目录': docInfo.pagePath || '未知页面',
'提交日期': submitDateTime, // YYYY-MM-DD HH:MM:SS 格式
'文档版本': docInfo.version || 'latest',
'问题描述': feedbackData.feedback || (imageUrls.length > 0 ? '[见贴图]' : ''),
'个人信息': personalInfo,
'页面URL': docInfo.pageUrl
};
// 如果有图片URL添加到 row_data
if (imageUrls.length > 0) {
rowData['问题贴图'] = imageUrls;
}
// 使用表单提交方式
return submitViaForm(rowData, feedbackData.images);
}
// 通过 SeaTable 表单 API 提交
async function submitViaForm(rowData, images) {
return new Promise((resolve) => {
try {
console.log('提交反馈数据:', {
table_id: SEATABLE_CONFIG.tableId,
row_data: rowData
});
// 创建隐藏的 form 元素
const form = document.createElement('form');
form.method = 'POST';
form.action = SEATABLE_CONFIG.formSubmitUrl;
form.target = 'feedback-submit-frame';
form.style.display = 'none';
form.enctype = 'multipart/form-data';
// 添加 table_id 字段
const tableIdInput = document.createElement('input');
tableIdInput.type = 'hidden';
tableIdInput.name = 'table_id';
tableIdInput.value = SEATABLE_CONFIG.tableId;
form.appendChild(tableIdInput);
// 添加 row_data 字段
const rowDataInput = document.createElement('input');
rowDataInput.type = 'hidden';
rowDataInput.name = 'row_data';
rowDataInput.value = JSON.stringify(rowData);
form.appendChild(rowDataInput);
// 创建或获取 iframe
let iframe = document.getElementById('feedback-submit-frame');
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'feedback-submit-frame';
iframe.name = 'feedback-submit-frame';
iframe.style.display = 'none';
document.body.appendChild(iframe);
}
// 提交表单
document.body.appendChild(form);
form.submit();
setTimeout(() => {
document.body.removeChild(form);
console.log('反馈已提交');
resolve({ success: true });
}, 800);
} catch (error) {
console.error('提交失败:', error);
resolve({ success: true });
}
});
}
// 初始化反馈组件
function initFeedbackWidget() {
// 创建反馈按钮 HTML
const feedbackHTML = `
<button class="feedback-button" id="feedbackButton" title="提交反馈">
提交反馈
</button>
<div class="feedback-modal-overlay" id="feedbackModal">
<div class="feedback-modal">
<button class="feedback-modal-close" id="feedbackClose">×</button>
<div class="feedback-message" id="feedbackMessage"></div>
<form class="feedback-form" id="feedbackForm">
<div class="feedback-form-group">
<label for="feedbackContent">问题描述(支持粘贴图片):</label>
<div
id="feedbackContent"
class="feedback-editor"
contenteditable="true"
data-placeholder="请描述您遇到的问题或建议"
></div>
<input type="hidden" name="feedback" id="feedbackText">
</div>
<div class="feedback-contact-info">
<p>如果希望我们联系您,请留下信息</p>
<div class="feedback-contact-fields">
<div class="feedback-form-group">
<label for="feedbackName">姓名</label>
<input
type="text"
id="feedbackName"
name="name"
placeholder="您的姓名"
/>
</div>
<div class="feedback-form-group">
<label for="feedbackEmail">邮箱</label>
<input
type="email"
id="feedbackEmail"
name="email"
placeholder="您的邮箱"
/>
</div>
</div>
</div>
<button type="submit" class="feedback-submit-btn">提交</button>
</form>
</div>
</div>
`;
// 将 HTML 插入到页面
document.body.insertAdjacentHTML('beforeend', feedbackHTML);
// 获取元素
const feedbackButton = document.getElementById('feedbackButton');
const feedbackModal = document.getElementById('feedbackModal');
const feedbackClose = document.getElementById('feedbackClose');
const feedbackForm = document.getElementById('feedbackForm');
const feedbackMessage = document.getElementById('feedbackMessage');
const feedbackEditor = document.getElementById('feedbackContent');
const feedbackTextInput = document.getElementById('feedbackText');
// 存储图片数据
let uploadedImages = [];
// 显示消息
function showMessage(message, type) {
feedbackMessage.textContent = message;
feedbackMessage.className = `feedback-message ${type}`;
setTimeout(() => {
feedbackMessage.className = 'feedback-message';
}, 5000);
}
// 打开弹窗
feedbackButton.addEventListener('click', function() {
feedbackModal.classList.add('active');
document.body.style.overflow = 'hidden';
});
// 关闭弹窗
function closeModal() {
feedbackModal.classList.remove('active');
document.body.style.overflow = '';
feedbackForm.reset();
feedbackEditor.innerHTML = '';
uploadedImages = [];
feedbackMessage.className = 'feedback-message';
}
feedbackClose.addEventListener('click', closeModal);
// 点击遮罩关闭
feedbackModal.addEventListener('click', function(e) {
if (e.target === feedbackModal) {
closeModal();
}
});
// ESC 键关闭
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && feedbackModal.classList.contains('active')) {
closeModal();
}
});
// 处理粘贴事件
feedbackEditor.addEventListener('paste', async function(e) {
e.preventDefault();
const items = e.clipboardData.items;
let hasImage = false;
let textContent = '';
for (let item of items) {
if (item.type.indexOf('image') !== -1) {
hasImage = true;
const file = item.getAsFile();
await handleImagePaste(file);
} else if (item.type === 'text/plain') {
textContent = e.clipboardData.getData('text/plain');
}
}
// 如果只有文字,插入文字
if (!hasImage && textContent) {
document.execCommand('insertText', false, textContent);
}
});
// 处理图片粘贴
async function handleImagePaste(file) {
// 创建图片预览
const reader = new FileReader();
reader.onload = function(e) {
const img = document.createElement('img');
img.src = e.target.result;
img.style.maxWidth = '100%';
img.style.margin = '10px 0';
img.style.borderRadius = '4px';
img.classList.add('pasted-image');
// 插入图片到编辑器
feedbackEditor.appendChild(img);
// 保存图片数据
uploadedImages.push({
file: file,
dataUrl: e.target.result,
element: img
});
console.log('图片已粘贴,共', uploadedImages.length, '张图片');
};
reader.readAsDataURL(file);
}
// 提取编辑器内容
function extractEditorContent() {
const text = feedbackEditor.innerText.trim();
return {
text: text,
images: uploadedImages
};
}
// 表单提交
feedbackForm.addEventListener('submit', async function(e) {
e.preventDefault();
const content = extractEditorContent();
const formData = new FormData(feedbackForm);
const feedbackData = {
feedback: content.text,
images: content.images,
name: formData.get('name'),
email: formData.get('email')
};
// 验证反馈内容:至少要有文字或图片
const hasText = feedbackData.feedback && feedbackData.feedback.trim() !== '';
const hasImages = feedbackData.images && feedbackData.images.length > 0;
if (!hasText && !hasImages) {
showMessage('请输入反馈内容或粘贴图片', 'error');
return;
}
// 禁用提交按钮
const submitBtn = feedbackForm.querySelector('.feedback-submit-btn');
submitBtn.disabled = true;
submitBtn.textContent = '提交中...';
try {
const result = await submitFeedback(feedbackData);
if (result.success) {
showMessage('感谢您的反馈!我们已收到您的意见。', 'success');
feedbackForm.reset();
// 3秒后关闭弹窗
setTimeout(() => {
closeModal();
}, 3000);
} else {
showMessage('提交失败,请稍后重试。', 'error');
}
} catch (error) {
console.error('提交反馈失败:', error);
showMessage('提交失败,请稍后重试。', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = '提交';
}
});
console.log('反馈组件已初始化');
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initFeedbackWidget);
} else {
initFeedbackWidget();
}
})();

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,84 @@
/**
* Source Link Styling
*
* Simple and clean styling for source code location links
*/
.source-link-box {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-left: 3px solid #2196f3;
border-radius: 4px;
padding: 10px 16px;
margin: 16px 0 24px 0;
}
.source-link-box p {
margin: 0 !important;
color: #495057;
font-size: 14px;
line-height: 1.5;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.source-link-icon {
font-size: 16px;
margin-right: 2px;
}
.source-link-box strong {
color: #495057;
font-weight: 500;
}
.source-link-path {
background-color: #e9ecef;
color: #495057 !important;
padding: 2px 8px;
border-radius: 3px;
font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace;
font-size: 13px;
font-weight: normal;
}
.source-link-button {
background-color: #2196f3;
color: #ffffff !important;
padding: 3px 12px;
border-radius: 3px;
text-decoration: none !important;
font-size: 13px;
transition: background-color 0.2s ease;
display: inline-block;
margin-left: 4px;
}
.source-link-button:hover {
background-color: #1976d2;
color: #ffffff !important;
text-decoration: none !important;
}
/* Mobile responsive */
@media screen and (max-width: 768px) {
.source-link-box {
padding: 10px 12px;
}
.source-link-box p {
font-size: 13px;
}
.source-link-path {
font-size: 12px;
word-break: break-all;
}
.source-link-button {
margin-top: 4px;
font-size: 12px;
}
}

View File

@@ -0,0 +1,124 @@
/* Version switcher in sidebar (dropdown style) */
.version-switcher-sidebar {
margin: 1em 0;
padding: 0;
}
.version-select {
width: 100%;
padding: 8px 12px;
font-size: 90%;
font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif;
background-color: #2980B9;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.version-select:hover {
background-color: #3091d1;
}
.version-select:focus {
outline: 2px solid #fcfcfc;
outline-offset: 2px;
}
.version-select option {
background-color: white;
color: #333;
padding: 8px;
}
/* Old bottom version switcher (kept for backward compatibility) */
.rst-versions {
position: fixed;
bottom: 0;
left: 0;
width: 300px;
color: #fcfcfc;
background: #1f1d1d;
font-family: "Lato", "proxima-nova", "Helvetica Neue", Arial, sans-serif;
z-index: 400;
border-top: solid 10px #343131;
}
.rst-versions a {
color: #2980B9;
text-decoration: none;
}
.rst-versions a:hover {
color: #3091d1;
text-decoration: underline;
}
.rst-versions .rst-current-version {
padding: 12px;
background-color: #272525;
display: block;
text-align: right;
font-size: 90%;
cursor: pointer;
color: #27ae60;
transition: background-color 0.2s ease;
}
.rst-versions .rst-current-version:hover {
background-color: #2e2c2c;
}
.rst-versions .rst-current-version .fa {
color: #fcfcfc;
}
.rst-versions .rst-current-version .fa-book,
.rst-versions .rst-current-version .fa-caret-down {
color: #fcfcfc;
}
.rst-versions .rst-other-versions {
display: none;
padding: 12px;
color: #fcfcfc;
font-size: 90%;
}
.rst-versions.shift-up .rst-other-versions {
display: block;
}
.rst-versions .rst-other-versions dl {
margin-bottom: 0;
}
.rst-versions .rst-other-versions dt {
display: block;
font-weight: bold;
margin-bottom: 6px;
color: #fcfcfc;
}
.rst-versions .rst-other-versions dd {
display: inline-block;
margin: 0 0 6px 0;
}
.rst-versions .rst-other-versions dd a {
display: inline-block;
padding: 6px;
color: #fcfcfc;
}
.rst-versions .rst-other-versions dd a:hover {
background-color: #1f1d1d;
color: #27ae60;
}
.rst-versions .rst-other-versions dd strong {
display: inline-block;
padding: 6px;
color: #27ae60;
}

View File

@@ -0,0 +1,123 @@
// Version switcher toggle functionality
document.addEventListener('DOMContentLoaded', function() {
var versionSelector = document.querySelector('.rst-versions');
if (versionSelector) {
var currentVersion = versionSelector.querySelector('.rst-current-version');
if (currentVersion) {
currentVersion.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
versionSelector.classList.toggle('shift-up');
});
}
// Close when clicking outside
document.addEventListener('click', function(e) {
if (!versionSelector.contains(e.target)) {
versionSelector.classList.remove('shift-up');
}
});
}
// Update displayed version when a version link is clicked
document.addEventListener('click', function(e) {
if (e.target.tagName === 'A' && e.target.closest('.rst-other-versions')) {
const versionName = e.target.textContent;
const versionLabel = document.querySelector('.current-version-label');
if (versionLabel) {
versionLabel.textContent = versionName;
}
}
});
// Detect current version from URL and update label on page load
function updateVersionFromURL() {
const versionLabel = document.querySelector('.current-version-label');
if (!versionLabel) return;
const pathParts = window.location.pathname.split('/');
let version = 'latest'; // default version
// Look for version pattern in URL path
for (const part of pathParts) {
if (part.startsWith('v') || part === 'latest') {
version = part;
break;
}
}
versionLabel.textContent = version;
// 更新版本列表中各项的显示方式strong 或 a 标签)
updateVersionList(version);
}
// 根据当前版本更新版本列表的显示
function updateVersionList(currentVersion) {
const versionItems = document.querySelectorAll('.rst-other-versions dd');
versionItems.forEach(item => {
const link = item.querySelector('a');
const strong = item.querySelector('strong');
// 获取版本名称
let versionName = '';
if (link) {
versionName = link.textContent;
} else if (strong) {
versionName = strong.textContent;
}
// 移除现有元素
if (link) {
link.remove();
}
if (strong) {
strong.remove();
}
// 根据是否是当前版本决定显示方式
if (versionName === currentVersion) {
const strongEl = document.createElement('strong');
strongEl.textContent = versionName;
item.appendChild(strongEl);
} else {
// 找到对应版本的链接地址
const allLinks = Array.from(document.querySelectorAll('.rst-other-versions dd a, .rst-other-versions dd strong'));
let targetHref = '';
// 尝试从现有链接中找到目标URL
for (const el of allLinks) {
if (el.textContent === versionName) {
// 找到包含相同版本名的父级dd元素中的链接
const parentDD = el.closest('dd');
const parentLink = parentDD.querySelector('a');
if (parentLink) {
targetHref = parentLink.href;
break;
}
}
}
// 如果没找到链接,则从当前点击的链接或其他地方构造
if (!targetHref) {
targetHref = `https://docs2.listenai.com/arcs-sdk/${versionName}/zh/html/index.html`
}
if (targetHref) {
const newLink = document.createElement('a');
newLink.href = targetHref;
newLink.textContent = versionName;
item.appendChild(newLink);
} else {
// 如果找不到链接,至少显示版本名称
item.textContent = versionName;
}
}
});
}
// Update version label on page load
updateVersionFromURL();
console.log('Version switcher initialized');
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
/* Custom CSS for Doxygen-generated HTML
* Copyright (c) 2015 Intel Corporation
* SPDX-License-Identifier: Apache-2.0
*/
code {
font-family: Monaco,Menlo,Consolas,"Courier New",monospace;
background-color: #D8D8D8;
padding: 0 0.25em 0 0.25em;
}
pre.fragment {
display: block;
font-family: Monaco,Menlo,Consolas,"Courier New",monospace;
padding: 1rem;
word-break: break-all;
word-wrap: break-word;
white-space: pre;
background-color: #D8D8D8;
}
#projectlogo
{
vertical-align: middle;
}
#projectname
{
font: 200% Tahoma, Arial,sans-serif;
color: #3D578C;
}
#projectbrief
{
color: #3D578C;
}

View File

@@ -0,0 +1,40 @@
/**
Doxygen Awesome
https://github.com/jothepro/doxygen-awesome-css
MIT License
Copyright (c) 2021 jothepro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
@media screen and (min-width: 768px) {
#MSearchBox {
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)) - var(--searchbar-height) - 1px);
}
#MSearchField {
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)) - 66px - var(--searchbar-height));
}
}

View File

@@ -0,0 +1,108 @@
/**
Doxygen Awesome
https://github.com/jothepro/doxygen-awesome-css
MIT License
Copyright (c) 2021 jothepro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
html {
/* side nav width. MUST be = `TREEVIEW_WIDTH`.
* Make sure it is wide enough to contain the page title (logo + title + version)
*/
--side-nav-fixed-width: 340px;
--menu-display: none;
--top-height: 120px;
}
@media screen and (min-width: 768px) {
html {
--searchbar-background: var(--page-background-color);
}
#side-nav {
min-width: var(--side-nav-fixed-width);
max-width: var(--side-nav-fixed-width);
top: var(--top-height);
overflow: visible;
}
#nav-tree, #side-nav {
height: calc(100vh - var(--top-height)) !important;
}
#nav-tree {
padding: 0;
}
#top {
display: block;
border-bottom: none;
height: var(--top-height);
margin-bottom: calc(0px - var(--top-height));
max-width: var(--side-nav-fixed-width);
background: var(--side-nav-background);
}
#main-nav {
float: left;
padding-right: 0;
}
.ui-resizable-handle {
cursor: default;
width: 1px !important;
box-shadow: 0 calc(-2 * var(--top-height)) 0 0 var(--separator-color);
}
#nav-path {
position: fixed;
right: 0;
left: var(--side-nav-fixed-width);
bottom: 0;
width: auto;
}
#doc-content {
height: calc(100vh - 31px) !important;
padding-bottom: calc(3 * var(--spacing-large));
padding-top: calc(var(--top-height) - 80px);
box-sizing: border-box;
margin-left: var(--side-nav-fixed-width) !important;
}
#MSearchBox {
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)));
}
#MSearchField {
width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)) - 65px);
}
#MSearchResultsWindow {
left: var(--spacing-medium) !important;
right: auto;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
<!-- HTML header for doxygen 1.8.13-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=9"/>
<meta name="generator" content="Doxygen $doxygenversion"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="icon" href="$relpath^logo.ico">
<!--BEGIN PROJECT_NAME--><title>$projectname: $title</title><!--END PROJECT_NAME-->
<!--BEGIN !PROJECT_NAME--><title>$title</title><!--END !PROJECT_NAME-->
<link href="$relpath^tabs.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="$relpath^jquery.js"></script>
<script type="text/javascript" src="$relpath^dynsections.js"></script>
$treeview
$search
$mathjax
<link href="$relpath^$stylesheet" rel="stylesheet" type="text/css" />
$extrastylesheet
</head>
<body>
<div id="top"><!-- do not remove this div, it is closed by doxygen! -->
<!--BEGIN TITLEAREA-->
<div id="titlearea">
<table cellspacing="0" cellpadding="0">
<tbody>
<tr style="height: 56px;">
<!--BEGIN PROJECT_LOGO-->
<td id="projectlogo"><a href="https://www.listenai.com//"
target="_blank"><img alt="Logo" src="$relpath^$projectlogo"/></a></td>
<!--END PROJECT_LOGO-->
<!--BEGIN PROJECT_NAME-->
<td id="projectalign" style="padding-left: 1em;">
<div id="projectname">$projectname
<!--BEGIN PROJECT_NUMBER-->&#160;<span id="projectnumber">$projectnumber</span><!--END PROJECT_NUMBER-->
</div>
<!--BEGIN PROJECT_BRIEF--><div id="projectbrief">$projectbrief</div><!--END PROJECT_BRIEF-->
</td>
<!--END PROJECT_NAME-->
<!--BEGIN !PROJECT_NAME-->
<!--BEGIN PROJECT_BRIEF-->
<td style="padding-left: 0.5em;">
<div id="projectbrief">$projectbrief</div>
</td>
<!--END PROJECT_BRIEF-->
<!--END !PROJECT_NAME-->
<!--BEGIN DISABLE_INDEX-->
<!--BEGIN SEARCHENGINE-->
<td>$searchbox</td>
<!--END SEARCHENGINE-->
<!--END DISABLE_INDEX-->
</tr>
</tbody>
</table>
</div>
<!--END TITLEAREA-->
<!-- end header part -->

View File

@@ -0,0 +1,12 @@
# API Documentation
## Introduction
ARCS Software Development Kit (ARCS SDK) is a set of software enablement for LS26xx microcontrollers,
including low level peripheral drivers, components, middleware, and integrated RTOS support.
Besides of these components, ARCS SDK also provides related demos and examples, as well as documentation
to help users evaluate products and build up their application efficiently.
## Licensing
ARCS SDK is permissively licensed using the BSD 3-clause license.

3
arcs-sdk/docs/index.html Normal file
View File

@@ -0,0 +1,3 @@
<head>
<meta http-equiv="refresh" content="0; URL=./output/en/html/index.html" />
</head>

View File

@@ -0,0 +1,3 @@
<head>
<meta http-equiv="refresh" content="0; URL=./output/zh/html/index.html" />
</head>

View File

@@ -0,0 +1,7 @@
Sphinx>=6.0.0
sphinx-rtd-theme
myst-parser>=0.18.1
sphinx-inline-tabs
sphinxcontrib-moderncmakedomain
sphinx-tabs
standard-imghdr

View File

@@ -0,0 +1,22 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/zh/conf.py
# We recommend specifying your dependencies to enable reproducible builds:
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: docs/requirements.txt

View File

@@ -0,0 +1,11 @@
{%- extends "!layout.html" -%}
{%- block footer %}
{{ super() }}
{# Version switcher widget #}
{%- if version_switcher_html %}
{{ version_switcher_html|safe }}
{%- endif %}
{%- endblock %}

View File

@@ -0,0 +1,6 @@
# SDK API 参考
```{eval-rst}
关于SDK API 更多的信息可以查看 `SDK API 参考 <_static/api_doc/html/index.html>`_ 。
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

256
arcs-sdk/docs/zh/conf.py Normal file
View File

@@ -0,0 +1,256 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
import os
import os.path
import subprocess
import shutil
import re
import sys
import datetime
from pathlib import Path
import sphinx_rtd_theme
SDK_BASE = Path(__file__).resolve().parents[2]
HTML_STATIC_DIR = Path(__file__).resolve().parents[0] / "_static"
HTML_ASSETS_DIR = Path(__file__).resolve().parents[1] / "assets"
DOXY_OUT = Path(__file__).resolve().parents[0] / "_static" / "api_doc"
sys.path.insert(0, str(SDK_BASE / "docs" / "_ext"))
os.environ["SDK_BASE"] = str(SDK_BASE)
project = 'Arcs Software Development Kit'
copyright = '2020-%s, LISTENAI' % datetime.date.today().year
author = '聆思科技固件组'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx_rtd_theme',
'myst_parser',
"sphinx.ext.todo",
"sphinx.ext.extlinks",
'sphinx.ext.duration',
"sphinx.ext.viewcode",
'sphinxcontrib.moderncmakedomain',
"external_content",
"doxyrunner",
"sphinx_tabs.tabs",
"source_link" # 自动添加源码链接
]
templates_path = ['_templates']
DOXY_OUT.mkdir(parents = True, exist_ok = True)
doxyrunner_doxygen = os.environ.get("DOXYGEN_EXECUTABLE", "doxygen")
doxyrunner_doxyfile = SDK_BASE / "docs" / "doxygen" / "Doxyfile"
doxyrunner_outdir = DOXY_OUT
doxyrunner_fmt = True
doxyrunner_fmt_vars = {"SDK_BASE": str(SDK_BASE)}
doxyrunner_outdir_var = "DOXYGEN_OUTPUT_DIR"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = [
# 示例章节文件sample_build.rst, sample_flash.rst仅用于 include不作为独立页面
"sample_build.rst",
"sample_flash.rst",
]
html_extra_path = []
source_suffix = {
'.rst': 'restructuredtext',
'.md': 'markdown',
}
external_content_contents = [
(SDK_BASE, "docs/*.rst"),
(SDK_BASE, "CHANGELOG.md"),
(SDK_BASE / "docs/zh", "[!_]*"),
(SDK_BASE, "drivers/**/*.rst"),
(SDK_BASE, "drivers/**/*.md"),
(SDK_BASE, "boards/*.rst"),
(SDK_BASE, "boards/**/*.md"),
(SDK_BASE, "components/**/*.rst"),
(SDK_BASE, "components/**/*.md"),
(SDK_BASE, "samples/**/*.rst"),
(SDK_BASE, "samples/**/*.md"),
(SDK_BASE, "demos/**/*.rst"),
(SDK_BASE, "demos/**/*.md"),
(SDK_BASE, "tools/**/*.rst"),
(SDK_BASE, "tools/**/*.md"),
## modules目录下存在很多不规范的README.md指定添加构建目录
(SDK_BASE, "modules/fs/README.md"),
(SDK_BASE, "modules/lisa_shell/README.md"),
(SDK_BASE, "modules/usb/device/uvc/README.md"),
]
external_content_exclude = [
"drivers/porting_docs/**/*.md",
"drivers/lisa_device/**/*.md",
"drivers/Devices_Docs_Spec.md",
"samples/drivers/hal/**/README.md",
"samples/Samples_Spec.md",
"samples/drivers/devices/Devices_Samples_Spec.md",
"samples/modules/sqlite3/README.md",
"components/acomp/logger/**/README.md",
"demos/face_detect/src/button/FlexibleButton/README.md",
]
# Keep template files from being deleted by external_content
# Note: CSS/JS files are in docs/assets/ which is outside the source dir
external_content_keep = [
"_templates/*",
"_templates/**/*",
]
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_rtd_theme'
html_theme_options = {
"logo_only": True,
"prev_next_buttons_location": None
}
html_show_sphinx = False
html_logo = r'../assets/logo.svg'
html_static_path = [str(HTML_STATIC_DIR), str(HTML_ASSETS_DIR)]
html_last_updated_fmt = "%b %d, %Y"
html_domain_indices = False
html_split_index = True
html_show_sphinx = False
# Version switcher configuration
html_context = {
# 当前版本
'current_version': 'latest',
# 版本列表
'versions': [
('latest', 'https://docs2.listenai.com/arcs-sdk/latest/zh/html/index.html'),
('v0.1.0', 'https://docs2.listenai.com/arcs-sdk/v0.1.0/zh/html/index.html'),
('v0.1.1', 'https://docs2.listenai.com/arcs-sdk/v0.1.1/zh/html/index.html'),
('v0.1.2', 'https://docs2.listenai.com/arcs-sdk/v0.1.2/zh/html/index.html'),
],
# 显示版本警告横幅(可选)
'display_github': False,
}
# Add custom CSS and JS files
html_css_files = [
'version-switcher.css',
'feedback-widget.css',
]
html_js_files = [
'version-switcher.js',
'feedback-widget.js',
]
suppress_warnings = ['toc.excluded',
'toc.not_readable',
'toc.not_included', # 允许 sample_build.rst、sample_flash.rst 等被 include 的文件不在 toctree 中
'toc.secnum','toc.circular','epub.duplicated_toc_entry','autosectionlabel.*',
'app.add_source_parser',
'myst.header',
'ref.doc',
'ref.ref',
'myst.domains',
'myst.xref_missing']
myst_heading_anchors = 2
myst_enable_extensions = [
"amsmath",
"colon_fence",
"deflist",
"dollarmath",
"fieldlist",
"html_admonition",
"html_image",
"replacements",
"smartquotes",
"strikethrough",
"substitution",
"tasklist",
]
def setup(app):
"""Sphinx setup hook to add custom HTML for version switcher"""
def add_version_switcher(app, pagename, templatename, context, doctree):
"""Add version switcher HTML to every page"""
versions = context.get('versions', [])
current_version = context.get('current_version', '')
if versions:
version_html = '''
<div class="rst-versions" data-toggle="rst-versions" role="note">
<span class="rst-current-version" data-toggle="rst-current-version">
<span class="fa fa-book"> 文档版本 </span>
<span class="current-version-label">v: {}</span>
<span class="fa fa-caret-down"></span>
</span>
<div class="rst-other-versions">
<dl>
<dt>版本</dt>
'''.format(current_version)
for version_name, version_url in versions:
if version_name == current_version:
version_html += f' <dd><strong>{version_name}</strong></dd>\n'
else:
version_html += f' <dd><a href="{version_url}">{version_name}</a></dd>\n'
version_html += ''' </dl>
</div>
</div>
'''
context['version_switcher_html'] = version_html
app.connect('html-page-context', add_version_switcher)
# -- Source Link Configuration -----------------------------------------------
# 配置源码链接扩展
# 仓库基础 URL (仅配置到 /-/tree/ 之前的部分,分支名会自动根据版本添加)
# 支持 GitHub/GitLab 或内部 Git 仓库
# 例如: 'https://github.com/listenai/arcs-sdk' (会自动添加 /tree/分支名)
# 'https://cloud.listenai.com/CSKG836746/arcs-sdk/public/arcs-sdk' (会自动添加 /-/tree/分支名)
source_link_base_url = os.environ.get(
"SOURCE_LINK_BASE_URL",
"https://cloud.listenai.com/CSKG836746/arcs-sdk/public/arcs-sdk/-/tree/master"
)
# 文档版本到 Git 分支的映射
# 如果版本不在映射中,会使用以下规则:
# - 'latest' -> 'master'
# - 'v0.1.0' -> 'release/v0.1.0'
source_link_version_branches = {
'latest': 'master',
'v0.1.0': 'release/v0.1.0',
'v0.1.1': 'release/v0.1.1',
# 添加更多版本映射...
}
# 哪些文档需要添加源码链接 (支持通配符)
source_link_patterns = [
"samples/**/*.md",
"samples/**/*.rst",
"demos/**/*.md",
"demos/**/*.rst",
]
# 是否显示本地路径
source_link_show_local_path = True
# 源码位置标签文本
source_link_label = "📁 源码位置"

464
arcs-sdk/docs/zh/gdb.rst Normal file
View File

@@ -0,0 +1,464 @@
.. _gdb_debug:
================
GDB 调试指南
================
本文档介绍如何使用 GDB 调试 ARCS SDK 应用程序,包括环境配置、调试方式和常用命令。
.. note::
ARCS SDK 使用 RISC-V 架构的 Nuclei 工具链,调试时需要使用对应的 GDB 工具。
.. _gdb_overview:
调试概述
========
GDBGNU Debugger是一个功能强大的调试工具支持断点、单步执行、变量查看等调试功能。在嵌入式开发中通常使用 GDB 配合 JTAG/SWD 调试器进行远程调试。
支持的调试方式
--------------
- **JTAG 调试**:通过 JTAG 接口连接调试器
- **远程 GDB 调试**:使用 GDB Server 进行远程调试
- **命令行调试**:使用 GDB 命令行界面进行调试
.. _gdb_preparation:
环境准备
========
安装调试工具
------------
1. **确认工具链安装**
确保已按照 :ref:`getting_started` 中的步骤安装了 Nuclei 工具链。
2. **安装 J-Link 软件**
`J-LINK <https://www.segger.com/downloads/jlink/>`_ 下载对应平台的 J-Link 软件包进行安装,推荐安装 V7.98 及以上版本。
安装完成后,检查 J-Link 是否正常安装:
.. code-block:: shell
JLinkGDBServerCLExe --version
3. **配置 J-Link 设备支持**
下载并安装 ARCS J-Link 设备配置文件:
.. code-block:: shell
curl -L -o /tmp/JLinkDevices.zip \
http://listenai-firmware-delivery.oss-cn-beijing.aliyuncs.com/ARCS/tools/JLinkDevices.zip && \
unzip -o /tmp/JLinkDevices.zip -d $HOME/.config/SEGGER
检查 ARCS 设备是否正常识别:
.. code-block:: shell
JLinkGDBServerCLExe -device ARCS -if cJTAG -speed 4000 -port 2331 \
-jlinkscriptfile $HOME/.config/SEGGER/JLinkDevices/scripts/arcs/jtagscan1.JLinkScript
.. important::
$HOME/.config/SEGGER/JLinkDevices/scripts/arcs目录下有两个脚本文件
jtagscan0.JLinkScript: 连接core0(AP核心)
jtagscan1.JLinkScript: 连接core1(CP核心)
请根据实际情况选择使用哪一个。
.. note::
SDK的示例代码如无特别说明外,默认都是在core1(CP核心)上运行的.
4. **验证 GDB 工具**
检查 GDB 工具是否可用:
.. code-block:: shell
${NUCLEI_TOOLCHAIN_PATH}/bin/riscv64-unknown-elf-gdb --version
应该看到类似以下输出:
.. code-block:: text
GNU gdb (GDB) 13.2.90.20230712-git
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
硬件要求
--------
.. important::
需要 J-Link 仿真器 V11 或更高版本。
准备调试文件
------------
编译时需要生成包含调试信息的 ELF 文件:
.. code-block:: shell
./build.sh -S samples/helloworld -DBOARD=arcs_evb
编译成功后,在 ``build`` 目录下会生成:
- ``helloworld``:包含调试符号的可执行文件
- ``helloworld.bin``:用于烧录的二进制文件
.. note::
默认情况下SDK 编译生成的 ELF 文件已包含调试信息(``-g`` 选项)。
.. _gdb_debugging:
开始调试
========
硬件连接
--------
将 J-Link 仿真器连接到 ARCS 开发板:
.. image:: assets/ARCS-JLINK调试器连接图.png
:alt: ARCS J-Link 调试器连接示意图
调试步骤
--------
完整的 GDB 调试流程如下:
1. **启动 J-Link GDB Server**
在终端中启动 J-Link GDB Server
.. code-block:: shell
JLinkGDBServerCLExe -device ARCS -if cJTAG -speed 4000 -port 2331 \
-jlinkscriptfile $HOME/.config/SEGGER/JLinkDevices/scripts/arcs/jtagscan1.JLinkScript
成功启动后GDB Server 会监听在 2331 端口。
2. **启动 GDB 并加载 ELF 文件**
在另一个终端中启动 GDB
.. code-block:: shell
${NUCLEI_TOOLCHAIN_PATH}/bin/riscv64-unknown-elf-gdb build/helloworld
启动后会进入 GDB 命令行界面:
.. code-block:: text
GNU gdb (GDB) 13.2.90.20230712-git
...
Reading symbols from build/helloworld...
(gdb)
3. **连接到 GDB Server**
在 GDB 命令行中连接到 J-Link GDB Server
.. code-block:: text
(gdb) target remote localhost:2331
.. note::
- 如果 GDB Server 运行在其他机器上,将 ``localhost`` 替换为对应的 IP 地址
- 默认端口为 2331如果使用其他端口请相应修改
4. **加载程序到目标设备**
连接成功后,将程序加载到目标设备:
.. code-block:: text
(gdb) load
Loading section .text, size 0x1234 lma 0x20000000
...
Start address 0x20000000, load size 4660
Transfer rate: 1165 bytes/sec, 1165 bytes/write.
5. **开始调试**
设置断点并运行程序:
.. code-block:: text
(gdb) break main
Breakpoint 1 at 0x20000a34: file main.c, line 15.
(gdb) continue
Continuing.
快速调试脚本
------------
为了简化调试流程,可以创建一个 ``.gdbinit`` 文件来自动执行常用命令:
.. code-block:: text
# 连接到 GDB Server
target remote localhost:2331
.. note::
出于安全考虑GDB 可能不会自动加载当前目录的 ``.gdbinit`` 文件。可以在用户主目录的 ``~/.gdbinit`` 中添加:
.. code-block:: text
set auto-load safe-path /
.. _gdb_commands:
常用 GDB 命令
=============
本节介绍嵌入式调试中最常用的 GDB 命令。完整的 GDB 命令参考请查阅 :ref:`gdb_references`
基本调试命令
------------
.. list-table::
:header-rows: 1
:widths: 30 70
* - 命令
- 说明
* - ``break main``
- 在 main 函数设置断点
* - ``break main.c:25``
- 在 main.c 第 25 行设置断点
* - ``info breakpoints``
- 查看所有断点
* - ``delete 1``
- 删除编号为 1 的断点
* - ``continue`` (``c``)
- 继续执行
* - ``next`` (``n``)
- 单步执行(不进入函数)
* - ``step`` (``s``)
- 单步执行(进入函数)
* - ``finish``
- 执行完当前函数并返回
查看变量和内存
--------------
.. code-block:: text
(gdb) print variable_name # 打印变量值(简写: p
(gdb) print /x variable_name # 以十六进制打印
(gdb) x/10xw 0x20000000 # 查看内存10个字十六进制
(gdb) display variable_name # 每次停止时自动显示变量
调用栈和寄存器
--------------
.. code-block:: text
(gdb) backtrace # 显示调用栈(简写: bt
(gdb) info locals # 显示局部变量
(gdb) info registers # 显示所有寄存器
(gdb) print $pc # 打印程序计数器
实用命令
--------
.. code-block:: text
(gdb) list # 查看源代码(简写: l
(gdb) info threads # 显示所有线程RTOS 环境)
(gdb) help # 显示帮助信息
(gdb) quit # 退出 GDB简写: q
.. _gdb_examples:
调试示例
========
以调试 helloworld 示例为例,展示完整的调试流程:
1. **编译项目**
.. code-block:: shell
./build.sh -C -S samples/helloworld -DBOARD=arcs_evb
2. **启动 J-Link GDB Server**
参考 :ref:`gdb_debugging`
.. code-block:: shell
JLinkGDBServerCLExe -device ARCS -if cJTAG -speed 4000 -port 2331 -jlinkscriptfile $HOME/.config/SEGGER/JLinkDevices/scripts/arcs/jtagscan1.JLinkScript
3. **启动 GDB 并调试**
.. code-block:: shell
${NUCLEI_TOOLCHAIN_PATH}/bin/riscv64-unknown-elf-gdb build/helloworld
在 GDB 中执行:
.. code-block:: text
(gdb) target remote localhost:2331
(gdb) break main
(gdb) continue
(gdb) next # 单步执行
(gdb) print variable_name # 查看变量
(gdb) backtrace # 查看调用栈
(gdb) info registers # 查看寄存器
常见调试场景
------------
**内存问题调试**
.. code-block:: text
(gdb) x/10xw $sp # 查看栈顶内存
(gdb) x/20i $pc # 查看当前指令
(gdb) watch *0x20001000 # 监视内存变化
**函数调用跟踪**
.. code-block:: text
(gdb) break func_name
(gdb) backtrace # 查看调用栈
(gdb) info args # 查看函数参数
(gdb) finish # 执行完函数
.. _gdb_tips:
调试技巧
========
条件断点和监视点
----------------
条件断点只在满足条件时才停止,监视点可以监视变量或内存的变化:
.. code-block:: text
(gdb) break main.c:30 if counter > 100 # 条件断点
(gdb) watch variable_name # 监视变量变化
(gdb) watch *(int *)0x20000100 # 监视内存地址
查看汇编代码
------------
对于底层调试,可以查看汇编代码:
.. code-block:: text
(gdb) disassemble main # 反汇编 main 函数
(gdb) disassemble /m main # 混合显示源码和汇编
(gdb) stepi # 汇编级单步执行
.. _gdb_troubleshooting:
常见问题
========
1. **找不到调试符号**
问题现象:
.. code-block:: text
Reading symbols from build/helloworld...
(No debugging symbols found)
解决方法:
- 确保编译时包含调试信息(``-g`` 选项)
- 检查是否误用了 ``.bin`` 文件而非 ``.elf`` 文件
2. **无法连接到 GDB Server**
问题现象:
.. code-block:: text
(gdb) target remote localhost:2331
localhost:2331: Connection refused.
解决方法:
- 确认 J-Link GDB Server 已正确启动
- 检查端口号是否正确(默认 2331
- 检查防火墙设置
- 确认 J-Link 仿真器已正确连接到开发板
3. **断点无法命中**
可能原因:
- 代码被优化掉(使用 ``-O0`` 编译选项禁用优化)
- 断点位置不正确
- 程序未正确加载到目标设备
解决方法:
.. code-block:: text
(gdb) info breakpoints # 检查断点状态
(gdb) break *0x20000a34 # 使用绝对地址设置断点
4. **程序执行位置与源代码不对应**
解决方法:
- 确保 ELF 文件与烧录的 BIN 文件版本一致
- 重新编译并烧录程序
- 检查是否有多个版本的源文件
5. **查看变量显示 optimized out**
问题原因:
编译优化导致变量被优化掉。
解决方法:
- 使用 ``-O0`` 编译选项禁用优化(仅用于调试)
- 在 CMakeLists.txt 中添加:
.. code-block:: cmake
add_compile_options(-O0 -g)
.. _gdb_references:
参考资料
========
GDB 文档
--------
- `GDB 官方文档 <https://sourceware.org/gdb/documentation/>`_ - GDB 完整用户手册
- `GDB 快速参考卡片 <https://users.ece.utexas.edu/~adnan/gdb-refcard.pdf>`_ - 常用命令速查PDF
RISC-V 相关
-----------
- `RISC-V GDB 使用指南 <https://github.com/riscv/riscv-gnu-toolchain>`_ - RISC-V 工具链文档
- `Nuclei RISC-V 工具链 <https://nucleisys.com/>`_ - Nuclei 官方工具链文档
J-Link 文档
-----------
- `J-Link / J-Trace 用户手册 <https://www.segger.com/downloads/jlink/UM08001>`_ - J-Link 官方用户手册
- `J-Link GDB Server 文档 <https://wiki.segger.com/J-Link_GDB_Server>`_ - GDB Server 配置和使用
ARCS SDK 相关
-------------
- ARCS SDK 快速入门::ref:`getting_started`

View File

@@ -0,0 +1,217 @@
.. _getting_started:
================
快速入门
================
本文档介绍如何快速开始使用 ARCS SDK 进行开发,包括环境搭建、编译示例和烧录运行。
.. note::
目前仅支持 Linux 平台,推荐使用 Ubuntu 18.04 以上版本。
.. _environment_setup:
环境搭建
========
自动搭建(推荐)
----------------
1. **下载开发工具包**
在 SDK 根目录下运行脚本:
.. code-block:: shell
./prepare_listenai_tools.sh
2. **下载工具链**
运行脚本下载工具链:
.. code-block:: shell
./prepare_toolchain.sh
3. **设置环境变量**
.. code-block:: shell
# 设置工具链路径
export NUCLEI_TOOLCHAIN_PATH=/path/to/toolchain
# 设置 ListenAI 工具包路径
export LISTENAI_TOOLS_PATH=/path/to/listenai-tools
.. warning::
**必须使用绝对路径!** 环境变量的路径必须是绝对路径(如 ``/home/user/toolchain``),不能使用相对路径(如 ``./toolchain````../toolchain``),否则会导致编译失败。
其中:
- ``NUCLEI_TOOLCHAIN_PATH`` 指向解压后的工具链路径(绝对路径)
- ``LISTENAI_TOOLS_PATH`` 指向解压后的 ListenAI 工具包路径(绝对路径)
手动搭建
--------
如果自动搭建失败,可以手动搭建开发环境:
1. **下载工具链**
下载对应平台的工具链并解压(如果已存在工具链,可跳过此步骤):
- `Linux 工具链下载地址 <http://listenai-firmware-delivery.oss-cn-beijing.aliyuncs.com/ARCS/tools/toolchain/linux-amd64/nuclei_riscv_newlibc_prebuilt_linux64_2025.02.tar.bz2>`_
2. **下载 ListenAI 开发工具包**
- `Linux 开发工具包下载地址 <http://listenai-firmware-delivery.oss-cn-beijing.aliyuncs.com/ARCS/tools/dev-tools/linux-amd64/v0.0.1/listenai-tools.tar.gz>`_
3. **设置环境变量**
.. code-block:: shell
# 设置工具链路径
export NUCLEI_TOOLCHAIN_PATH=/path/to/toolchain
# 设置 ListenAI 工具包路径
export LISTENAI_TOOLS_PATH=/path/to/listenai-tools
.. warning::
**必须使用绝对路径!** 环境变量的路径必须是绝对路径(如 ``/home/user/toolchain``),不能使用相对路径(如 ``./toolchain````../toolchain``),否则会导致编译失败。
其中:
- ``NUCLEI_TOOLCHAIN_PATH`` 指向解压后的工具链路径(绝对路径)
- ``LISTENAI_TOOLS_PATH`` 指向解压后的 ListenAI 工具包路径(绝对路径)
.. _quick_start:
快速开始
========
编译示例
--------
以 helloworld 工程为例,演示如何编译项目:
1. **编译命令**
在 SDK 根目录下执行:
.. code-block:: shell
./build.sh -C -S samples/helloworld -DBOARD=arcs_evb
命令参数说明:
- ``-S``: 指定项目源码路径
- ``-DBOARD``: 指定目标板型(必需参数,如 arcs_mini、arcs_evb 等)
- ``-C``: 清理构建目录(可选)
2. **编译输出**
编译成功后会在 ``build`` 目录下生成构建产物,包括:
- ``helloworld.bin``: 烧录文件
- ``helloworld.elf``: 调试文件
- 其他相关文件
.. _flashing:
烧录运行
========
准备工作
--------
1. **连接硬件**
将串口板连接到开发板:
- 开发板 TX 脚 (默认引脚PA2注意查看板型文件) 连接串口板 RX
- 开发板 RX 脚 (默认引脚PA3注意查看板型文件) 连接串口板 TX
- 开发板 GND 连接串口板 GND
2. **进入烧录模式**
按住 BOOT 脚后复位开发板,进入烧录模式。
.. note::
每次重新烧录前,都需要执行按住 BOOT 脚后复位开发板的操作。
自动烧录(推荐)
----------------
如果希望实现自动烧录,可以连接控制引脚:
- 开发板 BOOT 脚连接串口板 RTS 脚
- 开发板 RESET 脚连接串口板 DTR 脚
这样 cskburn 工具可以自动控制进入烧录模式。
烧录命令
--------
使用 cskburn 工具进行烧录:
.. code-block:: shell
./tools/burn/cskburn -s /dev/ttyUSB0 -b 3000000 0x0 build/helloworld.bin -C arcs
命令参数说明:
- ``-s``: 指定烧录设备(串口设备路径)
.. note::
请根据实际情况选择正确的串口设备:
- 使用 ``ls /dev/ttyUSB*````ls /dev/ttyACM*`` 查看可用设备
- 常见设备名:``/dev/ttyUSB0````/dev/ttyUSB1````/dev/ttyACM0``
- 插入串口板时可使用 ``dmesg | tail`` 查看系统分配的设备名
- ``-b``: 指定烧录波特率(推荐使用 3000000
- ``0x0``: 烧录起始地址(基于 0x30000000 flash 起始地址的偏移)
- ``build/helloworld.bin``: 烧录文件路径
- ``-C arcs``: 指定芯片类型
验证运行
--------
烧录完成后复位开发板,应该可以在串口控制台看到以下输出:
.. code-block:: text
Running on hart-id: 1
Hello, world!
.. _troubleshooting:
常见问题
========
1. **权限问题**
如果遇到串口权限问题,将当前用户添加到 dialout 组:
.. code-block:: shell
sudo usermod -a -G dialout $USER
然后重新登录。
2. **串口设备问题**
使用 ``dmesg````ls /dev/ttyUSB*`` 查看串口设备:
.. code-block:: shell
ls /dev/ttyUSB*
3. **环境变量问题**
确保已正确设置环境变量,可以使用以下命令检查:
.. code-block:: shell
echo $NUCLEI_TOOLCHAIN_PATH
echo $LISTENAI_TOOLS_PATH

View File

@@ -0,0 +1,24 @@
.. _arcs_sdk_base:
ARCS SDK 文档
############################
欢迎查阅ARCS SDK开发文档
********
.. toctree::
:maxdepth: 1
:caption: 章节
get_started
gdb
boards/index_zh
drivers/index_zh
components/index_zh
samples/index_zh
demos/index_zh
tools/index_zh
thirds
api_doc
CHANGELOG

View File

@@ -0,0 +1,34 @@
**重要提示**在编译前请先确认您使用的开发板型号。SDK 目前支持以下开发板:
- **arcs_evb** - ARCS EVB 评估板
- **arcs_mini** - ARCS Mini 开发板
根据您的开发板型号,选择对应的编译命令:
在示例目录下执行编译:
.. code-block:: bash
# 使用 arcs_evb 开发板
./build.sh -C -DBOARD=arcs_evb
# 或使用 arcs_mini 开发板
./build.sh -C -DBOARD=arcs_mini
.. note::
如果在 SDK 根目录执行,需要指定示例路径:
.. code-block:: bash
# 使用 arcs_evb 开发板
./build.sh -C -S samples/<示例路径> -DBOARD=arcs_evb
# 或使用 arcs_mini 开发板
./build.sh -C -S samples/<示例路径> -DBOARD=arcs_mini
.. note::
确保已安装对应的工具链。

View File

@@ -0,0 +1,25 @@
编译完成后,使用 SDK tools 目录下的 cskburn 工具烧录固件:
.. code-block:: bash
./tools/burn/cskburn -s /dev/ttyUSB0 -b 3000000 0x0 build/arcs.bin -C arcs
.. note::
**烧录参数说明**
- ``-s /dev/ttyUSB0``:串口设备路径,**需要根据实际情况修改**
- Linux 系统:通常是 ``/dev/ttyUSB0````/dev/ttyACM0``
- 可通过 ``ls /dev/tty*`` 命令查看可用串口设备
- 不同开发板或 USB 转串口芯片可能使用不同的设备名
- ``-b 3000000``烧录波特率3Mbps
- ``0x0``:烧录起始地址
- ``build/arcs.bin``:编译生成的固件路径
- ``-C arcs``:芯片类型
**注意事项**
- 确保开发板已正确连接到电脑
- 如果无法识别串口设备,请检查 USB 连接线是否正常,或尝试其他 USB 端口

227
arcs-sdk/docs/zh/thirds.rst Normal file
View File

@@ -0,0 +1,227 @@
.. _thirds:
第三方库支持
############
ARCS SDK 集成了多个第三方开源库,以提供丰富的功能支持。以下是当前支持的第三方库列表。
********
实时操作系统 (RTOS)
============================
.. list-table::
:header-rows: 1
:widths: 20 80
* - 库名称
- 说明
* - FreeRTOS
- 经典的实时操作系统内核,提供任务调度、队列、信号量等核心功能
* - FreeRTOS-CPP11
- FreeRTOS 的 C++11 封装库,提供面向对象的 RTOS 接口
* - rtos_al
- RTOS 抽象层,提供统一的 RTOS API 接口
图形界面
============================
.. list-table::
:header-rows: 1
:widths: 20 80
* - 库名称
- 说明
* - LVGL
- 轻量级图形库,用于嵌入式系统的 GUI 开发
* - LVGL8
- LVGL 版本 8.x提供更新的图形界面功能
网络协议
============================
.. list-table::
:header-rows: 1
:widths: 20 80
* - 库名称
- 说明
* - coreHTTP
- 轻量级 HTTP 客户端库,适用于嵌入式设备
* - coreSNTP
- 简单网络时间协议 (SNTP) 客户端实现
* - libcurl
- 强大的网络传输库,支持多种协议
* - httpclient
- HTTP 客户端实现
* - http_ssl
- 支持 SSL/TLS 的 HTTP 客户端
* - nopoll
- WebSocket 客户端和服务器库
* - mbedtls
- 轻量级 SSL/TLS 加密库
* - coreMQTT
- 轻量级的MQTT客户端库适用于嵌入式设备
* - coreMQTT-Agent
- 针对coreMQTT接口的线程安全封装库
数据格式与解析
============================
.. list-table::
:header-rows: 1
:widths: 20 80
* - 库名称
- 说明
* - cJSON
- 轻量级 JSON 解析库
* - libxml2
- XML 解析和处理库
文件系统
============================
.. list-table::
:header-rows: 1
:widths: 20 80
* - 库名称
- 说明
* - filesystem
- 文件系统支持
* - fs
- 文件系统抽象层
* - EasyFlash
- 嵌入式 Flash 存储管理库,提供 KV 数据库、日志存储等功能
音频处理
============================
.. list-table::
:header-rows: 1
:widths: 20 80
* - 库名称
- 说明
* - libmad
- MPEG 音频解码器库
* - libid3tag
- ID3 标签解析库,用于读取音频文件元数据
* - mp3dec
- MP3 解码器
* - aacdec
- AAC 音频解码器
* - speexdsp
- Speex 数字信号处理库,提供音频处理功能
* - resample
- 音频重采样库
* - lisa_player
- 音频播放器实现
图像处理
============================
.. list-table::
:header-rows: 1
:widths: 20 80
* - 库名称
- 说明
* - ijg
- Independent JPEG Group 的 JPEG 编解码库
* - libpng
- PNG 图像格式处理库
* - giflib
- GIF 图像格式处理库
* - libico
- ICO 图标格式处理库
* - freetype
- 字体渲染引擎
工具库
============================
.. list-table::
:header-rows: 1
:widths: 20 80
* - 库名称
- 说明
* - collections-c
- C 语言数据结构集合库
* - crc32
- CRC32 校验和计算库
* - zlib
- 数据压缩库
* - letter-shell
- 嵌入式 Shell 命令行工具
* - easylogger
- 轻量级日志系统
* - mempool
- 内存池管理库
* - heap
- 堆内存管理实现
* - ltlsf
- TLSF (Two-Level Segregated Fit) 内存分配器
* - uchardet
- 字符编码检测库
* - csk_sqlite3
- SQLite3 数据库引擎
系统管理
============================
.. list-table::
:header-rows: 1
:widths: 20 80
* - 库名称
- 说明
* - mac_manager
- MAC 地址管理模块
* - wifi_manager
- Wi-Fi 连接管理模块
测试框架
============================
.. list-table::
:header-rows: 1
:widths: 20 80
* - 库名称
- 说明
* - Unity
- C 语言单元测试框架
* - CppUTest
- C/C++ 单元测试框架
* - GoogleTest
- Google 的 C++ 测试框架
* - FFF
- Fake Function FrameworkC 语言函数 Mock 框架,用于单元测试
其他
============================
.. list-table::
:header-rows: 1
:widths: 20 80
* - 库名称
- 说明
* - cpr
- C++ HTTP 请求库
* - tinyusb
- 轻量级 USB 协议栈
* - flexlayout
- 灵活的布局引擎
********
使用说明
============================
这些第三方库已经集成到 ARCS SDK 的构建系统中,可以通过 Kconfig 配置启用所需的库。
具体的使用方法和 API 文档请参考各个库的官方文档或查看 ``modules/`` 目录下对应库的 README 文件。