Compare commits
291
Commits
e89578fa82
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81ba942b8e | ||
|
|
bbb88a96f8 | ||
|
|
7b0b49fde1 | ||
|
|
5a8ac61502 | ||
|
|
8b3863f3ae | ||
|
|
6483719551 | ||
|
|
9c73d70582 | ||
|
|
0348b959bb | ||
|
|
3ebf402efd | ||
|
|
63b5243faf | ||
|
|
1b6a767f62 | ||
|
|
7f6f077469 | ||
|
|
60db2c40d8 | ||
|
|
08aff2656d | ||
|
|
c059589e94 | ||
|
|
d1e4b5b01a | ||
|
|
897e76fd8f | ||
|
|
ac2677abc7 | ||
|
|
d680dcd058 | ||
|
|
84a1a5e3be | ||
|
|
058eb11f5f | ||
|
|
85c9699758 | ||
|
|
9dd16e0eef | ||
|
|
28894e48dc | ||
|
|
924c718005 | ||
|
|
d71f526af4 | ||
|
|
09556c9ef4 | ||
|
|
cc671a9fce | ||
|
|
f7ed1f785b | ||
|
|
2ff7908bc7 | ||
|
|
745ce06f85 | ||
|
|
f17a6a8233 | ||
|
|
30360cb106 | ||
|
|
ec3df34b43 | ||
|
|
c7441c1c6f | ||
|
|
fed2d42472 | ||
|
|
2918f2c83c | ||
|
|
87538e7729 | ||
|
|
fd088f5959 | ||
|
|
15f8c9552d | ||
|
|
43c5821d3d | ||
|
|
1d7641cc89 | ||
|
|
6a68b3393d | ||
|
|
0700491300 | ||
|
|
d036af935f | ||
|
|
34cd295005 | ||
|
|
c8812dd27c | ||
|
|
3460a157d0 | ||
|
|
fe618be1fa | ||
|
|
4d9bb5d940 | ||
|
|
37c6d401d6 | ||
|
|
507054c4b2 | ||
|
|
85e0d0740b | ||
|
|
dc52ec13ad | ||
|
|
5ff1dfa4d2 | ||
|
|
2eb6d3c310 | ||
|
|
d921f84b83 | ||
|
|
69a8eee192 | ||
|
|
fa9679b7a6 | ||
|
|
53ddcb364f | ||
|
|
fb485ebac4 | ||
|
|
da4957c52d | ||
|
|
b37bf255d9 | ||
|
|
2f5fe8baaa | ||
|
|
2e3f07bf4f | ||
|
|
028e0e0806 | ||
|
|
37c50ed517 | ||
|
|
02904f711e | ||
|
|
d24066369b | ||
|
|
417fda5479 | ||
|
|
fb5920d89f | ||
|
|
dd4fef6dee | ||
|
|
87f209af73 | ||
|
|
47ab9f2fe6 | ||
|
|
ac16d89150 | ||
|
|
f53085b2a6 | ||
|
|
4749e58eeb | ||
|
|
5fbe3ed5e4 | ||
|
|
d3158c2420 | ||
|
|
a7a78d1424 | ||
|
|
9dbe395484 | ||
|
|
9f440ee884 | ||
|
|
4449a23ba1 | ||
|
|
40b9b9c92f | ||
|
|
0493383ee8 | ||
|
|
84fb370da8 | ||
|
|
8499631091 | ||
|
|
38a6e815f0 | ||
|
|
c58ef2b873 | ||
|
|
52052e51a8 | ||
|
|
ffbe076a32 | ||
|
|
92820799fa | ||
|
|
054f65f858 | ||
|
|
918f3ea32a | ||
|
|
b216535757 | ||
|
|
45db2f780b | ||
|
|
c1724ccea9 | ||
|
|
f9c02cd6ad | ||
|
|
c7364286b7 | ||
|
|
c3c07ab06d | ||
|
|
2e210e4729 | ||
|
|
5a3dff9026 | ||
|
|
7e4fd7e54a | ||
|
|
8200cbfba7 | ||
|
|
192610615e | ||
|
|
2bb1d69d8a | ||
|
|
392a734707 | ||
|
|
89b374c21d | ||
|
|
af5cd02663 | ||
|
|
763faf9c83 | ||
|
|
f3d06252bb | ||
|
|
b60ac06d55 | ||
|
|
66963092a3 | ||
|
|
27ab50ae68 | ||
|
|
627950394d | ||
|
|
16b059253b | ||
|
|
f701730016 | ||
|
|
f319686571 | ||
|
|
08f737ba2c | ||
|
|
c7ef7e715c | ||
|
|
9d737897df | ||
|
|
b935fefb82 | ||
|
|
9ccb9cbdca | ||
|
|
de58c0487a | ||
|
|
e7a2877422 | ||
|
|
7d6ba6de93 | ||
|
|
b30ce1424b | ||
|
|
7ef6a0b65f | ||
|
|
531358919c | ||
|
|
bd1d7ca4d0 | ||
|
|
d883dc620e | ||
|
|
101aed48be | ||
|
|
0ae107f953 | ||
|
|
de0e4168b2 | ||
|
|
1a5e9f7064 | ||
|
|
84adec4722 | ||
|
|
2cbb93b3b7 | ||
|
|
6ebd544619 | ||
|
|
c06eb19f64 | ||
|
|
18387586a4 | ||
|
|
46b66f8bd8 | ||
|
|
e25c132722 | ||
|
|
373f6af352 | ||
|
|
65580e7e50 | ||
|
|
227fbb17f4 | ||
|
|
cebef9fe70 | ||
|
|
12ece9020c | ||
|
|
6ed88feaa4 | ||
|
|
bf2b5d31f7 | ||
|
|
9815ff44af | ||
|
|
5edb67b46d | ||
|
|
039d6acf37 | ||
|
|
ba9a77fe66 | ||
|
|
e007990594 | ||
|
|
b79044d631 | ||
|
|
81ae84851d | ||
|
|
4e190953c0 | ||
|
|
b4436b192a | ||
|
|
bd544a4181 | ||
|
|
164bc65b95 | ||
|
|
53ce69c8dd | ||
|
|
2f3e63e499 | ||
|
|
90761a723d | ||
|
|
b53d4999ce | ||
|
|
e4e9968d14 | ||
|
|
9efb96ea66 | ||
|
|
6c587a22d8 | ||
|
|
bdbea6ac40 | ||
|
|
3be9e78335 | ||
|
|
1b9c727e80 | ||
|
|
e8fa26d79f | ||
|
|
da7f11d352 | ||
|
|
9708fbc87e | ||
|
|
ae42ab8a91 | ||
|
|
a80bd582da | ||
|
|
1b9e6f6b3a | ||
|
|
f003b4dfaa | ||
|
|
eb506e5cbf | ||
|
|
ec1eb1e805 | ||
|
|
6ee1eca789 | ||
|
|
0ef96c28f6 | ||
|
|
49f0698b9d | ||
|
|
2774c3471f | ||
|
|
287cab189d | ||
|
|
7d7e3dc10a | ||
|
|
55fbe2f1f1 | ||
|
|
ae92a6edde | ||
|
|
267d572906 | ||
|
|
e248290860 | ||
|
|
48b45cb44a | ||
|
|
e7f435dc4e | ||
|
|
ada2af47af | ||
|
|
ea670adde2 | ||
|
|
5839172dae | ||
|
|
3f97dd92ef | ||
|
|
f935448389 | ||
|
|
32d3a5a19f | ||
|
|
edb53d079c | ||
|
|
74610c32c8 | ||
|
|
eec869a28d | ||
|
|
d309f41e47 | ||
|
|
7589b77916 | ||
|
|
e4831522a4 | ||
|
|
8a14247aed | ||
|
|
803d176066 | ||
|
|
ac8f3da9df | ||
|
|
6649b31e17 | ||
|
|
254fbf4628 | ||
|
|
931e134796 | ||
|
|
63a80694ae | ||
|
|
4506601773 | ||
|
|
f214c862d3 | ||
|
|
a8a190571c | ||
|
|
aa3a99825c | ||
|
|
dcfdbcb9f2 | ||
|
|
62007e7848 | ||
|
|
91e6659dac | ||
|
|
9765edac9b | ||
|
|
40175eddb5 | ||
|
|
5c33dbe027 | ||
|
|
a58a0f97e3 | ||
|
|
55b127747a | ||
|
|
8c1956cdeb | ||
|
|
1388fcb72b | ||
|
|
b4e1cf462a | ||
|
|
46c3a197be | ||
|
|
e9f9860fe1 | ||
|
|
949d274c22 | ||
|
|
bcd6c84e3b | ||
|
|
5275b8bf75 | ||
|
|
e6e5dab4cd | ||
|
|
3c02ba43ad | ||
|
|
c5dd8d198d | ||
|
|
f4a7aca2fa | ||
|
|
f5b69d0b4c | ||
|
|
53c742c26e | ||
|
|
11993e178b | ||
|
|
2a6d68386d | ||
|
|
3bc4f9720f | ||
|
|
751866d9c1 | ||
|
|
3f9bffb9ea | ||
|
|
ace98caff6 | ||
|
|
a0f6a14b5d | ||
|
|
2fc12e7b00 | ||
|
|
dae81ba4a1 | ||
|
|
a905dac00d | ||
|
|
b8ec752a12 | ||
|
|
b86b089634 | ||
|
|
f9cf408c82 | ||
|
|
f0cdaacd85 | ||
|
|
6a0551ab34 | ||
|
|
c2e9cce6a7 | ||
|
|
b07dbf2da5 | ||
|
|
19d5b65625 | ||
|
|
12e736d264 | ||
|
|
8be62683dc | ||
|
|
c93023bd5d | ||
|
|
422ca798da | ||
|
|
c064caa3ec | ||
|
|
f91d4166d6 | ||
|
|
afb55494e5 | ||
|
|
6692c9c4ee | ||
|
|
dabd892601 | ||
|
|
2982bc30a4 | ||
|
|
aaeac343d3 | ||
|
|
9e2667d604 | ||
|
|
c592b02636 | ||
|
|
d5a67ce77e | ||
|
|
6330e04474 | ||
|
|
df518d1adb | ||
|
|
9078c4d051 | ||
|
|
bcd49f0e81 | ||
|
|
78ad3e6d57 | ||
|
|
7803843155 | ||
|
|
ec755a30f2 | ||
|
|
483c5f227f | ||
|
|
bfd2d334fe | ||
|
|
50f209e84a | ||
|
|
680d28b616 | ||
|
|
4d16bc32fc | ||
|
|
7726857d6b | ||
|
|
3ef7d1b780 | ||
|
|
62804d242c | ||
|
|
a5e581ef24 | ||
|
|
6fcb70d235 | ||
|
|
323dd3061b | ||
|
|
44c1e5ef72 | ||
|
|
d06546f6bf | ||
|
|
4ad3fbfca8 | ||
|
|
b2ddd9e7e3 | ||
|
|
e923b32b70 |
No files matched your search
@@ -0,0 +1,61 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.so
|
||||||
|
.env
|
||||||
|
.env/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
*.bin
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Documentation (exclude READMEs)
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Data files (handled via volumes)
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
data/
|
||||||
|
chroma/
|
||||||
|
|
||||||
|
# Other modules not needed for build
|
||||||
|
bot4cj/
|
||||||
|
core/
|
||||||
|
gateway/
|
||||||
|
lib*/
|
||||||
|
waf/
|
||||||
|
captcha/
|
||||||
|
smtp/
|
||||||
|
static/
|
||||||
|
tests/
|
||||||
|
3d/
|
||||||
|
BotOps/
|
||||||
|
pymsg/tests/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# AI Review 服务配置
|
||||||
|
# 复制此文件为 ai_review/.env 并填入你的 API Key
|
||||||
|
|
||||||
|
# DeepSeek/OpenAI API 密钥
|
||||||
|
OPENAI_API_KEY=your_api_key_here
|
||||||
|
|
||||||
|
# API Base URL(默认使用 DeepSeek)
|
||||||
|
OPENAI_BASE_URL=https://api.deepseek.com/v1
|
||||||
|
|
||||||
|
# 使用的模型名称
|
||||||
|
MODEL_NAME=deepseek-chat
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
gateway/shufeng.ini
|
gateway/shufeng.ini
|
||||||
core/Nidus.service.shufeng
|
core/Nidus.service.shufeng
|
||||||
ai_review/.env
|
ai_review/.env
|
||||||
|
confs/__pycache__/
|
||||||
|
.workbuddy/
|
||||||
|
libhttp/test_server
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13452
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+1026
File diff suppressed because it is too large.
Load diff
+2476
File diff suppressed because it is too large.
Load diff
BIN
Binary file not shown.
+2698
File diff suppressed because it is too large.
Load diff
BIN
Binary file not shown.
+2809
File diff suppressed because it is too large.
Load diff
+2834
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,9 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# nuitka 编译产物
|
||||||
|
botops.bin
|
||||||
|
app.build/
|
||||||
|
app.dist/
|
||||||
|
app.onefile-build/
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# BotOps — aibox 智能体 TUI 前端
|
||||||
|
|
||||||
|
复刻并增强 [aibox/client.py](../aibox/client.py),基于 [Textual](https://textual.textualize.io/) 的现代化终端 UI,连接 aibox 微服务(WebSocket,默认 `ws://localhost:8765`)。
|
||||||
|
|
||||||
|
## 相比 client.py 的增强
|
||||||
|
|
||||||
|
| 能力 | client.py | BotOps |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| AI 回复渲染 | 纯文本流式 / 一次性静态渲染 | **Markdown 增量流式渲染**(无闪烁) |
|
||||||
|
| 终端管理 | 手搓光标/提示符 | Textual 部件框架(多面板布局) |
|
||||||
|
| 任务事件 | 混在对话流里 | **独立侧栏**(task_process / task_status / task_error) |
|
||||||
|
| 自动重连 | ✅(已移植) | ✅ 指数退避 2s→30s,`/disconnect` 抑制 |
|
||||||
|
| 快捷键 | Ctrl+C 中断 | Ctrl+C 中断 / Ctrl+Q 退出 / Ctrl+L 清屏 |
|
||||||
|
|
||||||
|
## 安装与运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd BotOps
|
||||||
|
|
||||||
|
# 一键初始化(创建 venv + 安装依赖)
|
||||||
|
bash _set.sh
|
||||||
|
|
||||||
|
# 启动
|
||||||
|
bash _run.sh # 默认 ws://localhost:8765
|
||||||
|
bash _run.sh ws://your-host:8765 # 指定 aibox 地址
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nuitka 编译为独立二进制
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash _inst.sh # 产出 ./botops.bin(单文件,无需 Python 环境)
|
||||||
|
./botops.bin [ws://host:port]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命令
|
||||||
|
|
||||||
|
- 本地命令(不发给服务器):`/connect` `/disconnect` `/interrupt` `/clear` `/status` `/help` `/quit`
|
||||||
|
- 其余 `/` 命令透传给 aibox 服务器(与 client.py 行为一致)
|
||||||
|
|
||||||
|
## 结构
|
||||||
|
|
||||||
|
```
|
||||||
|
BotOps/
|
||||||
|
├── app.py # Textual App:布局 + 消息路由 + 快捷键
|
||||||
|
├── ws_client.py # WebSocket 协议层(自动重连)
|
||||||
|
├── commands.py # 斜杠命令注册表
|
||||||
|
├── _run.sh # 启动脚本
|
||||||
|
├── _set.sh # 初始化脚本(venv + 依赖安装)
|
||||||
|
├── _inst.sh # Nuitka 编译脚本(产出 botops.bin)
|
||||||
|
├── requirements.txt # Python 依赖
|
||||||
|
└── widgets/
|
||||||
|
├── __init__.py
|
||||||
|
├── chat_view.py # 聊天区:AI 回复 Markdown 增量流式渲染
|
||||||
|
└── status_bar.py # 底部状态栏(连接状态)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 协议说明
|
||||||
|
|
||||||
|
与 aibox 服务端协议完全兼容(详见 aibox/client.py 注释):
|
||||||
|
- 发送:`{"type":"message","content":...}` / `{"type":"command","command":"/..."}` / `{"type":"interrupt",...}`
|
||||||
|
- 接收:
|
||||||
|
- `chunk`(**增量片段**):服务端 `chunk_buffer` 发送后即清零,前端直接 `append_ai` 拼接,为内容权威来源
|
||||||
|
- `reasoning_start`(**思考开始**):创建灰色斜体思考区域,实时展示 AI 思考过程
|
||||||
|
- `reasoning`(**思考流**):思考内容增量片段,追加到思考区域(超长自动折叠显示最后 3000 字符)
|
||||||
|
- `reasoning_end`(**思考结束**):思考结束,切换到正式回答
|
||||||
|
- **顺序保证**:不预创建空 AI widget,按消息到达顺序自然挂载(`reasoning_start`→`reasoning`→`chunk`),思考区域必然在 AI 正式回答**之前**
|
||||||
|
- `complete`(**收尾信号**):chunk 流式已完整渲染,complete 仅用于收尾(重置状态)
|
||||||
|
- `line`(过程行):🔧 工具调用提示、工具输出,路由到任务侧栏;助手前缀/思考提示在聊天区已由对应区域承载,不重复展示
|
||||||
|
- `info`(系统信息):过滤"Conversation started"/"对话开始"等隐式通知,避免污染聊天流
|
||||||
|
- `dialogue_ended` / `dialogue_reset` / `task_added` / `task_process` / `task_status` / `task_error` / `interrupt_ack` / `error` / `command_result` / `connection_ack` / `notification` / `pong`
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.3
|
||||||
|
- ✅ **适配服务端中断机制(Ctrl+C可靠停止)**:
|
||||||
|
- 收到 `interrupt_ack` 时立即收尾前端状态:调用 `abort_reasoning()` 将思考区域更新为"🛑 思考已中断"标签,调用 `finish_ai()` 结束AI回复状态
|
||||||
|
- 新增 `ChatView.abort_reasoning()` 方法:中断时更新思考区域标签(保留已展示的思考内容)
|
||||||
|
- `append_reasoning()` 增加 `_ai_finalized` 检查,中断后丢弃残留的思考片段防止重新创建思考区域
|
||||||
|
- 中断后服务端发来的残留 `chunk`/`reasoning`/`complete` 消息均被安全忽略,不会污染UI
|
||||||
|
|
||||||
|
### v1.2
|
||||||
|
- ✅ **彻底修复思考区域顺序问题**:移除预创建空 AI widget 的逻辑,改为第一次收到 `chunk` 时才创建 widget。按消息到达顺序自然挂载(`reasoning_start`→`reasoning`→`chunk`),从根本上保证思考区域始终在正式回答之前
|
||||||
|
- ✅ 简化 `complete` 处理逻辑:仅作为收尾重置状态信号,不再进行补建逻辑
|
||||||
|
|
||||||
|
### v1.1
|
||||||
|
- ✅ 支持 DeepSeek 思考模式展示:新增 `reasoning_start`/`reasoning`/`reasoning_end` 消息处理,思考内容以灰色斜体区域展示
|
||||||
|
- ✅ 超长思考内容自动折叠(显示最后 3000 字符)
|
||||||
|
- ✅ 过滤"Conversation started"/"对话开始"等隐式系统通知,避免在回答末尾显示无用信息
|
||||||
|
|
||||||
|
### v1.0
|
||||||
|
- 初始版本:Markdown 增量流式渲染、任务独立侧栏、自动重连、快捷键支持
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 编译 BotOps 为独立单文件(不运行):产出 ./botops.bin
|
||||||
|
# 用法: ./_inst.sh
|
||||||
|
cd "$(dirname "$0")" || exit
|
||||||
|
source venv/bin/activate
|
||||||
|
python3 -m nuitka --standalone --onefile \
|
||||||
|
--assume-yes-for-downloads \
|
||||||
|
--include-package=websockets \
|
||||||
|
--include-package=textual \
|
||||||
|
--include-package-data=textual \
|
||||||
|
--output-filename=botops.bin \
|
||||||
|
app.py
|
||||||
|
echo "编译完成: ./botops.bin(运行: ./botops.bin [ws://host:port])"
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")" || exit
|
||||||
|
source venv/bin/activate
|
||||||
|
exec python3 app.py $@
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")" || exit
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
+281
@@ -0,0 +1,281 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""BotOps — aibox 智能体的现代化 TUI 前端(Textual)。
|
||||||
|
|
||||||
|
复刻并增强 aibox/client.py:
|
||||||
|
- AI 回复流式 Markdown 渲染(Markdown 部件增量追加,无闪烁)
|
||||||
|
- 断线指数退避自动重连(/disconnect 抑制)
|
||||||
|
- 任务事件独立侧栏(task_process / task_status / task_error)
|
||||||
|
- Ctrl+C 中断生成、Ctrl+Q 退出、Ctrl+L 清屏
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python app.py [ws://host:port] # 默认 ws://localhost:8765
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.binding import Binding
|
||||||
|
from textual.containers import Horizontal
|
||||||
|
from textual.widgets import Footer, Header, Input, RichLog
|
||||||
|
|
||||||
|
from commands import CommandRegistry
|
||||||
|
from widgets.chat_view import ChatView
|
||||||
|
from widgets.status_bar import StatusBar
|
||||||
|
from ws_client import BotWSClient
|
||||||
|
|
||||||
|
DEFAULT_URI = "ws://localhost:8765"
|
||||||
|
|
||||||
|
|
||||||
|
class BotOpsApp(App):
|
||||||
|
TITLE = "BotOps"
|
||||||
|
SUB_TITLE = "aibox TUI"
|
||||||
|
|
||||||
|
CSS = """
|
||||||
|
#chat {
|
||||||
|
width: 3fr;
|
||||||
|
border: solid $primary;
|
||||||
|
}
|
||||||
|
#tasks {
|
||||||
|
width: 1fr;
|
||||||
|
border: solid $secondary;
|
||||||
|
}
|
||||||
|
.msg-user {
|
||||||
|
color: cyan;
|
||||||
|
padding: 0 1;
|
||||||
|
margin-top: 1;
|
||||||
|
}
|
||||||
|
.msg-ai {
|
||||||
|
padding: 0 1;
|
||||||
|
}
|
||||||
|
.msg-system {
|
||||||
|
color: $text-muted;
|
||||||
|
padding: 0 1;
|
||||||
|
}
|
||||||
|
.msg-error {
|
||||||
|
color: red;
|
||||||
|
padding: 0 1;
|
||||||
|
}
|
||||||
|
.msg-task {
|
||||||
|
color: yellow;
|
||||||
|
padding: 0 1;
|
||||||
|
}
|
||||||
|
.msg-reasoning {
|
||||||
|
color: $text-muted;
|
||||||
|
background: $surface;
|
||||||
|
padding: 0 1;
|
||||||
|
margin: 0 1;
|
||||||
|
text-style: italic;
|
||||||
|
}
|
||||||
|
#status {
|
||||||
|
height: 1;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
BINDINGS = [
|
||||||
|
Binding("ctrl+c", "interrupt", "中断", show=True),
|
||||||
|
Binding("ctrl+q", "quit", "退出", show=True),
|
||||||
|
Binding("ctrl+l", "clear", "清屏", show=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, uri: str = DEFAULT_URI):
|
||||||
|
super().__init__()
|
||||||
|
self.ws = BotWSClient(uri)
|
||||||
|
self.ws.on_message = self._on_ws_message
|
||||||
|
self.ws.on_state = self._on_ws_state
|
||||||
|
self.registry = CommandRegistry(self)
|
||||||
|
|
||||||
|
# ── 布局 ─────────────────────────────────────────────────────────────
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield Header()
|
||||||
|
with Horizontal():
|
||||||
|
yield ChatView(id="chat")
|
||||||
|
yield RichLog(id="tasks", max_lines=1000)
|
||||||
|
yield Input(placeholder="输入消息,/help 查看命令,Enter 发送", id="input")
|
||||||
|
yield StatusBar(id="status")
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
async def on_mount(self):
|
||||||
|
self.query_one("#input", Input).focus()
|
||||||
|
self._refresh_status("")
|
||||||
|
self.run_worker(self.connect_to(self.ws.uri))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chat(self) -> ChatView:
|
||||||
|
return self.query_one("#chat", ChatView)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tasks(self) -> RichLog:
|
||||||
|
return self.query_one("#tasks", RichLog)
|
||||||
|
|
||||||
|
# ── 连接管理 ──────────────────────────────────────────────────────────
|
||||||
|
async def connect_to(self, uri: str):
|
||||||
|
if self.ws.connected:
|
||||||
|
await self.ws.disconnect()
|
||||||
|
self.ws.uri = uri
|
||||||
|
await self.notify_system(f"正在连接 {uri} …")
|
||||||
|
await self.ws.connect()
|
||||||
|
|
||||||
|
async def disconnect(self):
|
||||||
|
await self.ws.disconnect()
|
||||||
|
|
||||||
|
async def send_interrupt(self):
|
||||||
|
if await self.ws.send_interrupt():
|
||||||
|
await self.notify_system("🛑 已发送中断信号")
|
||||||
|
else:
|
||||||
|
await self.notify_system("未连接到服务器", level="error")
|
||||||
|
|
||||||
|
# ── UI 操作 ───────────────────────────────────────────────────────────
|
||||||
|
async def notify_system(self, text: str, level: str = "info"):
|
||||||
|
css = {"info": "msg-system", "error": "msg-error", "task": "msg-task"}.get(level, "msg-system")
|
||||||
|
await self.chat.add_system(text, css)
|
||||||
|
|
||||||
|
async def clear_chat(self):
|
||||||
|
await self.chat.reset()
|
||||||
|
|
||||||
|
def _refresh_status(self, extra: str):
|
||||||
|
self.query_one("#status", StatusBar).set_state(
|
||||||
|
self.ws.connected, self.ws.uri if self.ws.connected else "", extra)
|
||||||
|
|
||||||
|
# ── 输入处理 ──────────────────────────────────────────────────────────
|
||||||
|
async def on_input_submitted(self, event: Input.Submitted):
|
||||||
|
text = event.value.strip()
|
||||||
|
event.input.value = ""
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
if text.startswith("/") and await self.registry.execute(text):
|
||||||
|
return
|
||||||
|
if not self.ws.connected:
|
||||||
|
await self.notify_system("未连接到服务器,先 /connect", level="error")
|
||||||
|
return
|
||||||
|
await self.chat.add_user(text)
|
||||||
|
await self.chat.start_ai() # 标记AI回复开始状态(不预创建空widget)
|
||||||
|
if not await self.ws.send_message(text):
|
||||||
|
await self.chat.finish_ai("") # 发送失败:重置AI状态
|
||||||
|
await self.notify_system("❌ 发送失败:连接已断开,请 /connect 重连", level="error")
|
||||||
|
|
||||||
|
# ── 快捷键 ────────────────────────────────────────────────────────────
|
||||||
|
async def action_interrupt(self):
|
||||||
|
await self.send_interrupt()
|
||||||
|
|
||||||
|
async def action_clear(self):
|
||||||
|
await self.clear_chat()
|
||||||
|
|
||||||
|
def action_quit(self):
|
||||||
|
self.exit()
|
||||||
|
|
||||||
|
# ── aibox 协议消息路由(对应 client.py 的 _handle_message)─────────────
|
||||||
|
async def _on_ws_message(self, data: dict):
|
||||||
|
t = data.get("type", "")
|
||||||
|
content = data.get("content", "")
|
||||||
|
|
||||||
|
if t == "chunk":
|
||||||
|
# 服务端 chunk 为增量片段(发送后即清零),直接拼接
|
||||||
|
await self.chat.append_ai(content)
|
||||||
|
elif t == "reasoning_start":
|
||||||
|
# 开始深度思考,创建思考区域
|
||||||
|
await self.chat.start_reasoning(content or "🤔 深度思考中...")
|
||||||
|
elif t == "reasoning":
|
||||||
|
# 思考内容增量
|
||||||
|
stripped = content
|
||||||
|
if stripped:
|
||||||
|
await self.chat.append_reasoning(stripped)
|
||||||
|
elif t == "reasoning_end":
|
||||||
|
# 思考结束
|
||||||
|
await self.chat.end_reasoning()
|
||||||
|
elif t == "line":
|
||||||
|
# 过程行(服务端 output_callback 原样透传):工具调用提示 / 工具输出 / 前缀
|
||||||
|
stripped = content.strip()
|
||||||
|
if not stripped:
|
||||||
|
pass
|
||||||
|
elif stripped.startswith((
|
||||||
|
"💬 助手", "💬 Assistant", # 助手前缀(聊天区已有对应区域)
|
||||||
|
"🤔 思考中", "🤔 Thinking", # 思考提示(聊天区已有思考区域)
|
||||||
|
"🤔 深度思考中",
|
||||||
|
"🤔 分析工具结果", "🤔 Analyzing", # 工具分析提示(后续有具体工具输出,此为冗余)
|
||||||
|
"🔧 需要调用工具", "🔧 Need to call",# 工具调用前置提示(后续有"调用工具: xxx"具体提示)
|
||||||
|
)):
|
||||||
|
pass # 冗余/有替代展示的提示行,过滤避免重复
|
||||||
|
else:
|
||||||
|
# 🔧 调用工具: xxx / 工具输出 / 用户中断提示等 → 任务侧栏
|
||||||
|
self.tasks.write(stripped)
|
||||||
|
elif t == "ai_break":
|
||||||
|
# 工具调用前分段:结束当前AI文本段(保留已显示内容),后续chunk创建新widget
|
||||||
|
await self.chat.break_ai()
|
||||||
|
elif t == "complete":
|
||||||
|
# complete 携带完整全文,用于对齐已显示内容
|
||||||
|
await self.chat.finish_ai(content)
|
||||||
|
elif t == "dialogue_ended":
|
||||||
|
await self.chat.finish_ai(self.chat.ai_text)
|
||||||
|
if content:
|
||||||
|
await self.notify_system(f"✅ {content}")
|
||||||
|
elif t == "task_added":
|
||||||
|
await self.notify_system(f"➕ {content}")
|
||||||
|
elif t == "dialogue_reset":
|
||||||
|
if content:
|
||||||
|
await self.notify_system(f"🔄 {content}")
|
||||||
|
elif t == "pong":
|
||||||
|
pass
|
||||||
|
elif t == "task_process":
|
||||||
|
sub = data.get("msg_type", "")
|
||||||
|
title = data.get("task_title", "未知任务")
|
||||||
|
if sub == "chunk":
|
||||||
|
tail = str(data.get("content", ""))[-200:]
|
||||||
|
if tail:
|
||||||
|
self.tasks.write(f"⚡ {title}: {tail}")
|
||||||
|
elif sub == "complete":
|
||||||
|
self.tasks.write(f"✅ 任务完成: {title}")
|
||||||
|
elif t == "task_status":
|
||||||
|
icon = {"executing": "⚙️", "complete": "✅", "failed": "❌",
|
||||||
|
"added": "➕", "batch_start": "🚀", "batch_complete": "🏁"
|
||||||
|
}.get(data.get("status", ""), "📋")
|
||||||
|
self.tasks.write(
|
||||||
|
f"{icon} [{data.get('task_id', '?')}] {data.get('title', '')}: "
|
||||||
|
f"{data.get('message', content)}")
|
||||||
|
elif t == "task_error":
|
||||||
|
self.tasks.write(
|
||||||
|
f"❌ [{data.get('task_id', '?')}] {data.get('task_title', '?')}: "
|
||||||
|
f"{data.get('error', '未知错误')}")
|
||||||
|
elif t == "interrupt_ack":
|
||||||
|
# 服务端确认中断:立即收尾前端状态(更新思考区域为已中断、结束AI回复)
|
||||||
|
await self.chat.abort_reasoning()
|
||||||
|
await self.chat.finish_ai("")
|
||||||
|
await self.notify_system(f"🛑 {content}")
|
||||||
|
elif t == "error":
|
||||||
|
await self.notify_system(f"❌ {content}", level="error")
|
||||||
|
elif t == "info":
|
||||||
|
# 过滤掉对话开始这类隐式通知(用户发消息自然知道对话开始了)
|
||||||
|
skip_keywords = ("Conversation started", "对话开始", "🎭")
|
||||||
|
if not any(k in content for k in skip_keywords):
|
||||||
|
await self.notify_system(f"ℹ️ {content}")
|
||||||
|
elif t == "command_result":
|
||||||
|
await self.notify_system(content)
|
||||||
|
elif t == "connection_ack":
|
||||||
|
await self.notify_system(f"✅ {content}")
|
||||||
|
elif t == "notification":
|
||||||
|
await self.notify_system(f"🔔 {content}")
|
||||||
|
elif content:
|
||||||
|
# 未知消息类型不追加到AI聊天区(避免非AI内容污染回复),仅记入侧栏日志
|
||||||
|
self.tasks.write(f"[未知消息 type={t}] {str(content)[:200]}")
|
||||||
|
|
||||||
|
# ── 连接状态回调 ──────────────────────────────────────────────────────
|
||||||
|
async def _on_ws_state(self, state: str, detail: str):
|
||||||
|
if state == "connected":
|
||||||
|
self._refresh_status("")
|
||||||
|
await self.notify_system(f"✅ 已连接到 {self.ws.uri}")
|
||||||
|
elif state == "disconnected":
|
||||||
|
self._refresh_status("")
|
||||||
|
await self.notify_system(
|
||||||
|
f"❌ {detail}(中断期间的服务端输出已丢失,如回复不完整请重发)")
|
||||||
|
elif state == "reconnecting":
|
||||||
|
self._refresh_status(detail)
|
||||||
|
elif state == "error":
|
||||||
|
self._refresh_status("")
|
||||||
|
await self.notify_system(detail, level="error")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
uri = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_URI
|
||||||
|
BotOpsApp(uri).run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""斜杠命令注册表:/connect /disconnect /interrupt /clear /status /help /quit。
|
||||||
|
|
||||||
|
本地命令在此拦截处理;未注册的 / 命令透传给 aibox 服务器(与 client.py 行为一致)。
|
||||||
|
"""
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app import BotOpsApp
|
||||||
|
|
||||||
|
|
||||||
|
class CommandRegistry:
|
||||||
|
def __init__(self, app: "BotOpsApp"):
|
||||||
|
self.app = app
|
||||||
|
self._commands = {
|
||||||
|
"/connect": ("连接服务器:/connect ws://host:port", self._connect),
|
||||||
|
"/disconnect": ("断开连接(不自动重连)", self._disconnect),
|
||||||
|
"/interrupt": ("中断当前生成(或按 Ctrl+C)", self._interrupt),
|
||||||
|
"/clear": ("清空聊天区", self._clear),
|
||||||
|
"/status": ("显示连接状态", self._status),
|
||||||
|
"/help": ("显示帮助", self._help),
|
||||||
|
"/quit": ("退出 BotOps", self._quit),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, line: str) -> bool:
|
||||||
|
"""是本地命令则执行并返回 True;否则返回 False(调用方应透传服务器)。"""
|
||||||
|
cmd = line.split()[0].lower()
|
||||||
|
entry = self._commands.get(cmd)
|
||||||
|
if entry is None:
|
||||||
|
return False
|
||||||
|
await entry[1](line)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def _connect(self, line: str):
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) < 2:
|
||||||
|
await self.app.notify_system("用法: /connect ws://host:port", level="error")
|
||||||
|
return
|
||||||
|
await self.app.connect_to(parts[1])
|
||||||
|
|
||||||
|
async def _disconnect(self, _line: str):
|
||||||
|
await self.app.disconnect()
|
||||||
|
|
||||||
|
async def _interrupt(self, _line: str):
|
||||||
|
await self.app.send_interrupt()
|
||||||
|
|
||||||
|
async def _clear(self, _line: str):
|
||||||
|
await self.app.clear_chat()
|
||||||
|
|
||||||
|
async def _status(self, _line: str):
|
||||||
|
if self.app.ws.connected:
|
||||||
|
await self.app.notify_system(f"✅ 已连接: {self.app.ws.uri}")
|
||||||
|
else:
|
||||||
|
await self.app.notify_system("○ 未连接")
|
||||||
|
|
||||||
|
async def _help(self, _line: str):
|
||||||
|
lines = ["可用命令:"] + [f" {k:<14} {v[0]}" for k, v in self._commands.items()]
|
||||||
|
lines.append(" 其余 / 命令将透传给 aibox 服务器")
|
||||||
|
await self.app.notify_system("\n".join(lines))
|
||||||
|
|
||||||
|
async def _quit(self, _line: str):
|
||||||
|
await self.app.ws.disconnect()
|
||||||
|
self.app.exit()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
textual>=0.60.0
|
||||||
|
websockets>=11.0
|
||||||
|
nuitka
|
||||||
Whitespace-only changes.
@@ -0,0 +1,205 @@
|
|||||||
|
"""聊天视图:用户/AI/系统消息 + AI 回复流式 Markdown 渲染。
|
||||||
|
|
||||||
|
服务端协议(aibox/aibox.py output_callback 原样透传 msg_type):
|
||||||
|
- chunk 增量片段(正式回答内容),直接 append 到 AI 消息
|
||||||
|
- reasoning_start 思考开始信号,在当前末尾创建新的思考区域
|
||||||
|
- reasoning 思考内容增量片段,追加到当前活跃的思考区域
|
||||||
|
- reasoning_end 思考结束信号:更新标签后清除引用(widget保留显示),
|
||||||
|
后续 reasoning_start 会创建新的思考区域
|
||||||
|
- complete 收尾信号:chunk 流式是内容权威来源,complete 仅用于收尾;
|
||||||
|
仅当流式完全丢帧(未建 widget 且无内容)时,用 full_text 补建
|
||||||
|
|
||||||
|
顺序保证:不预创建空AI widget,按消息到达顺序自然挂载。
|
||||||
|
多轮工具调用场景:每轮思考(reasoning_start→reasoning_end)创建独立的思考区域,
|
||||||
|
按时间顺序分布在对话流中各自的AI回答之前,而非全部堆在顶部。
|
||||||
|
"""
|
||||||
|
from textual.containers import VerticalScroll
|
||||||
|
from textual.widgets import Markdown, Static
|
||||||
|
|
||||||
|
|
||||||
|
class ChatView(VerticalScroll):
|
||||||
|
"""可滚动聊天区。AI 回复用单个 Markdown 部件增量 append,实现流式渲染。
|
||||||
|
|
||||||
|
不预创建空widget:避免重排复杂性,消息按接收顺序自然挂载,
|
||||||
|
天然保证思考过程在正式回答之前。
|
||||||
|
滚动策略:每条新消息挂载后调用 anchor() 锚定底部——之后内容增长
|
||||||
|
(流式 append)容器会自动保持吸底;用户上翻时 Textual 自动脱锚,
|
||||||
|
下一条新消息再重新锚定。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._ai_widget: Markdown | None = None
|
||||||
|
self._reasoning_widget: Static | None = None
|
||||||
|
self._ai_text = ""
|
||||||
|
self._reasoning_text = ""
|
||||||
|
self._ai_finalized = True # 初始无活跃 AI 消息
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ai_text(self) -> str:
|
||||||
|
return self._ai_text
|
||||||
|
|
||||||
|
def _anchor_bottom(self):
|
||||||
|
"""锚定吸底(需在布局刷新后执行,否则滚到的是旧位置)。"""
|
||||||
|
self.call_after_refresh(self.anchor)
|
||||||
|
|
||||||
|
async def add_user(self, text: str):
|
||||||
|
await self.mount(Static(f"👤 你: {text}", classes="msg-user", markup=False))
|
||||||
|
self._anchor_bottom()
|
||||||
|
|
||||||
|
async def add_system(self, text: str, css_class: str = "msg-system"):
|
||||||
|
await self.mount(Static(text, classes=css_class, markup=False))
|
||||||
|
self._anchor_bottom()
|
||||||
|
|
||||||
|
async def start_ai(self):
|
||||||
|
"""开始AI回复(标记状态,不预创建空widget)。
|
||||||
|
|
||||||
|
为保持顺序正确,空widget不在此处创建。第一个chunk到来时才创建AI widget,
|
||||||
|
此时思考widget(如果有)已经先挂载,顺序自然正确。
|
||||||
|
"""
|
||||||
|
if not self._ai_finalized:
|
||||||
|
return
|
||||||
|
self._ai_text = ""
|
||||||
|
self._ai_finalized = False
|
||||||
|
|
||||||
|
async def start_reasoning(self, hint: str = "🤔 思考中..."):
|
||||||
|
"""开始展示思考过程区域。直接挂载在当前末尾位置。"""
|
||||||
|
if self._reasoning_widget is not None:
|
||||||
|
return
|
||||||
|
self._reasoning_text = ""
|
||||||
|
self._reasoning_widget = Static(hint + "\n", classes="msg-reasoning", markup=False)
|
||||||
|
await self.mount(self._reasoning_widget)
|
||||||
|
self._anchor_bottom()
|
||||||
|
|
||||||
|
async def append_reasoning(self, delta: str):
|
||||||
|
"""追加思考内容增量。"""
|
||||||
|
if not delta:
|
||||||
|
return
|
||||||
|
if self._ai_finalized:
|
||||||
|
# 已收尾(中断/完成),丢弃残留的思考片段
|
||||||
|
return
|
||||||
|
if self._reasoning_widget is None:
|
||||||
|
await self.start_reasoning()
|
||||||
|
self._reasoning_text += delta
|
||||||
|
# 限制思考内容显示长度(最多显示最后 3000 字符,避免思考过长占满屏幕)
|
||||||
|
display = self._reasoning_text
|
||||||
|
if len(display) > 3000:
|
||||||
|
display = "...(前略)\n" + display[-3000:]
|
||||||
|
self._reasoning_widget.update(f"💭 思考中...\n{display}")
|
||||||
|
self.call_after_refresh(self.scroll_end, animate=False)
|
||||||
|
|
||||||
|
async def end_reasoning(self):
|
||||||
|
"""结束当前思考区域,更新标签后清除引用(widget保留在界面上显示)。
|
||||||
|
|
||||||
|
清除引用后,后续 reasoning_start 会在当前末尾位置创建新的思考区域,
|
||||||
|
实现多轮工具调用时思考区域按时间顺序分布,而非全部堆在顶部。
|
||||||
|
"""
|
||||||
|
if self._reasoning_widget is not None and self._reasoning_text:
|
||||||
|
display = self._reasoning_text
|
||||||
|
if len(display) > 3000:
|
||||||
|
display = "...(前略)\n" + display[-3000:]
|
||||||
|
self._reasoning_widget.update(f"💭 思考过程({len(self._reasoning_text)}字符)\n{display}\n{'─' * 30}")
|
||||||
|
# 清除引用但保留widget在DOM中显示——下一轮reasoning_start会创建新widget
|
||||||
|
self._reasoning_widget = None
|
||||||
|
self._reasoning_text = ""
|
||||||
|
|
||||||
|
async def abort_reasoning(self):
|
||||||
|
"""中断当前思考区域,更新提示文字后清除引用。"""
|
||||||
|
if self._reasoning_widget is not None:
|
||||||
|
if self._reasoning_text:
|
||||||
|
display = self._reasoning_text
|
||||||
|
if len(display) > 3000:
|
||||||
|
display = "...(前略)\n" + display[-3000:]
|
||||||
|
self._reasoning_widget.update(f"🛑 思考已中断({len(self._reasoning_text)}字符)\n{display}")
|
||||||
|
else:
|
||||||
|
self._reasoning_widget.update("🛑 思考已中断")
|
||||||
|
# 清除引用,已显示的widget保留
|
||||||
|
self._reasoning_widget = None
|
||||||
|
self._reasoning_text = ""
|
||||||
|
|
||||||
|
async def _ensure_ai_widget(self):
|
||||||
|
"""确保AI widget存在(第一次chunk到来时创建)。"""
|
||||||
|
if self._ai_widget is None:
|
||||||
|
self._ai_widget = Markdown("", classes="msg-ai")
|
||||||
|
await self.mount(self._ai_widget)
|
||||||
|
self._anchor_bottom()
|
||||||
|
|
||||||
|
async def break_ai(self):
|
||||||
|
"""分段当前AI回答:清除引用但保留widget在DOM中显示。
|
||||||
|
|
||||||
|
用于工具调用场景:工具调用前的AI文字(如"好的让我查一下")保留显示,
|
||||||
|
后续chunk会创建新的AI widget,保证思考区域按时间顺序插入在两段AI文字之间。
|
||||||
|
与finish_ai不同:不设置_ai_finalized,对话继续中。
|
||||||
|
"""
|
||||||
|
if self._ai_widget is not None:
|
||||||
|
self._ai_widget = None
|
||||||
|
self._ai_text = ""
|
||||||
|
|
||||||
|
async def append_ai(self, delta: str):
|
||||||
|
"""追加增量片段(服务端 chunk 为增量语义,累积文本后 update 整个 Markdown)。
|
||||||
|
|
||||||
|
使用 update() 而非 Markdown.append():Textual 的 Markdown.append() 在
|
||||||
|
chunk 边界切断 Markdown 语法(如列表项、代码块)时可能导致重复解析渲染。
|
||||||
|
累积完整文本后 update() 确保每次都从完整文本重新解析,避免重复。
|
||||||
|
"""
|
||||||
|
if not delta:
|
||||||
|
return
|
||||||
|
if self._ai_finalized:
|
||||||
|
# 收尾后收到的延迟 chunk,丢弃以避免在上一条回复后追加脏数据
|
||||||
|
return
|
||||||
|
# 第一次收到chunk时创建widget(此时思考widget如果存在已经在前面了)
|
||||||
|
await self._ensure_ai_widget()
|
||||||
|
self._ai_text += delta
|
||||||
|
# 累积文本后整体update,而非增量append,避免Markdown增量解析导致的重复渲染
|
||||||
|
self._ai_widget.update(self._ai_text)
|
||||||
|
self.call_after_refresh(self.scroll_end, animate=False)
|
||||||
|
|
||||||
|
async def finish_ai(self, full_text: str = ""):
|
||||||
|
"""收尾一条 AI 消息。
|
||||||
|
|
||||||
|
chunk 流式追加是内容权威来源,complete 只做收尾:
|
||||||
|
- 已收尾则直接返回(防止 dialogue_ended + complete 双发导致重复挂载)
|
||||||
|
- widget 存在且有流式内容:保持已渲染内容不变,仅清空引用
|
||||||
|
- widget 存在但无内容(异常场景)且有 full_text:用 full_text 填充
|
||||||
|
- widget 不存在(未收到任何 chunk)且有 full_text:补建
|
||||||
|
- widget 不存在且无内容:正常无内容场景
|
||||||
|
"""
|
||||||
|
if self._ai_finalized:
|
||||||
|
return
|
||||||
|
w = self._ai_widget
|
||||||
|
self._ai_widget = None
|
||||||
|
|
||||||
|
if w is not None:
|
||||||
|
if self._ai_text:
|
||||||
|
# 有流式内容,chunk 已完整渲染,直接保留,不做替换
|
||||||
|
pass
|
||||||
|
elif full_text:
|
||||||
|
# widget存在但没收到任何chunk(异常):用full_text填充
|
||||||
|
w.update(full_text)
|
||||||
|
self._ai_text = full_text
|
||||||
|
else:
|
||||||
|
# widget存在但没有任何输出:移除空框(正常不会走到这里,因为不预创建)
|
||||||
|
await w.remove()
|
||||||
|
else:
|
||||||
|
# widget不存在(start_ai后没收到任何chunk),有full_text则补建
|
||||||
|
if full_text:
|
||||||
|
self._ai_text = full_text
|
||||||
|
self._ai_widget = Markdown(full_text, classes="msg-ai")
|
||||||
|
await self.mount(self._ai_widget)
|
||||||
|
self._ai_widget = None
|
||||||
|
self._anchor_bottom()
|
||||||
|
|
||||||
|
self._ai_text = ""
|
||||||
|
self._reasoning_widget = None
|
||||||
|
self._reasoning_text = ""
|
||||||
|
self._ai_finalized = True
|
||||||
|
self.call_after_refresh(self.scroll_end, animate=False)
|
||||||
|
|
||||||
|
async def reset(self):
|
||||||
|
"""清空聊天区(/clear)。"""
|
||||||
|
self._ai_widget = None
|
||||||
|
self._reasoning_widget = None
|
||||||
|
self._ai_text = ""
|
||||||
|
self._reasoning_text = ""
|
||||||
|
self._ai_finalized = True
|
||||||
|
await self.remove_children()
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"""底部状态栏:连接状态 + 服务器地址 + 附加信息。"""
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
|
class StatusBar(Static):
|
||||||
|
def set_state(self, connected: bool, uri: str = "", extra: str = ""):
|
||||||
|
dot = "●" if connected else "○"
|
||||||
|
color = "green" if connected else "red"
|
||||||
|
text = f"[{color}]{dot}[/] {'已连接' if connected else '未连接'}"
|
||||||
|
if uri:
|
||||||
|
text += f" {uri}"
|
||||||
|
if extra:
|
||||||
|
text += f" | {extra}"
|
||||||
|
self.update(text)
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
"""BotOps WebSocket 协议层(提炼自 aibox/client.py,增加状态回调与自动重连)。
|
||||||
|
|
||||||
|
协议:JSON 消息。
|
||||||
|
发送:{"type": "message", "content": ...}
|
||||||
|
{"type": "command", "command": "/..."}
|
||||||
|
{"type": "interrupt", "content": ...}
|
||||||
|
接收:chunk / line / reasoning / complete / dialogue_ended / dialogue_reset /
|
||||||
|
task_process / task_status / task_error / task_added / interrupt_ack /
|
||||||
|
error / info / command_result / connection_ack / notification / pong
|
||||||
|
|
||||||
|
说明:服务端 chunk 为**增量片段**(output_callback 发送 chunk_buffer 后即清零),
|
||||||
|
complete 为完整全文(可用于对齐);line 为过程行(工具提示/输出/前缀)。
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from typing import Awaitable, Callable, Optional
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
MessageHandler = Callable[[dict], Awaitable[None]]
|
||||||
|
StateHandler = Callable[[str, str], Awaitable[None]] # (state, detail)
|
||||||
|
|
||||||
|
|
||||||
|
class BotWSClient:
|
||||||
|
"""aibox 协议 WebSocket 客户端:断线指数退避自动重连,手动断开则抑制。"""
|
||||||
|
|
||||||
|
def __init__(self, uri: str):
|
||||||
|
self.uri = uri
|
||||||
|
self.ws = None
|
||||||
|
self.connected = False
|
||||||
|
self._manual_disconnect = False
|
||||||
|
self._listen_task: Optional[asyncio.Task] = None
|
||||||
|
self._reconnect_task: Optional[asyncio.Task] = None
|
||||||
|
self.on_message: Optional[MessageHandler] = None
|
||||||
|
self.on_state: Optional[StateHandler] = None
|
||||||
|
|
||||||
|
async def _emit_state(self, state: str, detail: str = ""):
|
||||||
|
if self.on_state:
|
||||||
|
await self.on_state(state, detail)
|
||||||
|
|
||||||
|
async def connect(self) -> bool:
|
||||||
|
"""连接并开始监听;成功返回 True。"""
|
||||||
|
await self._close_ws()
|
||||||
|
try:
|
||||||
|
self.ws = await websockets.connect(self.uri)
|
||||||
|
except Exception as e:
|
||||||
|
await self._emit_state("error", f"连接失败: {e}")
|
||||||
|
return False
|
||||||
|
self.connected = True
|
||||||
|
self._manual_disconnect = False
|
||||||
|
self._listen_task = asyncio.create_task(self._listen())
|
||||||
|
await self._emit_state("connected", self.uri)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def disconnect(self):
|
||||||
|
"""手动断开(抑制自动重连)。"""
|
||||||
|
self._manual_disconnect = True
|
||||||
|
if self._reconnect_task and not self._reconnect_task.done():
|
||||||
|
self._reconnect_task.cancel()
|
||||||
|
self.connected = False
|
||||||
|
await self._close_ws()
|
||||||
|
if self._listen_task and not self._listen_task.done():
|
||||||
|
self._listen_task.cancel()
|
||||||
|
await self._emit_state("disconnected", "已手动断开")
|
||||||
|
|
||||||
|
async def _close_ws(self):
|
||||||
|
if self.ws is not None:
|
||||||
|
try:
|
||||||
|
await self.ws.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.ws = None
|
||||||
|
|
||||||
|
async def send_message(self, text: str) -> bool:
|
||||||
|
"""发送用户消息(/ 开头按 command 发送,与 aibox 协议一致)。"""
|
||||||
|
if not self.connected or not self.ws:
|
||||||
|
return False
|
||||||
|
data = ({"type": "command", "command": text} if text.startswith("/")
|
||||||
|
else {"type": "message", "content": text})
|
||||||
|
try:
|
||||||
|
await self.ws.send(json.dumps(data, ensure_ascii=False))
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
await self._emit_state("error", f"发送失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def send_interrupt(self) -> bool:
|
||||||
|
"""发送中断信号(对应 client.py 的 Ctrl+C 行为)。"""
|
||||||
|
if not self.connected or not self.ws:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
await self.ws.send(json.dumps(
|
||||||
|
{"type": "interrupt", "content": "用户按下了 Ctrl+C 中断当前操作"},
|
||||||
|
ensure_ascii=False))
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _listen(self):
|
||||||
|
"""接收循环:断线时在 finally 中触发自动重连。"""
|
||||||
|
try:
|
||||||
|
async for raw in self.ws:
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
data = {"type": "chunk", "content": raw}
|
||||||
|
if self.on_message:
|
||||||
|
await self.on_message(data)
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
pass
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
await self._emit_state("error", f"接收错误: {e}")
|
||||||
|
finally:
|
||||||
|
was_connected = self.connected
|
||||||
|
self.connected = False
|
||||||
|
if was_connected:
|
||||||
|
await self._emit_state("disconnected", "连接已断开")
|
||||||
|
self._schedule_reconnect()
|
||||||
|
|
||||||
|
def _schedule_reconnect(self):
|
||||||
|
if self._manual_disconnect:
|
||||||
|
return
|
||||||
|
if self._reconnect_task and not self._reconnect_task.done():
|
||||||
|
return
|
||||||
|
self._reconnect_task = asyncio.create_task(self._reconnect_loop())
|
||||||
|
|
||||||
|
async def _reconnect_loop(self):
|
||||||
|
"""指数退避 2s→30s 封顶,直到成功或手动断开。"""
|
||||||
|
delay = 2
|
||||||
|
while not self._manual_disconnect and not self.connected:
|
||||||
|
await self._emit_state("reconnecting", f"{delay}s 后自动重连 {self.uri} …")
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
if self._manual_disconnect:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.ws = await websockets.connect(self.uri)
|
||||||
|
except Exception:
|
||||||
|
delay = min(delay * 2, 30)
|
||||||
|
continue
|
||||||
|
self.connected = True
|
||||||
|
self._listen_task = asyncio.create_task(self._listen())
|
||||||
|
await self._emit_state("connected", self.uri)
|
||||||
|
return
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# micro_servers
|
# micro_servers
|
||||||
|
|
||||||
一套 DIY 的轻量级微服务架构(UniServe Framework),由 C++、Python 与 Go 混合实现。
|
一套 DIY 的轻量级微服务架构(UniServe Framework),由 C++、Python、Go 与仓颉(Cangjie) 混合实现。
|
||||||
`core` 作为进程托管服务负责拉起并守护各业务子进程,`waf` 作为前置 WAF 网关终止 TLS 并做规则匹配,`gateway` 作为应用层网关统一鉴权与反向代理,`db` 提供通用数据库访问与集中式日志收集,`ai_review` 提供 AI 内容审核,`static` 与 `dnfs` 作为业务服务对外提供能力。
|
`core` 作为进程托管服务负责拉起并守护各业务子进程,`waf` 作为前置 WAF 网关终止 TLS 并做规则匹配,`gateway` 作为应用层网关统一鉴权与反向代理,`db` 提供通用数据库访问与集中式日志收集,`confs` 提供统一配置分发,`ai_review` 提供 AI 内容审核,`static` 与 `dnfs` 作为业务服务对外提供能力。此外包含独立的机器人硬件控制模块 `bot4cj` 和嵌入式脚本引擎 `libtry`。
|
||||||
|
|
||||||
## 架构总览
|
## 架构总览
|
||||||
|
|
||||||
@@ -24,14 +24,15 @@
|
|||||||
└──────────────────►│ ├─────────────►│─────────────────┘ │
|
└──────────────────►│ ├─────────────►│─────────────────┘ │
|
||||||
├────────────►│ └─────── HTTP /review ──────────────┘
|
├────────────►│ └─────── HTTP /review ──────────────┘
|
||||||
└────────────►│
|
└────────────►│
|
||||||
日志链路: waf / gateway / static / dnfs / ai_review / captcha / smtp / aibox ──(log_client, pymsg)──► db:8005 (Logs 表)
|
日志链路: waf / gateway / static / dnfs / ai_review / captcha / smtp / aibox / confs ──(log_client, pymsg)──► db:8005 (Logs 表)
|
||||||
|
配置链路: static 等业务服务 ──(pymsg get)──► confs:8010(配置统一存放于 confs/configs/,mtime 变化自动重载)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 请求链路
|
### 请求链路
|
||||||
|
|
||||||
1. 客户端所有请求先打到 `waf:443`(HTTPS),WAF 终止 TLS、运行 coraza 规则匹配,命中则直接返回 403。
|
1. 客户端所有请求先打到 `waf:443`(HTTPS),WAF 终止 TLS、运行 coraza 规则匹配,命中则直接返回 403。
|
||||||
2. 未命中规则的请求由 WAF 以明文 HTTP 转发到 `gateway:8443`,并设置 `X-Forwarded-For` / `X-Forwarded-Proto` 头。
|
2. 未命中规则的请求由 WAF 以明文 HTTP 转发到 `gateway:8443`,并设置 `X-Forwarded-For` / `X-Forwarded-Proto` 头。
|
||||||
3. `gateway` 对 `/login`、`/register` 做账号鉴权(PBKDF2 密码哈希 + RSA 签名 Token + 可选 CAPTCHA + 速率限制),用户数据通过 pymsg/TCP 调用 `db:8005` 存取。
|
3. `gateway` 对 `/login`、`/register` 做账号鉴权(PBKDF2 密码哈希 + RSA 签名 Token + 可选 CAPTCHA + 速率限制;注册需邮箱验证码,经 pymsg/TCP 调 `smtp:8008` 发送与校验),用户数据通过 pymsg/TCP 调用 `db:8005` 存取。
|
||||||
4. 其余请求携带 Token,经 `gateway` 校验后反向代理到 `Proxy.opened` 中放开的内部服务(`static:9200` / `dnfs:8006`)。
|
4. 其余请求携带 Token,经 `gateway` 校验后反向代理到 `Proxy.opened` 中放开的内部服务(`static:9200` / `dnfs:8006`)。
|
||||||
5. `db:8005` 与 `ai_review:8007` 在 `Proxy.closed` 中,仅允许内部服务间调用,不对外暴露。
|
5. `db:8005` 与 `ai_review:8007` 在 `Proxy.closed` 中,仅允许内部服务间调用,不对外暴露。
|
||||||
6. `dnfs` 处理节点增删改查时,通过 pymsg/TCP 调用 `db` 服务完成数据持久化,新增节点前调用 `ai_review` 进行内容审核。
|
6. `dnfs` 处理节点增删改查时,通过 pymsg/TCP 调用 `db` 服务完成数据持久化,新增节点前调用 `ai_review` 进行内容审核。
|
||||||
@@ -48,9 +49,11 @@
|
|||||||
| [dnfs](dnfs/) | Python(Flask) | 8006 | 树状网络文件系统(Dendrachy Network File-System),基于父子 pid 的节点树增删改查,集成 AI 内容审核 |
|
| [dnfs](dnfs/) | Python(Flask) | 8006 | 树状网络文件系统(Dendrachy Network File-System),基于父子 pid 的节点树增删改查,集成 AI 内容审核 |
|
||||||
| [db](db/) | Python(pymsg) | 8005 | 通用数据库服务,基于 pymsg 协议(TCP 长度前缀帧 + JSON)的 SQL-over-TCP,SQLAlchemy Core 参数化查询防注入;内置集中式日志收集(Logs 表 + 自动清理) |
|
| [db](db/) | Python(pymsg) | 8005 | 通用数据库服务,基于 pymsg 协议(TCP 长度前缀帧 + JSON)的 SQL-over-TCP,SQLAlchemy Core 参数化查询防注入;内置集中式日志收集(Logs 表 + 自动清理) |
|
||||||
| [ai_review](ai_review/) | Python(Flask) | 8007 | AI 内容审核服务,DeepSeek LLM + ChromaDB RAG,支持人工反馈纠偏 |
|
| [ai_review](ai_review/) | Python(Flask) | 8007 | AI 内容审核服务,DeepSeek LLM + ChromaDB RAG,支持人工反馈纠偏 |
|
||||||
| [captcha](captcha/) | Python(stdlib+PIL) | 8009 | 验证码服务,生成图形验证码(5 分钟过期、一次性),供 gateway 登录/注册可选启用 CAPTCHA 校验 |
|
| [captcha](captcha/) | Python(pymsg) | 8009 | 验证码服务(pymsg/TCP),生成图形验证码(5 分钟过期、一次性),经 gateway 的 `GET /captcha/image` 代理给浏览器 |
|
||||||
| [smtp](smtp/) | Python(Flask) | 8008 | 邮件服务,发送/校验邮箱验证码(/send_email、/verify_email),注册流程的邮件验证码后端,SMTP 账号配置在 .env |
|
| [smtp](smtp/) | Python(pymsg) | 8008 | 邮件服务,发送/校验邮箱验证码(pymsg 路由 send_email/verify_email/health),注册流程的邮件验证码后端,SMTP 账号配置在 .env |
|
||||||
|
| [confs](confs/) | Python(pymsg) | 8010 | 统一配置服务,集中存放并分发各服务配置文件(pymsg 路由 get/list/reload/health),内存缓存 + mtime 自动重载,路径遍历防护 |
|
||||||
| [aibox](aibox/) | Python | 8765 | 独立 AI 智能体系统(DeepSeek + WebSocket),自带技能系统/记忆/任务调度,由 core 拉起但与主干微服务无调用关系 |
|
| [aibox](aibox/) | Python | 8765 | 独立 AI 智能体系统(DeepSeek + WebSocket),自带技能系统/记忆/任务调度,由 core 拉起但与主干微服务无调用关系 |
|
||||||
|
| [bot4cj](bot4cj/) | 仓颉(Cangjie) | — | 机器人硬件脚本解析器,解析 .mt 矩阵脚本通过 I2C 驱动 PCA9685 舵机 / MPU6050 陀螺仪,面向树莓派 Dashun 蜘蛛机器人平台(独立硬件项目) |
|
||||||
|
|
||||||
### core(核心模块 · 进程托管服务)
|
### core(核心模块 · 进程托管服务)
|
||||||
|
|
||||||
@@ -68,17 +71,18 @@
|
|||||||
- 极客操作:`kill -HUP <pid>` 热加载规则;`waf -list` 列规则;`waf -test <file>` 校验语法;`waf -check "<input>"` 测试单条输入。
|
- 极客操作:`kill -HUP <pid>` 热加载规则;`waf -list` 列规则;`waf -test <file>` 校验语法;`waf -check "<input>"` 测试单条输入。
|
||||||
- 配置见 [waf/config.yaml](waf/config.yaml),详见 [waf/README.md](waf/README.md)。
|
- 配置见 [waf/config.yaml](waf/config.yaml),详见 [waf/README.md](waf/README.md)。
|
||||||
|
|
||||||
### gateway(应用层网关 · v0.3.0-security)
|
### gateway(应用层网关 · v0.5.3)
|
||||||
|
|
||||||
- 内部 HTTP 服务,默认监听 127.0.0.1:8443,不再承载 TLS(TLS 已由 WAF 终止)。
|
- 内部 HTTP 服务,默认监听 127.0.0.1:8443,不再承载 TLS(TLS 已由 WAF 终止)。
|
||||||
- 通过 `Server.tls = on/off` 配置切换 HTTPS/HTTP 模式([example.ini](gateway/example.ini)),独立部署时仍可启用 TLS。
|
- 通过 `Server.tls = on/off` 配置切换 HTTPS/HTTP 模式([example.ini](gateway/example.ini)),独立部署时仍可启用 TLS。
|
||||||
- **密码安全**:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节随机盐),密码强度校验(8+ 字符、4 类中至少 3 类),登录时自动迁移旧 SHA256 哈希,常数时间比较防 timing attack。
|
- **密码安全**:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节随机盐),密码强度校验(8+ 字符、4 类中至少 3 类),常数时间比较防 timing attack。
|
||||||
- **用户存储**:用户数据通过 pymsg/TCP 调用 `db:8005`(`DbC::call_msg`)存取,不再直连本地 SQLite。
|
- **用户存储**:用户数据通过 pymsg/TCP 调用 `db:8005`(`DbC::call_msg`)存取,不再直连本地 SQLite;登录成功即更新 `Users.last_login`(`"$now"` 哨兵)。
|
||||||
- **Token 机制**:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1 小时过期,验签通过才放行;RSA 密钥对启动时内存生成,重启后旧 Token 全部失效。
|
- **Token 机制**:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1 小时过期,验签通过才放行;RSA 密钥对启动时内存生成,重启后旧 Token 全部失效。
|
||||||
- **CAPTCHA**:`/captcha` 接口生成验证码(5 分钟过期、一次性使用),登录与注册均可选启用。
|
- **Cookie 认证**:登录成功同时签发 `HttpOnly + Secure + SameSite=Lax` 的 `q4_token` Cookie;代理时无 `Authorization` 头则回退读 Cookie 验签,并向下游注入 `Authorization: Bearer`(下游零改动);`POST /logout` 清除 Cookie。公开目标(`Proxy.default`)不做凭证校验,避免旧 Cookie 影响公开页面。
|
||||||
|
- **CAPTCHA**:`/captcha` 接口生成验证码(5 分钟过期、一次性使用),登录与注册均可选启用;`GET /captcha/image` 为 captcha 服务(pymsg/TCP :8009)的图形验证码代理路由。
|
||||||
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟,按 `X-Forwarded-For`(WAF 注入)客户端 IP 计数。
|
- **速率限制**:登录 5 次/5 分钟、注册 3 次/10 分钟,按 `X-Forwarded-For`(WAF 注入)客户端 IP 计数。
|
||||||
- **反向代理**:`Target-Url` 必须在 `Proxy.opened` 白名单中(`closed` 仅记录用途,实际隔离靠"不在 opened 中即拒绝"),缺省走 `Proxy.default`(不需 Token,服务前端静态资源)。
|
- **反向代理**:`Target-Url` 必须在 `Proxy.opened` 白名单中(`closed` 仅记录用途,实际隔离靠"不在 opened 中即拒绝"),缺省走 `Proxy.default`(不需 Token,服务前端静态资源)。
|
||||||
- **自动建表**:启动时若 `Users` 表不存在则自动创建,首位注册者即为管理员。
|
- **用户表**:`Users` 表由 [db/create_sqlite3.sql](db/create_sqlite3.sql) 建立(gateway 不再自动建表),无默认管理员账号,首位注册者即为管理员。
|
||||||
- 详见 [gateway/README.md](gateway/README.md)。
|
- 详见 [gateway/README.md](gateway/README.md)。
|
||||||
|
|
||||||
### static(前端托管服务)
|
### static(前端托管服务)
|
||||||
@@ -90,7 +94,7 @@
|
|||||||
- **账号旁路**:`/api/check_user`、`/api/reset_password` 等接口直接读写 `gateway/sqlite3.db` 的 Users 表;`/yhdet_callback`、`/api/yhdet_*` 对接 yhdet.top OAuth 登录。
|
- **账号旁路**:`/api/check_user`、`/api/reset_password` 等接口直接读写 `gateway/sqlite3.db` 的 Users 表;`/yhdet_callback`、`/api/yhdet_*` 对接 yhdet.top OAuth 登录。
|
||||||
- 统一品牌"树状菜单",支持深色模式(`css/theme.css`)。
|
- 统一品牌"树状菜单",支持深色模式(`css/theme.css`)。
|
||||||
- 可使用 Nuitka 编译为独立二进制(`fsys.bin`)。
|
- 可使用 Nuitka 编译为独立二进制(`fsys.bin`)。
|
||||||
- 配置见 [static/config.ini](static/config.ini)(注:CORS 白名单硬编码于 `fsys.py`,config.ini 的 cors 段未生效)。
|
- 配置由 `confs` 统一配置服务分发(`confs/configs/static.ini`,启动时经 pymsg 拉取,`CONFS_HOST`/`CONFS_PORT` 环境变量可覆盖);本地 [static/config.ini](static/config.ini) 作为 confs 不可用时的 fail-safe 回退。
|
||||||
|
|
||||||
### dnfs(树状菜单服务)
|
### dnfs(树状菜单服务)
|
||||||
|
|
||||||
@@ -106,12 +110,12 @@
|
|||||||
- 基于 SQLAlchemy Core 表反射,参数化查询,零 SQL 注入风险。
|
- 基于 SQLAlchemy Core 表反射,参数化查询,零 SQL 注入风险。
|
||||||
- 仅监听 127.0.0.1,自动提交/回滚,不暴露 DDL 操作。
|
- 仅监听 127.0.0.1,自动提交/回滚,不暴露 DDL 操作。
|
||||||
- 默认使用 SQLite(`database.db`),可通过 `DATABASE_URL` 环境变量切换至 PostgreSQL / MySQL。
|
- 默认使用 SQLite(`database.db`),可通过 `DATABASE_URL` 环境变量切换至 PostgreSQL / MySQL。
|
||||||
|
- **日期/时间列适配**:values 与 where 中的 DATETIME/DATE/TIME 列接受 ISO 8601 字符串(带时区偏移自动转 UTC)或哨兵值 `"$now"`(服务端当前 UTC);响应中的日期时间一律序列化为 ISO 8601(T 分隔)。
|
||||||
- **内置日志子模块**:
|
- **内置日志子模块**:
|
||||||
- 启动时自动建 `Logs` 表(service / level / message / trace_id / extra + 4 个索引)。
|
- 启动时自动建 `Logs` 表(service / level / message / trace_id / extra + 4 个索引)。
|
||||||
- 外部服务通过 `log_client.py` 以 pymsg 调 execute 路由逐条 INSERT 写入日志。
|
- 外部服务通过 `log_client.py` 以 pymsg 调 execute 路由逐条 INSERT 写入日志。
|
||||||
- `_log_internal()` — db 进程内部直接写 Logs 表,无网络往返,防递归设计。
|
- `_log_internal()` — db 进程内部直接写 Logs 表,无网络往返,防递归设计。
|
||||||
- 后台守护线程每小时自动清理:保留 30 天 / 上限 100 万条。
|
- 后台守护线程每小时自动清理:保留 30 天 / 上限 100 万条。
|
||||||
- 注:`db/log_module.py`(Flask `/log/*` Blueprint)为旧版 HTTP 接口的遗留文件,当前 dbv2 未挂载。
|
|
||||||
- **统一日志客户端 SDK**(各服务目录下的 `log_client.py`,如 [dnfs/log_client.py](dnfs/log_client.py)):各服务复制即用,纯标准库实现,异步非阻塞(内存队列 + 后台线程,50 条/2 秒批量 flush),fail-safe 设计(db 不可用时静默丢弃,不影响业务)。
|
- **统一日志客户端 SDK**(各服务目录下的 `log_client.py`,如 [dnfs/log_client.py](dnfs/log_client.py)):各服务复制即用,纯标准库实现,异步非阻塞(内存队列 + 后台线程,50 条/2 秒批量 flush),fail-safe 设计(db 不可用时静默丢弃,不影响业务)。
|
||||||
|
|
||||||
### ai_review(AI 内容审核服务)
|
### ai_review(AI 内容审核服务)
|
||||||
@@ -125,17 +129,25 @@
|
|||||||
|
|
||||||
### captcha(验证码服务)
|
### captcha(验证码服务)
|
||||||
|
|
||||||
- 生成图形验证码,Token 一次性使用、5 分钟过期;纯标准库实现(http.server + PIL,无 Flask)。
|
- 生成图形验证码,Token 一次性使用、5 分钟过期;基于 [pymsg](pymsg/) 协议(TCP 长度前缀帧 + JSON),仅依赖 PIL。
|
||||||
- 接口:`GET /captcha/image`(生成 PNG,Token 在 `X-Captcha-Token` 响应头)、`POST /captcha/verify`(校验,body 含 token/answer)、`GET /health`。
|
- pymsg 路由:`image`(生成,返回 token + base64 PNG)、`verify`(校验,一次性消费)、`health`。
|
||||||
- 答案错误返回 `200 {valid:false}`(Token 照常消费),仅参数 / Token 错误返回 `400`;比较采用 XOR 防时序攻击。
|
- 答案比较采用 XOR 防时序攻击;错误答案与无效 Token 分别返回 `{valid:false}` 与 `{valid:false, error:...}`。
|
||||||
|
- **gateway 代理路由**:`GET /captcha/image` 经 pymsg 调 captcha 后解码 PNG,以 `image/png` + `X-Captcha-Token` 响应头返回浏览器(`Cache-Control: no-store`)。
|
||||||
- 由 `gateway` 在 `/login`、`/register` 中可选启用 CAPTCHA 校验(字段 `captcha_token` / `captcha_answer`)。
|
- 由 `gateway` 在 `/login`、`/register` 中可选启用 CAPTCHA 校验(字段 `captcha_token` / `captcha_answer`)。
|
||||||
- 内存存储,不支持多实例水平扩展(需换 Redis);详见 [captcha/README.md](captcha/README.md)。
|
- 内存存储,不支持多实例水平扩展(需换 Redis);详见 [captcha/README.md](captcha/README.md)。
|
||||||
|
|
||||||
### smtp(邮件服务)
|
### smtp(邮件服务)
|
||||||
|
|
||||||
- 注册流程的邮箱验证码后端,基于 Flask + waitress,监听 127.0.0.1:8008。
|
- 注册流程的邮箱验证码后端,基于 pymsg(MsgServer,TCP 长度前缀帧 + JSON),监听 127.0.0.1:8008。
|
||||||
- 接口:`POST /send_email`(生成 6 位验证码并经 SMTP_SSL 发送至收件人,60 秒有效)、`POST /verify_email`(校验)。
|
- pymsg 路由:`send_email`(生成 6 位验证码并经 SMTP_SSL 发送,10 分钟有效,发送失败自动作废;同一邮箱 60 秒发送冷却,冷却期拒绝并返回 `retry_after`,不覆盖已发出的有效验证码,发送失败不计入冷却)、`verify_email`(校验,通过即一次性消费防重放,连续失败 5 次自动作废并返回 `invalidated`,兼容旧字段 `input_captcha`)、`health`。
|
||||||
- 验证码存于进程内存,SMTP 账号通过 `.env` 配置(`SMTP_SERVER` / `SMTP_PORT` / `SMTP_USER` / `__PASSWORD`)。
|
- 验证码存于进程内存(锁保护 + 惰性过期清理),SMTP 账号通过 `.env` 配置(`SMTP_SERVER` / `SMTP_PORT` / `SMTP_USER` / `__PASSWORD`,`__PASSWORD` 兼容别名 `SMTP_PASSWORD`)。
|
||||||
|
|
||||||
|
### confs(统一配置服务)
|
||||||
|
|
||||||
|
- 集中存放各微服务的配置文件(`confs/configs/`),基于 pymsg(RpcServer,TCP 长度前缀帧 + JSON)分发,监听 127.0.0.1:8010。
|
||||||
|
- pymsg 路由:`get`(取配置文件全文;带 `key` 时按键提取,ini 用 `Section.option`、json 用点号路径)、`list`(列出全部配置及元信息)、`reload`(重载指定配置,缺省清空全部缓存)、`health`。
|
||||||
|
- 内存缓存 + 文件 mtime 变化自动重载,改配置无需重启 confs;配置名校验(basename + realpath 前缀 + 扩展名白名单)防路径遍历。
|
||||||
|
- 消费方约定:服务启动时用 `RpcClient(8010).call("get", {"name": "<服务名>.ini"})` 拉取配置,confs 不可用时回退本地配置文件(fail-safe)。目前 `static` 已接入。
|
||||||
|
|
||||||
### aibox(独立 AI 智能体系统)
|
### aibox(独立 AI 智能体系统)
|
||||||
|
|
||||||
@@ -143,14 +155,23 @@
|
|||||||
- 自带技能系统(`skills/`)、任务调度器、记忆管理、前端页面与语音客户端(`voice_client.py`)。
|
- 自带技能系统(`skills/`)、任务调度器、记忆管理、前端页面与语音客户端(`voice_client.py`)。
|
||||||
- 默认模型 `deepseek-v4-flash`,配置在 `config.json`;详见 [aibox/README.md](aibox/README.md)。
|
- 默认模型 `deepseek-v4-flash`,配置在 `config.json`;详见 [aibox/README.md](aibox/README.md)。
|
||||||
|
|
||||||
|
### bot4cj(机器人硬件脚本解析器)
|
||||||
|
|
||||||
|
- 基于仓颉(Cangjie)编程语言和 Linux I2C 接口的机器人动作脚本解析器,面向树莓派 Dashun 蜘蛛机器人平台(10 舵机 + 2 云台)。
|
||||||
|
- 解析 `.mt` 矩阵脚本(行列式表格语法,支持 `@Servo` / `@Action` 等块),编译为舵机角度序列执行。
|
||||||
|
- 硬件层通过 I2C 驱动 PCA9685(PWM 舵机控制,50Hz,脉宽 0.5–2.5ms)、MPU6050(陀螺仪/加速度计,10ms 采样 + 卡尔曼滤波)、Speaker(正弦波发声)。
|
||||||
|
- 附带数学库(矩阵运算、NN/CNN、Adam 优化器)、CUDA GPU 加速 kernel 和 Lua 硬件模块(ADS1115 / ADS1256 / SPI)。
|
||||||
|
- 与主干微服务完全独立,需部署在树莓派上独立运行。
|
||||||
|
|
||||||
## 共享基础库
|
## 共享基础库
|
||||||
|
|
||||||
| 库 | 说明 |
|
| 库 | 说明 |
|
||||||
|----|------|
|
|----|------|
|
||||||
| [libth](libth/) | 线程库:`Thread`、`Cv_wait`(条件变量等待)、`Fsm`、`ITask` 任务接口 |
|
| [libth](libth/) | 线程库:`Thread`、`Cv_wait`(条件变量等待)、`Fsm`、`ITask` 任务接口 |
|
||||||
| [libhttp](libhttp/) | HTTP/WebSocket 库:`HttpServer`、`HttpClient`、`WebSocketServer/Client`(基于 Boost.Beast) |
|
| [libhttp](libhttp/) | HTTP/WebSocket 库:`HttpServer`(含 WebSocket 服务/代理)、`HttpClient`(含 WebSocket 客户端,基于 Boost.Beast) |
|
||||||
| [libcrypt](libcrypt/) | 加密库:`AES_CBC`、`MyRSA`、`SHA256`、`Base64`、`Crypto_Basic` |
|
| [libcrypt](libcrypt/) | 加密库:`AES_CBC`、`MyRSA`、`SHA256`、`Base64`、`Crypto_Basic` |
|
||||||
| [libsqlite3](libsqlite3/) | SQLite3 C++ 封装(Header-only) |
|
| [libsqlite3](libsqlite3/) | SQLite3 C++ 封装(Header-only) |
|
||||||
|
| [libtry](libtry/) | 嵌入式脚本解释器(~741 行 C),手写递归下降词法/语法/执行引擎,支持变量/数组/函数/控制流,宽字符(中文标识符),用于嵌入式操作界面 shell |
|
||||||
|
|
||||||
### Python 共享组件
|
### Python 共享组件
|
||||||
|
|
||||||
@@ -171,9 +192,25 @@
|
|||||||
| 8006 | dnfs | 127.0.0.1 | 树状菜单(经 gateway 代理) |
|
| 8006 | dnfs | 127.0.0.1 | 树状菜单(经 gateway 代理) |
|
||||||
| 8005 | db | 127.0.0.1 | 数据库服务(pymsg/TCP,仅内部调用) |
|
| 8005 | db | 127.0.0.1 | 数据库服务(pymsg/TCP,仅内部调用) |
|
||||||
| 8007 | ai_review | 127.0.0.1 | AI 审核(仅内部调用) |
|
| 8007 | ai_review | 127.0.0.1 | AI 审核(仅内部调用) |
|
||||||
| 8009 | captcha | 127.0.0.1 | 验证码服务(stdlib http.server,独立组件) |
|
| 8009 | captcha | 127.0.0.1 | 验证码服务(pymsg/TCP,经 gateway 代理) |
|
||||||
| 8008 | smtp | 127.0.0.1 | 邮件服务(仅内部调用,注册验证码邮件后端) |
|
| 8008 | smtp | 127.0.0.1 | 邮件服务(仅内部调用,注册验证码邮件后端) |
|
||||||
|
| 8010 | confs | 127.0.0.1 | 统一配置服务(pymsg/TCP,仅内部调用) |
|
||||||
| 8765 | aibox | 127.0.0.1 | AI 智能体(WebSocket,独立产品) |
|
| 8765 | aibox | 127.0.0.1 | AI 智能体(WebSocket,独立产品) |
|
||||||
|
| — | bot4cj | — (树莓派 I2C) | 机器人硬件脚本解析器(独立硬件项目,不联网) |
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
[tests/](tests/) 包含一套黑盒测试套件,基于子进程调用与 HTTP 请求验证各微服务功能正确性:
|
||||||
|
|
||||||
|
| 测试文件 | 被测模块 | 说明 |
|
||||||
|
|----------|----------|------|
|
||||||
|
| `test_db_*.py` | db | SQL-over-TCP 的 select/insert/update/delete、日期哨兵、边界条件 |
|
||||||
|
| `test_gateway_*.py` | gateway | 用户注册/登录/Token 校验、速率限制、CAPTCHA 集成 |
|
||||||
|
| `test_waf_*.py` | waf | coraza 规则匹配与拦截、白名单放行 |
|
||||||
|
| `test_dnfs_*.py` | dnfs | 节点树 CRUD、AI 审核集成 |
|
||||||
|
|
||||||
|
- 默认使用 `config.py` 中的生产地址(127.0.0.1),每个测试函数前后自动备份/恢复 db 数据,保证不污染线上数据。
|
||||||
|
- 测试依赖 `pymsg`、`Flask`、`flask-cors`、`requests`、`Pillow`,详见 [tests/requirements-test.txt](tests/requirements-test.txt)。
|
||||||
|
|
||||||
## 依赖
|
## 依赖
|
||||||
|
|
||||||
@@ -191,7 +228,7 @@ bash _setup.deb.sh # Debian/Ubuntu
|
|||||||
# 或
|
# 或
|
||||||
bash _setup.rhel.sh # RHEL/CentOS/Rocky/Fedora
|
bash _setup.rhel.sh # RHEL/CentOS/Rocky/Fedora
|
||||||
|
|
||||||
# 2. 依次安装各模块(libth -> libhttp -> libcrypt -> libsqlite3 -> gateway -> waf -> static -> dnfs -> db -> core)
|
# 2. 依次安装各模块(libth -> libhttp -> libcrypt -> libsqlite3 -> gateway -> waf -> static -> dnfs -> db -> smtp -> captcha -> confs -> core)
|
||||||
bash _inst.sh
|
bash _inst.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -204,7 +241,7 @@ bash _run.sh # 运行
|
|||||||
|
|
||||||
部署路径约定为 `/opt/micro_servers/`,`core` 的 `init.txt` 即从该路径拉起其余服务。
|
部署路径约定为 `/opt/micro_servers/`,`core` 的 `init.txt` 即从该路径拉起其余服务。
|
||||||
|
|
||||||
> **注意**:`ai_review` 未包含在 `_inst.sh` 中,需单独进入 `ai_review/` 目录执行 `_set.sh` 安装,并配置 `.env` 文件(DeepSeek API 密钥等)。
|
> **注意**:`ai_review` 未包含在 `_inst.sh` 中,需进入 `ai_review/` 目录执行 `_set.sh` 安装,并额外配置 `.env` 文件(DeepSeek API 密钥等)。
|
||||||
|
|
||||||
> **注意**:waf 首次编译需联网下载 Go 依赖,[waf/_set.sh](waf/_set.sh) 会自动调用 [waf/_goproxy.sh](waf/_goproxy.sh) 设置 GOPROXY 国内镜像。如需手动设置:`go env -w GOPROXY=https://goproxy.cn,direct`。
|
> **注意**:waf 首次编译需联网下载 Go 依赖,[waf/_set.sh](waf/_set.sh) 会自动调用 [waf/_goproxy.sh](waf/_goproxy.sh) 设置 GOPROXY 国内镜像。如需手动设置:`go env -w GOPROXY=https://goproxy.cn,direct`。
|
||||||
|
|
||||||
@@ -213,8 +250,9 @@ bash _run.sh # 运行
|
|||||||
由 `core` 守护的子进程通过 `init.txt` 自启动(顺序很重要,waf 必须先于 gateway):
|
由 `core` 守护的子进程通过 `init.txt` 自启动(顺序很重要,waf 必须先于 gateway):
|
||||||
|
|
||||||
```
|
```
|
||||||
bash /opt/micro_servers/ai_review/_run.sh
|
|
||||||
bash /opt/micro_servers/db/_run.sh
|
bash /opt/micro_servers/db/_run.sh
|
||||||
|
bash /opt/micro_servers/confs/_run.sh
|
||||||
|
bash /opt/micro_servers/ai_review/_run.sh
|
||||||
bash /opt/micro_servers/dnfs/_run.sh
|
bash /opt/micro_servers/dnfs/_run.sh
|
||||||
bash /opt/micro_servers/smtp/_run.sh
|
bash /opt/micro_servers/smtp/_run.sh
|
||||||
bash /opt/micro_servers/static/_run.sh
|
bash /opt/micro_servers/static/_run.sh
|
||||||
@@ -252,7 +290,7 @@ gateway 改为内部 HTTP 模式([example.ini](gateway/example.ini) 中 `tls =
|
|||||||
- **服务按业务能力拆分**:WAF 网关、应用网关、前端托管、节点树、数据库、内容审核各自独立、独立端口、独立数据存储。
|
- **服务按业务能力拆分**:WAF 网关、应用网关、前端托管、节点树、数据库、内容审核各自独立、独立端口、独立数据存储。
|
||||||
- **API 网关模式**:`waf` 作为边缘网关统一入口,`gateway` 承担鉴权与反向代理,内部服务通过 `closed` 列表隔离。
|
- **API 网关模式**:`waf` 作为边缘网关统一入口,`gateway` 承担鉴权与反向代理,内部服务通过 `closed` 列表隔离。
|
||||||
- **进程编排/守护**:`core` 作为服务管理器拉起并守护子进程,类似简易版 supervisor。
|
- **进程编排/守护**:`core` 作为服务管理器拉起并守护子进程,类似简易版 supervisor。
|
||||||
- **多语言异构**:C++(core / gateway)、Python(static / dnfs / db / ai_review)、Go(waf)混合,对外 HTTP、内部数据访问走 pymsg/TCP。
|
- **多语言异构**:C++(core / gateway)、Python(static / dnfs / db / ai_review)、Go(waf)、仓颉/Cangjie(bot4cj)混合,对外 HTTP、内部数据访问走 pymsg/TCP。
|
||||||
- **数据访问层解耦**:`db` 服务将数据库操作收敛为统一的 pymsg/TCP 接口,业务服务无需直连数据库。
|
- **数据访问层解耦**:`db` 服务将数据库操作收敛为统一的 pymsg/TCP 接口,业务服务无需直连数据库。
|
||||||
- **AI 能力集成**:`ai_review` 以 RAG 增强的 LLM 审核为业务层提供内容安全能力,支持人工反馈闭环。
|
- **AI 能力集成**:`ai_review` 以 RAG 增强的 LLM 审核为业务层提供内容安全能力,支持人工反馈闭环。
|
||||||
- **集中式可观测性**:`db` 内置日志表与自动清理,各服务通过 `log_client.py` SDK 异步推送日志(pymsg),实现全链路汇聚,无需引入额外中间件。
|
- **集中式可观测性**:`db` 内置日志表与自动清理,各服务通过 `log_client.py` SDK 异步推送日志(pymsg),实现全链路汇聚,无需引入额外中间件。
|
||||||
@@ -260,6 +298,22 @@ gateway 改为内部 HTTP 模式([example.ini](gateway/example.ini) 中 `tls =
|
|||||||
|
|
||||||
与云原生微服务的差异:未引入服务注册发现、容器编排、消息总线,服务地址在 `waf/config.yaml` 与 `gateway/example.ini` 中静态配置。对于个人/中小规模的 DIY 场景,这种取舍在简洁性与可控性上是合理的。
|
与云原生微服务的差异:未引入服务注册发现、容器编排、消息总线,服务地址在 `waf/config.yaml` 与 `gateway/example.ini` 中静态配置。对于个人/中小规模的 DIY 场景,这种取舍在简洁性与可控性上是合理的。
|
||||||
|
|
||||||
|
## 文档管理约定
|
||||||
|
|
||||||
|
所有项目文档统一通过根目录 [docs/](docs/) 聚合管理,入口索引见 [docs/INDEX.md](docs/INDEX.md)。文档分类与存放规则如下:
|
||||||
|
|
||||||
|
| 文档类型 | 存放位置 | 说明 |
|
||||||
|
|----------|----------|------|
|
||||||
|
| **模块 README** | 各模块根目录(如 `waf/README.md`) | 遵循行业惯例,方便 GitHub/IDE 直接识别,不移动 |
|
||||||
|
| **安全审计报告** | `docs/security/` | 各模块的安全审计、部署审计、依赖审计、加固计划统一存放,文件命名为 `<模块名>_<文档类型>_<日期>.md` |
|
||||||
|
| **代码审查日志** | `docs/reviews/` | 代码审查、评审记录统一存放 |
|
||||||
|
| **分析报告** | `docs/reports/` | 各类分析报告(HTML/MD 等)统一存放 |
|
||||||
|
| **全局文档** | `docs/` 根目录 | 全局审计总览、跨模块文档直接放在 `docs/` 下 |
|
||||||
|
| **aibox 技能提示词** | `aibox/skills/`、`aibox/tasks/` | 属于运行时数据,保留在原目录 |
|
||||||
|
| **虚拟环境/第三方文档** | `.venv/` 等 | 不纳入文档管理,由 `.gitignore` 排除 |
|
||||||
|
|
||||||
|
新增模块文档时遵循以上规则:模块说明写本目录 `README.md`,审计/审查/报告类文档放入 `docs/` 对应子目录。
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
各模块均采用 MIT License,详见各目录下的 `LICENSE`。
|
各模块均采用 MIT License,详见各目录下的 `LICENSE`。
|
||||||
@@ -28,6 +28,7 @@ inst_module() {
|
|||||||
inst_module "libth"
|
inst_module "libth"
|
||||||
inst_module "libhttp"
|
inst_module "libhttp"
|
||||||
inst_module "libcrypt"
|
inst_module "libcrypt"
|
||||||
|
inst_module "libmsg"
|
||||||
inst_module "libsqlite3"
|
inst_module "libsqlite3"
|
||||||
inst_module "gateway"
|
inst_module "gateway"
|
||||||
inst_module "waf" # waf 在 gateway 之前编译,部署时由 core 先拉起 waf 再拉起 gateway
|
inst_module "waf" # waf 在 gateway 之前编译,部署时由 core 先拉起 waf 再拉起 gateway
|
||||||
@@ -35,4 +36,6 @@ inst_module "static"
|
|||||||
inst_module "dnfs"
|
inst_module "dnfs"
|
||||||
inst_module "db"
|
inst_module "db"
|
||||||
inst_module "smtp"
|
inst_module "smtp"
|
||||||
|
inst_module "captcha"
|
||||||
|
inst_module "confs"
|
||||||
inst_module "core" # core last
|
inst_module "core" # core last
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.env
|
||||||
|
.env/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.so
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
.envrc
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
chroma/
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
LABEL maintainer="qingsong"
|
||||||
|
LABEL service="ai_review"
|
||||||
|
LABEL description="AI内容审核服务 - DeepSeek + RAG"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 安装依赖(ai_review不依赖pymsg)
|
||||||
|
COPY requirements.txt /app/
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
|
||||||
|
# 复制应用代码
|
||||||
|
COPY . /app/
|
||||||
|
|
||||||
|
# 创建ChromaDB持久化目录
|
||||||
|
RUN mkdir -p /app/chroma
|
||||||
|
VOLUME ["/app/chroma"]
|
||||||
|
|
||||||
|
# 创建非root用户
|
||||||
|
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
EXPOSE 8007
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8007/health', timeout=5)" || exit 1
|
||||||
|
|
||||||
|
# 默认环境变量
|
||||||
|
ENV AI_REVIEW_HOST=0.0.0.0 \
|
||||||
|
AI_REVIEW_PORT=8007 \
|
||||||
|
CHROMA_PERSIST_DIR=/app/chroma
|
||||||
|
|
||||||
|
CMD ["python", "ai_review.py"]
|
||||||
@@ -14,6 +14,9 @@ from dotenv import load_dotenv
|
|||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env"))
|
load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env"))
|
||||||
@@ -27,6 +30,53 @@ CORS(app)
|
|||||||
|
|
||||||
review_agent = ReviewAgent()
|
review_agent = ReviewAgent()
|
||||||
|
|
||||||
|
# ── 内容大小限制(10KB,审核文本不应超过此值)──
|
||||||
|
MAX_CONTENT_LENGTH = 10 * 1024
|
||||||
|
|
||||||
|
# ── 简易滑动窗口限流器 ──────────────────────────────────────────────
|
||||||
|
# /review: 30次/分钟/IP(正常审核请求)
|
||||||
|
# /rag/feedback: 5次/分钟/IP(人工纠偏低频操作)
|
||||||
|
|
||||||
|
_RATE_LIMIT_WINDOW = 60 # 窗口秒数
|
||||||
|
_RATE_LIMIT_REVIEW = 30 # /review 每分钟上限
|
||||||
|
_RATE_LIMIT_FEEDBACK = 5 # /rag/feedback 每分钟上限
|
||||||
|
_RATE_LIMIT_LOCK = threading.Lock()
|
||||||
|
_RATE_LIMIT_BUCKETS: "dict[str, list[float]]" = defaultdict(list)
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup_buckets():
|
||||||
|
"""惰性清理过期时间戳,在线程锁内完成。"""
|
||||||
|
now = time.monotonic()
|
||||||
|
cutoff = now - _RATE_LIMIT_WINDOW
|
||||||
|
stale_keys = []
|
||||||
|
for key, timestamps in _RATE_LIMIT_BUCKETS.items():
|
||||||
|
timestamps[:] = [t for t in timestamps if t > cutoff]
|
||||||
|
if not timestamps:
|
||||||
|
stale_keys.append(key)
|
||||||
|
for key in stale_keys:
|
||||||
|
del _RATE_LIMIT_BUCKETS[key]
|
||||||
|
|
||||||
|
|
||||||
|
def _check_rate_limit(key: str, limit: int) -> bool:
|
||||||
|
"""检查是否超出限流,返回 True 表示放行,False 表示拒绝。"""
|
||||||
|
now = time.monotonic()
|
||||||
|
cutoff = now - _RATE_LIMIT_WINDOW
|
||||||
|
with _RATE_LIMIT_LOCK:
|
||||||
|
bucket = _RATE_LIMIT_BUCKETS[key]
|
||||||
|
bucket[:] = [t for t in bucket if t > cutoff]
|
||||||
|
if len(bucket) >= limit:
|
||||||
|
return False
|
||||||
|
bucket.append(now)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _client_ip() -> str:
|
||||||
|
"""获取客户端真实 IP(优先 X-Forwarded-For)。"""
|
||||||
|
xff = request.headers.get("X-Forwarded-For", "")
|
||||||
|
if xff:
|
||||||
|
return xff.split(",")[0].strip()
|
||||||
|
return request.remote_addr or "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
@app.route('/review', methods=['POST'])
|
@app.route('/review', methods=['POST'])
|
||||||
def review():
|
def review():
|
||||||
@@ -35,6 +85,11 @@ def review():
|
|||||||
Request: {"content": "待审核的字符串"}
|
Request: {"content": "待审核的字符串"}
|
||||||
Response: {"invalid": "true"} 或 {"invalid": "false"}
|
Response: {"invalid": "true"} 或 {"invalid": "false"}
|
||||||
"""
|
"""
|
||||||
|
# 频率限制
|
||||||
|
ip = _client_ip()
|
||||||
|
if not _check_rate_limit(f"review:{ip}", _RATE_LIMIT_REVIEW):
|
||||||
|
return jsonify({"error": "rate limit exceeded", "retry_after": _RATE_LIMIT_WINDOW}), 429
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
if not data or 'content' not in data:
|
if not data or 'content' not in data:
|
||||||
@@ -45,6 +100,10 @@ def review():
|
|||||||
if not content:
|
if not content:
|
||||||
return jsonify({"error": "content cannot be empty"}), 400
|
return jsonify({"error": "content cannot be empty"}), 400
|
||||||
|
|
||||||
|
# 内容大小限制
|
||||||
|
if len(content) > MAX_CONTENT_LENGTH:
|
||||||
|
return jsonify({"error": f"content too large, max {MAX_CONTENT_LENGTH} bytes"}), 413
|
||||||
|
|
||||||
result = review_agent.review(content)
|
result = review_agent.review(content)
|
||||||
return jsonify(result), 200
|
return jsonify(result), 200
|
||||||
|
|
||||||
@@ -61,6 +120,11 @@ def rag_feedback():
|
|||||||
Response: {"added": true, "id": "v_xxx", "md5": "xxx"}
|
Response: {"added": true, "id": "v_xxx", "md5": "xxx"}
|
||||||
{"added": false, "reason": "duplicate"}
|
{"added": false, "reason": "duplicate"}
|
||||||
"""
|
"""
|
||||||
|
# 频率限制(人工纠偏低频操作,严格限制)
|
||||||
|
ip = _client_ip()
|
||||||
|
if not _check_rate_limit(f"feedback:{ip}", _RATE_LIMIT_FEEDBACK):
|
||||||
|
return jsonify({"error": "rate limit exceeded", "retry_after": _RATE_LIMIT_WINDOW}), 429
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
if not data or 'content' not in data:
|
if not data or 'content' not in data:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def apply_security_headers(app, csp_policy=None):
|
|||||||
@app.after_request
|
@app.after_request
|
||||||
def set_security_headers(response):
|
def set_security_headers(response):
|
||||||
# Prevent clickjacking
|
# Prevent clickjacking
|
||||||
response.headers['X-Frame-Options'] = 'DENY'
|
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
||||||
# XSS protection (legacy browsers)
|
# XSS protection (legacy browsers)
|
||||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||||
# Prevent MIME type sniffing
|
# Prevent MIME type sniffing
|
||||||
@@ -30,10 +30,10 @@ def apply_security_headers(app, csp_policy=None):
|
|||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
"script-src 'self' 'unsafe-inline'; "
|
"script-src 'self' 'unsafe-inline'; "
|
||||||
"style-src 'self' 'unsafe-inline'; "
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
"img-src 'self' data:; "
|
"img-src 'self' data: blob:; "
|
||||||
"font-src 'self'; "
|
"font-src 'self'; "
|
||||||
"connect-src 'self'; "
|
"connect-src 'self'; "
|
||||||
"frame-ancestors 'none'"
|
"frame-ancestors 'self'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove server header info
|
# Remove server header info
|
||||||
|
|||||||
+238
-49
@@ -15,10 +15,14 @@
|
|||||||
- 🔧 **扩展技能**:动态加载 `~/.aibox/skills/` 下的可执行脚本或 Markdown 技能文件
|
- 🔧 **扩展技能**:动态加载 `~/.aibox/skills/` 下的可执行脚本或 Markdown 技能文件
|
||||||
- 🌍 **多语言支持**:自动检测系统语言,支持中英文
|
- 🌍 **多语言支持**:自动检测系统语言,支持中英文
|
||||||
- 🧬 **智能体自我进化**:通过 `update_soul` / `update_identity` 工具动态更新角色定位与用户画像
|
- 🧬 **智能体自我进化**:通过 `update_soul` / `update_identity` 工具动态更新角色定位与用户画像
|
||||||
|
- 🤖 **子代理**:通过 `create_subagent` 工具派生出拥有独立上下文/角色设定的轻量子代理,主代理可分解长任务、隔离上下文,子代理可选用不同模型
|
||||||
- 🔄 **技能热重载**:无需重启即可添加、修改或删除扩展技能
|
- 🔄 **技能热重载**:无需重启即可添加、修改或删除扩展技能
|
||||||
- 🔐 **SOCKS5 代理**:通过 `set_proxy` / `get_proxy` 工具为网页访问与 API 调用配置代理,可持久化
|
- 🔐 **SOCKS5 代理**:通过 `set_proxy` / `get_proxy` 工具为网页访问与 API 调用配置代理,可持久化
|
||||||
- 💾 **对话管理**:历史对话归档、开始新对话(重置 AI 记忆并重新加载提示词)
|
- 💾 **对话管理**:历史对话归档、开始新对话(重置 AI 记忆并重新加载提示词)
|
||||||
- 📦 **对话压缩**:消息数或 Token 数达到阈值时自动压缩,保持上下文质量
|
- 📦 **对话压缩**:消息数或 Token 数达到阈值时自动压缩,保持上下文质量
|
||||||
|
- 🛡️ **内存安全防护**:文件读取按需加载不全载、命令输出 1MB 限制、单行超长截断、工具结果多层防护防止 OOM
|
||||||
|
- 🔒 **错误自动恢复**:API 请求失败自动回滚消息,工具结果安全清洗(控制字符过滤、非字符串转换)
|
||||||
|
- 🛑 **可靠中断机制**:支持 Ctrl+C 实时中断,通过 `threading.Event` + HTTP 连接关闭 + 子进程 kill 三层取消,流式响应和长时间运行命令均可立即终止
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
@@ -32,7 +36,7 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆或下载代码后,安装依赖
|
# 克隆或下载代码后,安装依赖
|
||||||
pip install websockets requests beautifulsoup4
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### 配置
|
### 配置
|
||||||
@@ -46,18 +50,18 @@ export DEEPSEEK_API_KEY="your-api-key"
|
|||||||
|
|
||||||
3. **启动服务器**:
|
3. **启动服务器**:
|
||||||
```bash
|
```bash
|
||||||
python main.py
|
python aibox.py
|
||||||
```
|
```
|
||||||
|
|
||||||
启动后,WebSocket 服务器将运行在 `ws://localhost:8765`(地址与端口可在 `config.json` 的 `system.websocket_host` / `system.websocket_port` 中修改)。
|
启动后,WebSocket 服务器将运行在 `ws://localhost:8765`(地址与端口可在 `config.json` 的 `system.websocket_host` / `system.websocket_port` 中修改)。
|
||||||
|
|
||||||
> 注意:本模块入口文件为 `main.py`(不是 `agent.py`)。`client.py` 为示例 WebSocket 客户端,`voice_client.py` 为语音客户端,`_run.sh` 为被 `core` 拉起的启动脚本。
|
> 注意:本模块入口文件为 `aibox.py`(单文件约 8450 行,集成 WebSocket 服务器、AI 引擎、工具/技能系统、任务调度器于一体)。`client.py` 为示例 WebSocket 客户端,`voice_client.py` 为语音客户端,`_run.sh` 为被 `core` 拉起的启动脚本。
|
||||||
|
|
||||||
## 📁 目录结构
|
## 📁 目录结构
|
||||||
|
|
||||||
```
|
```
|
||||||
aibox/
|
aibox/
|
||||||
├── main.py # 服务主程序(WebSocket 服务器 + AI 引擎 + 工具/技能系统)
|
├── aibox.py # 服务主程序(WebSocket 服务器 + AI 引擎 + 工具/技能系统 + 任务调度器)
|
||||||
├── client.py # 示例 WebSocket 客户端
|
├── client.py # 示例 WebSocket 客户端
|
||||||
├── voice_client.py # 语音客户端
|
├── voice_client.py # 语音客户端
|
||||||
├── entry.html # 前端入口页
|
├── entry.html # 前端入口页
|
||||||
@@ -85,12 +89,22 @@ aibox/
|
|||||||
```
|
```
|
||||||
~/.aibox/
|
~/.aibox/
|
||||||
├── config.json # 配置文件
|
├── config.json # 配置文件
|
||||||
├── prompts/ # 系统提示词目录
|
├── prompts/ # 系统提示词目录(按文件名数字前缀排序加载)
|
||||||
│ ├── 00_IDENTITY.md # 用户身份信息
|
│ ├── 00_IDENTITY.md # 用户身份信息(最先加载)
|
||||||
│ ├── 1_*.md # 基础技能提示词(文件操作、网页访问等)
|
│ ├── 1_memory_search.md # 基础技能提示词(1-10,按数字升序加载)
|
||||||
|
│ ├── 2_memory_save.md
|
||||||
|
│ ├── 3_file_operations.md
|
||||||
|
│ ├── 4_web_access.md
|
||||||
|
│ ├── 5_memo_management.md
|
||||||
|
│ ├── 6_command_execution.md
|
||||||
|
│ ├── 7_extension_skills.md
|
||||||
|
│ ├── 8_identity_management.md
|
||||||
|
│ ├── 9_language_instruction.md
|
||||||
|
│ ├── 10_task_handling.md
|
||||||
│ ├── 99_SOUL.md # AI 灵魂定义(角色、性格、能力边界)
|
│ ├── 99_SOUL.md # AI 灵魂定义(角色、性格、能力边界)
|
||||||
│ └── 100_STATUS.md # AI 状态记录
|
│ └── 100_STATUS.md # AI 状态记录(最后加载)
|
||||||
├── skills/ # 扩展技能目录(运行时从仓库 skills/ 复制或用户自建)
|
├── skills/ # 扩展技能目录(运行时从仓库 skills/ 复制或用户自建)
|
||||||
|
├── workspace/ # AI 专用工作目录(临时文件、脚本、数据文件默认存放位置)
|
||||||
├── memories.db # 记忆和任务数据库(SQLite)
|
├── memories.db # 记忆和任务数据库(SQLite)
|
||||||
├── current_conversation.json # 当前对话
|
├── current_conversation.json # 当前对话
|
||||||
├── conversation_history.json # 对话索引
|
├── conversation_history.json # 对话索引
|
||||||
@@ -98,16 +112,26 @@ aibox/
|
|||||||
└── tool_results/ # 工具结果缓存
|
└── tool_results/ # 工具结果缓存
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 📂 工作目录规范
|
||||||
|
|
||||||
|
为避免 AI 产生的临时文件污染系统其他目录,所有 AI 主动创建的临时文件、脚本、数据文件默认写入 `~/.aibox/workspace/` 目录:
|
||||||
|
|
||||||
|
- **文件操作**:`write_file` 工具默认路径为 `~/.aibox/workspace/`(用户明确指定路径时除外)
|
||||||
|
- **命令执行**:`run_command` 工具默认 `work_dir` 为 `~/.aibox/workspace/`,命令产生的文件落在该专用工作区
|
||||||
|
- **用户指定路径优先**:当用户明确要求在特定路径操作时,按用户指定路径执行
|
||||||
|
- **读取不受限**:读取系统文件、项目文件时使用实际路径,不受工作目录限制
|
||||||
|
- **技能调用**:`~/.aibox/skills/` 下的技能执行不受此限制
|
||||||
|
|
||||||
## 🔧 可用工具
|
## 🔧 可用工具
|
||||||
|
|
||||||
AI 智能体可以调用以下工具来完成任务(由 `Tool` 基类派生,在 `DeepSeekChat` 中注册):
|
AI 智能体可以调用以下工具来完成任务(由 `Tool` 基类派生,在 `DeepSeekChat` 中注册):
|
||||||
|
|
||||||
| 工具名称 | 功能描述 |
|
| 工具名称 | 功能描述 |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| `read_file` | 读取文件内容(支持分页、head/tail) |
|
| `read_file` | 读取文件内容(返回 markdown 指引,改用 `head`/`tail`/`sed`/`wc` 等命令行读取) |
|
||||||
| `write_file` | 写入或追加文件 |
|
| `write_file` | 写入或追加文件(返回 markdown 指引,改用 `mkdir -p` + `cat` 重定向写入) |
|
||||||
| `list_files` | 列出目录内容 |
|
| `list_files` | 列出目录内容(返回 markdown 指引,改用 `ls`/`du`/`find` 命令行列出) |
|
||||||
| `web` | 访问网页、获取标题/文本/链接 |
|
| `web` | 访问网页、获取标题/文本/链接(返回 markdown 指引,改用 `curl` 命令行访问) |
|
||||||
| `memory` | 保存、搜索(支持分页)、删除记忆 |
|
| `memory` | 保存、搜索(支持分页)、删除记忆 |
|
||||||
| `memo` | 创建、查看、完成、搜索任务 |
|
| `memo` | 创建、查看、完成、搜索任务 |
|
||||||
| `run_command` | 执行系统命令(实时输出,支持超时控制) |
|
| `run_command` | 执行系统命令(实时输出,支持超时控制) |
|
||||||
@@ -115,6 +139,7 @@ AI 智能体可以调用以下工具来完成任务(由 `Tool` 基类派生,
|
|||||||
| `reload_skills` | 热重载扩展技能 |
|
| `reload_skills` | 热重载扩展技能 |
|
||||||
| `create_skill` | 创建新技能(AI 自主编写 Python 脚本) |
|
| `create_skill` | 创建新技能(AI 自主编写 Python 脚本) |
|
||||||
| `install_skill` | 技能安装指引 |
|
| `install_skill` | 技能安装指引 |
|
||||||
|
| `ask_user` | 向用户提问(AI 主动交互) |
|
||||||
| `update_identity` | 更新用户身份信息(`00_IDENTITY.md`) |
|
| `update_identity` | 更新用户身份信息(`00_IDENTITY.md`) |
|
||||||
| `update_soul` | 更新 AI 自我定义(`99_SOUL.md`,智能体进化) |
|
| `update_soul` | 更新 AI 自我定义(`99_SOUL.md`,智能体进化) |
|
||||||
| `save_memory_and_end_conversation` | 保存记忆并结束对话 |
|
| `save_memory_and_end_conversation` | 保存记忆并结束对话 |
|
||||||
@@ -122,6 +147,8 @@ AI 智能体可以调用以下工具来完成任务(由 `Tool` 基类派生,
|
|||||||
| `calculator` | 数学计算 |
|
| `calculator` | 数学计算 |
|
||||||
| `set_proxy` | 设置 SOCKS5 代理(host/port/可选账号密码,可持久化,即时生效) |
|
| `set_proxy` | 设置 SOCKS5 代理(host/port/可选账号密码,可持久化,即时生效) |
|
||||||
| `get_proxy` | 查看当前 SOCKS5 代理配置 |
|
| `get_proxy` | 查看当前 SOCKS5 代理配置 |
|
||||||
|
| `express_emotion` | 表达情绪并记录当前情绪状态(anger/joy/sadness/surprise),可指定 `persona` 从 `~/.aibox/skills/` 加载人格技能(如 angry/coquettish/tsundere)派生携带该人格的情绪子代理,保证人格独立、不污染主对话上下文 |
|
||||||
|
| `create_subagent` | 派生子代理:独立上下文/角色设定,轻量无工具、可选独立模型,用于长任务分解、上下文隔离、独立角色专注处理 |
|
||||||
|
|
||||||
## 💬 WebSocket API
|
## 💬 WebSocket API
|
||||||
|
|
||||||
@@ -169,9 +196,16 @@ const ws = new WebSocket('ws://localhost:8765');
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 支持的命令
|
#### 5. 中断(Ctrl+C)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "interrupt",
|
||||||
|
"content": "用户按下了 Ctrl+C 中断当前操作"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
服务端收到后立即回复 `interrupt_ack`,并通过三层取消机制(Event标志 + HTTP连接关闭 + 子进程kill)终止正在进行的AI思考和工具执行。
|
||||||
|
|
||||||
> 注意:记忆搜索通过 `memory` 工具(在对话中由 AI 调用)完成,本系统**不存在** `/search` 斜杠命令。
|
### 支持的命令
|
||||||
|
|
||||||
| 命令 | 功能 |
|
| 命令 | 功能 |
|
||||||
|-----|------|
|
|-----|------|
|
||||||
@@ -179,6 +213,7 @@ const ws = new WebSocket('ws://localhost:8765');
|
|||||||
| `/new` | 开始新对话(保存当前,重置 AI 记忆,重新加载提示词) |
|
| `/new` | 开始新对话(保存当前,重置 AI 记忆,重新加载提示词) |
|
||||||
| `/list` | 列出所有历史对话 |
|
| `/list` | 列出所有历史对话 |
|
||||||
| `/memories` | 查看最近记忆(支持分页) |
|
| `/memories` | 查看最近记忆(支持分页) |
|
||||||
|
| `/search <关键词>` | 按关键词搜索记忆 |
|
||||||
| `/memos` | 查看待办任务 |
|
| `/memos` | 查看待办任务 |
|
||||||
| `/config` | 查看配置 |
|
| `/config` | 查看配置 |
|
||||||
| `/reload` | 重载配置 |
|
| `/reload` | 重载配置 |
|
||||||
@@ -191,15 +226,25 @@ const ws = new WebSocket('ws://localhost:8765');
|
|||||||
|
|
||||||
| 类型 | 说明 |
|
| 类型 | 说明 |
|
||||||
|-----|------|
|
|-----|------|
|
||||||
| `chunk` | AI 响应的片段(流式输出) |
|
| `chunk` | AI 响应的**增量片段**(流式输出,chunk_buffer 发送后即清零,为内容权威来源) |
|
||||||
| `complete` | AI 响应完成 |
|
| `reasoning_start` | AI 开始思考信号(思考模式,创建思考区域展示) |
|
||||||
| `reasoning` | AI 思考过程(思考模式) |
|
| `reasoning` | AI 思考过程增量片段(流式输出,灰色/斜体样式) |
|
||||||
| `error` | 错误信息 |
|
| `reasoning_end` | AI 思考结束信号(切换到正式回答) |
|
||||||
|
| `complete` | AI 响应**收尾信号**(chunk 流式是内容权威来源,complete 仅用于收尾;完全丢帧时可用 full_text 补建) |
|
||||||
|
| `line` | 过程行(🔧 工具调用提示、工具输出、💬 助手前缀等,供任务侧栏/终端显示) |
|
||||||
|
| `error` | 错误信息(API 请求失败、解析失败等;服务端自动回滚本轮消息防止对话历史被污染) |
|
||||||
| `command_result` | 命令执行结果 |
|
| `command_result` | 命令执行结果 |
|
||||||
| `task_status` | 任务状态更新(executing/complete/failed) |
|
| `task_status` | 任务状态更新(executing/complete/failed) |
|
||||||
| `task_process` | 任务执行过程中的 AI 输出 |
|
| `task_process` | 任务执行过程中的 AI 输出 |
|
||||||
|
| `task_error` | 任务执行错误 |
|
||||||
|
| `task_added` | 任务添加成功 |
|
||||||
|
| `task_list` | 待办任务列表(`task` action=list 的返回) |
|
||||||
|
| `task_completed` | 任务完成通知 |
|
||||||
| `dialogue_ended` | 对话结束通知 |
|
| `dialogue_ended` | 对话结束通知 |
|
||||||
| `dialogue_reset` | 对话重置完成 |
|
| `dialogue_reset` | 对话重置完成 |
|
||||||
|
| `notification` | 通知消息 |
|
||||||
|
| `connection_ack` | 连接确认 |
|
||||||
|
| `interrupt_ack` | 中断确认 |
|
||||||
| `info` | 系统信息 |
|
| `info` | 系统信息 |
|
||||||
| `pong` | Ping 响应 |
|
| `pong` | Ping 响应 |
|
||||||
|
|
||||||
@@ -357,15 +402,17 @@ AI 可以通过 `update_soul` 工具动态更新 `99_SOUL.md`,通过 `update_i
|
|||||||
|
|
||||||
## 🧩 提示词系统
|
## 🧩 提示词系统
|
||||||
|
|
||||||
提示词文件位于 `~/.aibox/prompts/`:
|
提示词文件位于 `~/.aibox/prompts/`,按文件名**数字前缀升序**加载(identity → 技能 1~10 → soul → status):
|
||||||
|
|
||||||
| 文件 | 用途 | 更新方式 |
|
| 文件 | 用途 | 更新方式 |
|
||||||
|-----|------|---------|
|
|-----|------|---------|
|
||||||
| `00_IDENTITY.md` | 用户身份信息 | AI 通过 `update_identity` 工具更新 |
|
| `00_IDENTITY.md` | 用户身份信息 | AI 通过 `update_identity` 工具更新 |
|
||||||
| `1_*.md` | 基础技能提示词 | 手动编辑,AI 通过 `reload_prompts` 重载 |
|
| `N_*.md`(N=1~10) | 基础技能提示词(记忆搜索/保存、文件/网页/命令/任务操作、技能扩展、身份管理、语言、任务规则) | 手动编辑,AI 通过 `/reload_prompts` 重载 |
|
||||||
| `99_SOUL.md` | AI 灵魂定义(角色、性格) | AI 通过 `update_soul` 工具更新 |
|
| `99_SOUL.md` | AI 灵魂定义(角色、性格、能力边界、行为准则) | AI 通过 `update_soul` 工具更新 |
|
||||||
| `100_STATUS.md` | AI 状态记录 | AI 通过 `save_memory_and_end_conversation` 更新 |
|
| `100_STATUS.md` | AI 状态记录 | AI 通过 `save_memory_and_end_conversation` 更新 |
|
||||||
|
|
||||||
|
> **加载顺序保证**:`00_IDENTITY.md` 最先加载(AI 先知道用户是谁),然后是 1~10 号技能提示词(按数字升序),接着是 `99_SOUL.md`(AI 的角色定位),最后是 `100_STATUS.md`(当前状态)。每轮用户输入前会附加一段简洁的工作流程提醒(不重复 system prompt 中的详细说明),工具调用后递归继续时不再重复注入。
|
||||||
|
|
||||||
## ⚙️ 配置说明
|
## ⚙️ 配置说明
|
||||||
|
|
||||||
主要配置项(`~/.aibox/config.json`,首次启动自动生成):
|
主要配置项(`~/.aibox/config.json`,首次启动自动生成):
|
||||||
@@ -374,46 +421,105 @@ AI 可以通过 `update_soul` 工具动态更新 `99_SOUL.md`,通过 `update_i
|
|||||||
{
|
{
|
||||||
"system": {
|
"system": {
|
||||||
"save_dir": "~/.aibox",
|
"save_dir": "~/.aibox",
|
||||||
|
"datetime_format": "%Y-%m-%d %H:%M:%S",
|
||||||
|
"date_format": "%Y%m%d_%H%M%S",
|
||||||
|
"debug_level": 1,
|
||||||
|
"load_history_by_default": true,
|
||||||
|
"log_file": "~/.aibox/ai.log",
|
||||||
|
"log_max_size": 10485760,
|
||||||
|
"log_backup_count": 5,
|
||||||
"websocket_host": "localhost",
|
"websocket_host": "localhost",
|
||||||
"websocket_port": 8765,
|
"websocket_port": 8765,
|
||||||
"debug_level": 1,
|
|
||||||
"log_file": "~/.aibox/ai.log",
|
|
||||||
"skills_dir": "~/.aibox/skills",
|
"skills_dir": "~/.aibox/skills",
|
||||||
"prompts_dir": "~/.aibox/prompts"
|
"prompts_dir": "~/.aibox/prompts",
|
||||||
|
"workspace_dir": "~/.aibox/workspace"
|
||||||
|
},
|
||||||
|
"scheduler": {
|
||||||
|
"enabled": true,
|
||||||
|
"check_interval": 1.0,
|
||||||
|
"max_tasks_per_run": 10,
|
||||||
|
"broadcast_enabled": true
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"provider": "deepseek",
|
||||||
|
"deepseek_api_url": "https://api.deepseek.com/chat/completions",
|
||||||
|
"deepseek_model": "deepseek-v4-flash",
|
||||||
|
"reasoning_enabled": false,
|
||||||
|
"api_key_env": "DEEPSEEK_API_KEY",
|
||||||
|
"timeout": 120,
|
||||||
|
"temperature": 0.7
|
||||||
},
|
},
|
||||||
"context": {
|
"context": {
|
||||||
"max_history_messages": 500,
|
"max_history_messages": 500,
|
||||||
|
"max_context_messages": 120,
|
||||||
|
"max_tokens": 8192,
|
||||||
|
"max_history_conversations": 5,
|
||||||
|
"preserve_system_prompts": true,
|
||||||
"compress_enabled": true,
|
"compress_enabled": true,
|
||||||
"compress_message_threshold": 200,
|
"compress_message_threshold": 200,
|
||||||
"compress_token_threshold": 120000,
|
"compress_token_threshold": 120000,
|
||||||
"compress_ratio": 0.2,
|
"compress_ratio": 0.2,
|
||||||
"compress_keep_recent": 8
|
"compress_keep_recent": 8
|
||||||
},
|
},
|
||||||
"scheduler": {
|
"memo": {
|
||||||
"enabled": true,
|
"default_reminder_minutes": 60,
|
||||||
"check_interval": 1.0,
|
"max_memos_per_check": 10,
|
||||||
"max_tasks_per_run": 10
|
"allow_user_tasks": true
|
||||||
},
|
},
|
||||||
"api": {
|
"files": {
|
||||||
"deepseek_api_url": "https://api.deepseek.com/chat/completions",
|
"max_file_size": 10485710
|
||||||
"deepseek_model": "deepseek-v4-flash",
|
},
|
||||||
"api_key_env": "DEEPSEEK_API_KEY"
|
"commands": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"timeout": 10,
|
||||||
|
"delay": 0,
|
||||||
|
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
|
"respect_robots": false,
|
||||||
|
"max_download_size": 104857600
|
||||||
},
|
},
|
||||||
"memory": {
|
"memory": {
|
||||||
"max_search_results": 100
|
"db_name": "memories.db",
|
||||||
|
"max_search_results": 100,
|
||||||
|
"default_importance": 2
|
||||||
|
},
|
||||||
|
"conversation": {
|
||||||
|
"current_file": "current_conversation.json",
|
||||||
|
"history_file": "conversation_history.json",
|
||||||
|
"archive_dir": "archive"
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"max_result_length": 10000
|
"max_result_length": 1048576,
|
||||||
|
"save_result_to_file": false,
|
||||||
|
"tool_results_dir": "tool_results"
|
||||||
|
},
|
||||||
|
"compressor": {
|
||||||
|
"min_messages_to_compress": 5,
|
||||||
|
"weight_decay_factor": 0.01,
|
||||||
|
"max_input_chars": 6000,
|
||||||
|
"llm_max_tokens": 800
|
||||||
|
},
|
||||||
|
"proxy": {
|
||||||
|
"type": "",
|
||||||
|
"host": "",
|
||||||
|
"port": 0,
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> 默认模型为 `deepseek-v4-flash`(通过 `api.deepseek_model` 修改)。API Key 从环境变量 `DEEPSEEK_API_KEY` 读取(亦支持 `~/.cert/deepseek.key` 文件)。
|
> 默认模型为 `deepseek-v4-flash`(通过 `api.deepseek_model` 修改)。设置 `api.reasoning_enabled: true` 可启用思考模式,此时会在请求中添加 `thinking: {type: "enabled"}` 参数,模型会先进行思考再回答,实时展示思考过程(灰色斜体区域),回答质量更高且**支持工具调用**(文件操作、命令执行、记忆搜索等工具均可在思考模式下正常使用)。
|
||||||
|
>
|
||||||
|
> API Key 从环境变量 `DEEPSEEK_API_KEY` 读取(亦支持 `~/.cert/deepseek.key` 文件)。
|
||||||
|
>
|
||||||
|
> **多厂商切换**:大模型调用已通过 `aibox/llm_client.py` 抽象层解耦,业务层不再直接依赖具体厂商。配置 `api.provider`(默认 `deepseek`)即可切换厂商,当前内置 `DeepSeekClient`(兼容 OpenAI Chat Completions 协议,Kimi / Qwen / GLM / Doubao 等绝大多数厂商均可复用)。新增厂商只需在此文件新增一个 `LLMClient` 子类并在 `create_llm_client` 工厂中注册,无需改动业务代码,为后续子代理/多模型共发预留了扩展点。
|
||||||
|
|
||||||
## 🛠️ 命令行参数
|
## 🛠️ 命令行参数
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python main.py [选项]
|
python aibox.py [选项]
|
||||||
|
|
||||||
选项:
|
选项:
|
||||||
--verbose, -v 显示详细日志
|
--verbose, -v 显示详细日志
|
||||||
@@ -434,19 +540,19 @@ python main.py [选项]
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 启动服务器(详细日志)
|
# 启动服务器(详细日志)
|
||||||
python main.py --verbose
|
python aibox.py --verbose
|
||||||
|
|
||||||
# 指定端口
|
# 指定端口
|
||||||
python main.py --port 9000
|
python aibox.py --port 9000
|
||||||
|
|
||||||
# 检查任务
|
# 检查任务
|
||||||
python main.py --check-memo
|
python aibox.py --check-memo
|
||||||
|
|
||||||
# 添加任务(命令行)
|
# 添加任务(命令行)
|
||||||
python main.py --memo-add "提醒开会|下午3点会议室A|+1h"
|
python aibox.py --memo-add "提醒开会|下午3点会议室A|+1h"
|
||||||
|
|
||||||
# 查看所有任务
|
# 查看所有任务
|
||||||
python main.py --memo-list
|
python aibox.py --memo-list
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔄 架构流程
|
## 🔄 架构流程
|
||||||
@@ -486,9 +592,8 @@ python main.py --memo-list
|
|||||||
### 普通对话流程
|
### 普通对话流程
|
||||||
```
|
```
|
||||||
Client -> Server: {type: "message", content: "帮我查天气"}
|
Client -> Server: {type: "message", content: "帮我查天气"}
|
||||||
Server -> Client: {type: "chunk", content: "正在查"}
|
Server -> Client: {type: "line", content: "🔧 调用工具: search_skills\n"}
|
||||||
Server -> Client: {type: "chunk", content: "询天气..."}
|
Server -> Client: {type: "line", content: "🔧 调用工具: bing_search\n"}
|
||||||
Server -> Client: {type: "tool_call", tool: "search_skills", args: {keyword: "天气"}}
|
|
||||||
Server -> Client: {type: "chunk", content: "今日天气晴朗..."}
|
Server -> Client: {type: "chunk", content: "今日天气晴朗..."}
|
||||||
Server -> Client: {type: "complete", content: "今日天气晴朗,温度25°C"}
|
Server -> Client: {type: "complete", content: "今日天气晴朗,温度25°C"}
|
||||||
```
|
```
|
||||||
@@ -516,10 +621,10 @@ Server -> Client: {type: "dialogue_reset", content: "✅ 对话状态已完全
|
|||||||
|
|
||||||
### 添加新工具
|
### 添加新工具
|
||||||
|
|
||||||
1. 继承 `Tool` 基类(位于 `main.py`)
|
1. 继承 `Tool` 基类(位于 `aibox.py`)
|
||||||
2. 实现 `get_name()`、`get_description()`、`get_parameters()`、`execute()`
|
2. 实现 `get_name()`、`get_description()`、`get_parameters()`、`execute()`
|
||||||
3. 可选实现 `execute_streaming()` 支持流式输出
|
3. 可选实现 `execute_streaming()` 支持流式输出(通过 `output_queue` 发送 `("line", content)` 和 `("complete", final_result)`)
|
||||||
4. 在 `DeepSeekChat` 的工具注册处注册
|
4. 在 `DeepSeekChat._register_tools()` 中注册
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class MyTool(Tool):
|
class MyTool(Tool):
|
||||||
@@ -540,6 +645,11 @@ class MyTool(Tool):
|
|||||||
return f"执行结果: {param}"
|
return f"执行结果: {param}"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **注意**:`execute()` 必须返回字符串。工具结果会经过多层安全防护:
|
||||||
|
> 1. **工具层源头限制**:文件/网页工具返回 markdown 指引,实际 I/O 由 `run_command` 完成(`run_command` 输出≤1MB,超出立即终止进程;`read_file` 指引命令限制 500 行、单行≤10KB)
|
||||||
|
> 2. **`_sanitize_tool_result()`**:None/非字符串转为安全字符串,过滤控制字符
|
||||||
|
> 3. **最终兜底截断**:超过 `tools.max_result_length`(默认1MB字符)时截断并提示,避免破坏 API 请求格式
|
||||||
|
|
||||||
### 添加新命令
|
### 添加新命令
|
||||||
|
|
||||||
在 `WebSocketHandler` 的命令处理分支(`handle_command`)中添加:
|
在 `WebSocketHandler` 的命令处理分支(`handle_command`)中添加:
|
||||||
@@ -550,6 +660,10 @@ elif command == "/mycommand":
|
|||||||
await self.send_message(websocket, {"type": "command_result", "content": result})
|
await self.send_message(websocket, {"type": "command_result", "content": result})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 错误恢复机制
|
||||||
|
|
||||||
|
`think_and_respond()` 在每轮对话开始前记录消息列表长度 `msgs_before_turn`。若 API 请求失败(网络错误、HTTP 错误、JSON 解析错误或其他异常),会自动调用 `_rollback_messages(msgs_before_turn)` 回滚本轮添加的所有消息(包括 user 消息、可能的 assistant/tool_calls 消息、tool 响应消息),防止格式错误的消息污染对话历史导致后续请求持续失败。客户端收到 `error` 消息后可安全重试。
|
||||||
|
|
||||||
## ❓ 常见问题
|
## ❓ 常见问题
|
||||||
|
|
||||||
### Q: API Key 无效怎么办?
|
### Q: API Key 无效怎么办?
|
||||||
@@ -595,15 +709,90 @@ elif command == "/mycommand":
|
|||||||
|
|
||||||
### Q: WebSocket 连接断开?
|
### Q: WebSocket 连接断开?
|
||||||
**A:**
|
**A:**
|
||||||
1. 检查服务器是否运行:`ps aux | grep main.py`
|
1. 检查服务器是否运行:`ps aux | grep aibox.py`
|
||||||
2. 确认端口未被占用:`netstat -tlnp | grep 8765`
|
2. 确认端口未被占用:`netstat -tlnp | grep 8765`
|
||||||
3. 检查防火墙设置
|
3. 检查防火墙设置
|
||||||
4. 查看服务器日志:`tail -f ~/.aibox/ai.log`
|
4. 查看服务器日志:`tail -f ~/.aibox/ai.log`
|
||||||
5. 考虑使用 `--host 0.0.0.0` 允许外部连接
|
5. 考虑使用 `--host 0.0.0.0` 允许外部连接
|
||||||
|
|
||||||
|
### Q: 工具调用后 API 报错无法继续?
|
||||||
|
**A:** 此问题已在 v2.1 修复:工具结果会经过安全清洗(控制字符过滤、非字符串转换、超长截断),API 请求失败时自动回滚本轮消息。如果仍出现问题:
|
||||||
|
1. 查看日志:`tail -f ~/.aibox/ai.log`
|
||||||
|
2. 使用 `/new` 命令重置对话
|
||||||
|
3. 确认工具返回值为字符串且不含二进制/控制字符
|
||||||
|
|
||||||
## 📄 更新日志
|
## 📄 更新日志
|
||||||
|
|
||||||
### v2.0 (当前版本)
|
### v2.8
|
||||||
|
- ✅ **新增 `express_emotion` 工具**:表达情绪并记录当前情绪状态(anger/joy/sadness/surprise),可指定 `persona` 从 `~/.aibox/skills/` 加载人格技能文件(如 angry/coquettish/tsundere),自动派生携带该人格的情绪子代理生成回应,保证人格独立、不污染主对话上下文
|
||||||
|
- ✅ **新增 `/search <关键词>` 命令**:在 WebSocket 会话中按关键词搜索记忆,返回匹配的记忆条目
|
||||||
|
|
||||||
|
### v2.7
|
||||||
|
- ✅ **新增子代理功能(`create_subagent` 工具)**:
|
||||||
|
- 主代理可派生出拥有**独立上下文窗口**和**独立角色设定**的轻量子代理,不污染主对话上下文
|
||||||
|
- 轻量子代理:无工具,通过一次 LLM 补全返回结果,适合长任务分解、上下文隔离、独立角色专注处理(如代码审查、文本总结、资料研究)
|
||||||
|
- 子代理可选用**不同模型**(复用 `llm_client` 抽象层,`create_llm_client` 新增 `model` 参数)
|
||||||
|
- 参数:`role`(角色/系统提示词,必填)、`task`(任务,必填)、`model`/`temperature`/`max_tokens`(可选)
|
||||||
|
- `max_tokens` 已钳制到 `[1, 主配置 DEFAULT_MAX_TOKENS]`,防止意外超限消耗
|
||||||
|
|
||||||
|
### v2.6
|
||||||
|
- ✅ **修复 Ctrl+C 中断无法停止服务器的问题**:
|
||||||
|
- 新增 `interrupt` WebSocket 消息类型处理(BotOps 前端 Ctrl+C 发送此消息),立即回复 `interrupt_ack` 确认
|
||||||
|
- `DeepSeekChat` 新增 `cancel()` 方法和 `_cancel_event`(threading.Event),线程安全可跨线程调用
|
||||||
|
- **三层取消机制**:
|
||||||
|
1. 设置取消标志,流式响应循环每轮检查,发现取消立即 break
|
||||||
|
2. 关闭当前 HTTP response 对象,中断 `iter_lines()` 阻塞读取
|
||||||
|
3. 终止正在运行的子进程(`SubprocessCommandExecutor.cancel()` → `process.kill()`)
|
||||||
|
- 工具调用循环中每个工具执行前检查取消信号,中断后不再继续后续工具
|
||||||
|
- API 请求异常/JSON解析异常/未知异常中均检查取消标志,若因取消导致异常则按中断处理而非报错
|
||||||
|
- 中断后自动回滚本轮消息,防止污染对话历史
|
||||||
|
- 新增 `EnhancedAIManager.cancel()` 统一入口,同时终止 AI 和命令执行器
|
||||||
|
|
||||||
|
### v2.5
|
||||||
|
- ✅ **新增 DeepSeek 思考模式支持**(Reasoning Mode):
|
||||||
|
- 新增 `api.reasoning_enabled` 配置项(默认 `false`),启用后在请求中添加 `thinking: {type: "enabled"}` 参数
|
||||||
|
- 使用 `deepseek-v4-flash` 模型原生思考能力,**思考模式下完整支持工具调用**(文件操作、命令执行、记忆搜索等均可正常使用)
|
||||||
|
- 实时展示 AI 思考过程:新增 `reasoning_start`/`reasoning`/`reasoning_end` WebSocket 消息类型
|
||||||
|
- 思考内容在前端以灰色斜体区域独立展示,超长思考自动折叠显示最后 2000 字符
|
||||||
|
- 即使未启用思考模式,若模型返回 `reasoning_content` 也会自动实时展示
|
||||||
|
- API 超时默认从 30s 提高到 120s,适配思考模式更长的响应时间
|
||||||
|
- ✅ 删除重复定义的 `_add_assistant_message_with_reasoning` 方法
|
||||||
|
|
||||||
|
### v2.4
|
||||||
|
- ✅ **新增专用工作目录规范**:
|
||||||
|
- AI 主动创建的临时文件、脚本、数据文件默认写入 `~/.aibox/workspace/`,避免污染系统其他目录
|
||||||
|
- `write_file` 工具默认路径设为工作目录(用户明确指定路径时除外)
|
||||||
|
- `run_command` 工具默认 `work_dir` 设为工作目录,命令产生的文件落在专用工作区
|
||||||
|
- 新增 `system.workspace_dir` 配置项(默认 `~/.aibox/workspace`),可自定义工作目录位置
|
||||||
|
- 在文件操作和命令执行提示词中明确工作目录规范
|
||||||
|
- ✅ 命令执行提示词补充 1MB 输出大小限制说明
|
||||||
|
|
||||||
|
### v2.3
|
||||||
|
- ✅ **修复工具输出OOM崩溃问题**:
|
||||||
|
- `read_file`:不再一次性 `readlines()` 加载整个文件,改为按行范围按需读取;大文件 `tail=true` 使用反向块读取(>1MB文件不全文遍历);新增单行长度限制(10KB/行,超长截断);`max_lines` 硬上限500行防止请求超大范围
|
||||||
|
- `run_command`:非流式执行从 `capture_output=True` 改为逐行读取并实时计数,输出达到1MB立即终止进程;流式执行同样增加1MB输出大小限制,超出终止并提示截断;统一stdout/stderr合并计数
|
||||||
|
- `tools.max_result_length` 默认值从10KB提高到1MB字符,作为最终兜底截断
|
||||||
|
- 形成"工具层源头限制→结果清洗→最终兜底"三层防护,彻底防止大文件/大输出导致内存溢出
|
||||||
|
|
||||||
|
### v2.2
|
||||||
|
- ✅ 修复提示词系统排序 bug:`SKILL_PREFIX = "1_"` 只匹配 `1_` 开头文件,导致 `2_`~`10_` 技能提示词排序错乱(`10_task_handling.md` 排在 `1_memory_search.md` 前面);改用正则按数字前缀排序
|
||||||
|
- ✅ 修复 `_init_system_prompts` 使用未排序列表的问题,确保 system prompt 按 identity → 技能 → soul → status 正确顺序加载
|
||||||
|
- ✅ 精简每轮用户输入的 pre_prompt:原来约 30 行重复指令(与 system prompt 内容重复),每轮浪费 ~300 token;精简为 5 条简洁提醒
|
||||||
|
- ✅ 修复工具调用后递归继续时重复注入工作流指令的问题:新增 `is_continuation` 参数,递归时跳过 pre_prompt
|
||||||
|
- ✅ 优化默认大文件阈值:50KB/200行 → 500KB/2000行,增加 `tail=true` 和二进制文件处理指导
|
||||||
|
- ✅ 优化记忆搜索习惯:"每次都要做"→"根据话题相关性判断",避免简单问候也触发无意义搜索
|
||||||
|
|
||||||
|
### v2.1
|
||||||
|
- ✅ 修复工具调用结果破坏对话导致 API 持续请求失败的问题
|
||||||
|
- 不再回传 `reasoning_content`(API 响应字段)到请求中
|
||||||
|
- `tool_call.id` 为空时自动生成 fallback id,防止 API 400
|
||||||
|
- 新增 `_sanitize_tool_result()`:None/非字符串安全转换、控制字符过滤、超长结果截断
|
||||||
|
- `execute_tool` 异常捕获,避免工具执行异常中断整个对话
|
||||||
|
- API 请求失败时自动回滚本轮消息(`_rollback_messages`),防止坏消息污染历史
|
||||||
|
- ✅ 修复 `complete` 消息语义:chunk 流式为内容权威来源,complete 仅用于收尾
|
||||||
|
- ✅ 修正文档:入口文件为 `aibox.py`(非 `main.py`),补充缺失的消息类型(`line`/`task_error`/`notification` 等)
|
||||||
|
|
||||||
|
### v2.0
|
||||||
- ✅ 新增 `update_soul` 工具(智能体自我进化)
|
- ✅ 新增 `update_soul` 工具(智能体自我进化)
|
||||||
- ✅ 新增 `create_skill` 工具(AI 自主创建技能)
|
- ✅ 新增 `create_skill` 工具(AI 自主创建技能)
|
||||||
- ✅ 新增 `install_skill` 工具(技能安装指引)
|
- ✅ 新增 `install_skill` 工具(技能安装指引)
|
||||||
|
|||||||
@@ -0,0 +1,824 @@
|
|||||||
|
# AIBOX 模块技术手册
|
||||||
|
|
||||||
|
> 文档版本:2026-07-30
|
||||||
|
> 代码版本:v2.0
|
||||||
|
> 语言:Python 3.8+
|
||||||
|
> 定位:单 AI 智能体系统(独立于微服务体系)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、模块概述
|
||||||
|
|
||||||
|
### 1.1 定位
|
||||||
|
|
||||||
|
**AIBOX** 是一个基于 DeepSeek API 的单 AI 智能体系统,通过 WebSocket 与客户端双向通信。所有任务(文件操作、网页访问、记忆管理、任务调度等)都由 AI 对话驱动执行。
|
||||||
|
|
||||||
|
### 1.2 与微服务体系的关系
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 启动方式 | `core`(PMC) 通过 `init.txt` 拉起 |
|
||||||
|
| 监听端口 | 8765(WebSocket) |
|
||||||
|
| 与主干关系 | **完全独立**,不调用 waf/gateway/dnfs 等服务 |
|
||||||
|
| 数据存储 | 本地 `~/.aibox/`(不使用 db:8005) |
|
||||||
|
| 前端 | 独立 HTML 页面 |
|
||||||
|
|
||||||
|
### 1.3 核心特性
|
||||||
|
|
||||||
|
| 特性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| AI 对话 | DeepSeek API + 思考模式(reasoning 流式输出) |
|
||||||
|
| 实时通信 | WebSocket 双向通信 + 流式输出 |
|
||||||
|
| 工具系统 | 19 个内置工具(文件/网页/记忆/任务/命令等) |
|
||||||
|
| 技能扩展 | 热重载 `.py` 脚本和 `.md` 技能 |
|
||||||
|
| 记忆管理 | 自动保存/检索对话记忆 |
|
||||||
|
| 任务调度 | 一次性/重复/即时任务 |
|
||||||
|
| 自我进化 | AI 动态更新角色与用户画像 |
|
||||||
|
| 对话压缩 | 自动压缩长对话历史 |
|
||||||
|
| 多语言 | 中英自动检测 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、技术架构
|
||||||
|
|
||||||
|
### 2.1 技术栈
|
||||||
|
|
||||||
|
| 层 | 技术 | 用途 |
|
||||||
|
|----|------|------|
|
||||||
|
| 语言 | Python 3.8+ | 主程序 |
|
||||||
|
| 通信 | websockets | WebSocket 服务器 |
|
||||||
|
| AI | DeepSeek API | 智能对话、工具调用、流式输出 |
|
||||||
|
| 存储 | SQLite | 记忆数据库、备忘录任务 |
|
||||||
|
| HTTP | requests | 网页访问、API 调用 |
|
||||||
|
| HTML 解析 | beautifulsoup4 | 网页内容提取 |
|
||||||
|
| SSH | paramiko | SSH 远程执行(技能) |
|
||||||
|
| 加密 | cryptography | AES 加密(技能) |
|
||||||
|
|
||||||
|
### 2.2 架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ WebSocket 客户端 │
|
||||||
|
│ (entry.html / frontend.html / client.py / voice_client.py) │
|
||||||
|
└────────────────────────┬────────────────────────────────────┘
|
||||||
|
│ ws://localhost:8765
|
||||||
|
┌────────────────────────▼────────────────────────────────────┐
|
||||||
|
│ WebSocket 服务器 (main.py) │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||||
|
│ │ 消息处理器 │ │ 命令处理器 │ │ 任务调度器 │ │
|
||||||
|
│ │ (handle_msg) │ │ (handle_cmd) │ │ (MemoScheduler) │ │
|
||||||
|
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
|
||||||
|
└─────────┼─────────────────┼───────────────────┼─────────────┘
|
||||||
|
│ │ │
|
||||||
|
┌─────────▼─────────────────▼───────────────────▼─────────────┐
|
||||||
|
│ AI 对话引擎 (DeepSeekChat) │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||||
|
│ │ 提示词管理器 │ │ 工具注册器 │ │ 对话压缩器 │ │
|
||||||
|
│ │ (PromptMgr) │ │ (ToolRegistry)│ │ (ConversationComp)│ │
|
||||||
|
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
|
||||||
|
└─────────┼─────────────────┼───────────────────┼─────────────┘
|
||||||
|
│ │ │
|
||||||
|
┌─────────▼─────────────────▼───────────────────▼─────────────┐
|
||||||
|
│ 数据持久层 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||||
|
│ │ 记忆数据库 │ │ 备忘录 DB │ │ 对话历史管理器 │ │
|
||||||
|
│ │ (memories.db) │ │ (memos.db) │ │ (JSON files) │ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 核心数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
客户端请求 → WebSocketHandler.handle_message()
|
||||||
|
↓
|
||||||
|
┌─── 消息类型判断 ───┬─── 命令类型判断 ───┬─── 任务操作判断 ───┐
|
||||||
|
│ type="message" │ type="command" │ type="task" │
|
||||||
|
└─────────┬─────────┴─────────┬─────────┴─────────┬─────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
AI 对话引擎 命令处理器 任务管理器
|
||||||
|
(DeepSeekChat) (handle_command) (handle_task)
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
工具调用循环 系统命令执行 任务 CRUD 操作
|
||||||
|
(ToolRegistry) (/new, /list...) (add, complete...)
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
流式响应生成 命令结果返回 任务状态广播
|
||||||
|
(chunk → complete) (command_result) (task_status)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、代码结构
|
||||||
|
|
||||||
|
### 3.1 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
aibox/
|
||||||
|
├── main.py # 核心(~8000 行)
|
||||||
|
│ ├── WebSocketHandler # WebSocket 消息处理
|
||||||
|
│ ├── DeepSeekChat # AI 对话引擎
|
||||||
|
│ ├── PromptManager # 提示词管理
|
||||||
|
│ ├── Tool + 19 个工具类 # 工具系统
|
||||||
|
│ ├── MemoScheduler # 任务调度器
|
||||||
|
│ ├── MemoryManager # 记忆管理
|
||||||
|
│ └── ConfigManager # 配置管理
|
||||||
|
├── client.py # CLI WebSocket 客户端
|
||||||
|
├── voice_client.py # 语音客户端(语音输入/输出)
|
||||||
|
├── entry.html # Web 前端入口
|
||||||
|
├── frontend.html # Web 主页面
|
||||||
|
├── skills/ # 内置扩展技能
|
||||||
|
│ ├── hello-world.py # 示例技能
|
||||||
|
│ ├── bing_search.py # 必应搜索
|
||||||
|
│ ├── ssh-exec.py # SSH 远程执行
|
||||||
|
│ ├── sftp-transfer.py # SFTP 传输
|
||||||
|
│ ├── facerec.py # 人脸识别
|
||||||
|
│ ├── aes-crypto.py # AES 加密
|
||||||
|
│ ├── gitclient.py # Git 操作
|
||||||
|
│ └── *.md # Markdown 技能(prompt)
|
||||||
|
├── tasks/ITIL.md # ITIL 任务模板
|
||||||
|
├── SKILLS_FORMAT.md # 技能格式规范
|
||||||
|
├── requirements.txt
|
||||||
|
├── _run.sh # core 拉起的启动脚本
|
||||||
|
├── _runcli.sh # CLI 启动
|
||||||
|
├── _set.sh # 依赖安装
|
||||||
|
├── _setenv.sh # 环境变量设置
|
||||||
|
└── README.md # 用户文档
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 main.py 核心类
|
||||||
|
|
||||||
|
| 类名 | 行号 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| `Tool` | L3642 | 工具基类(抽象方法:get_name/execute) |
|
||||||
|
| `WebSocketHandler` | ~L1900 | WebSocket 消息处理 |
|
||||||
|
| `DeepSeekChat` | ~L2400 | AI 对话引擎 |
|
||||||
|
| `PromptManager` | ~L1050 | 提示词文件管理 |
|
||||||
|
| `ConfigManager` | ~L1080 | 配置加载/合并 |
|
||||||
|
| `MemoryManager` | ~L5200 | 记忆 CRUD |
|
||||||
|
| `MemoScheduler` | ~L4700 | 任务调度器 |
|
||||||
|
| `ConversationCompressor` | ~L6480 | 对话压缩 |
|
||||||
|
|
||||||
|
### 3.3 运行期数据
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.aibox/
|
||||||
|
├── config.json # 配置(自动生成默认值)
|
||||||
|
├── prompts/
|
||||||
|
│ ├── 00_IDENTITY.md # 用户身份(AI 可更新)
|
||||||
|
│ ├── 1_*.md # 基础技能提示词
|
||||||
|
│ ├── 99_SOUL.md # AI 灵魂定义(AI 可更新)
|
||||||
|
│ └── 100_STATUS.md # AI 状态记录
|
||||||
|
├── skills/ # 用户扩展技能
|
||||||
|
├── memories.db # 记忆库(SQLite)
|
||||||
|
├── memos.db # 备忘录任务(SQLite)
|
||||||
|
├── current_conversation.json # 当前对话上下文
|
||||||
|
├── conversation_history.json # 对话索引
|
||||||
|
├── archive/ # 历史对话归档
|
||||||
|
├── tool_results/ # 工具执行结果缓存
|
||||||
|
└── ai.log # 日志(自动轮转)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、工具系统
|
||||||
|
|
||||||
|
### 4.1 工具基类
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Tool:
|
||||||
|
"""工具基类,所有工具必须继承此类"""
|
||||||
|
|
||||||
|
def get_name(self) -> str:
|
||||||
|
"""返回工具名称(抽象方法,必须实现)"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_description(self) -> str:
|
||||||
|
"""返回工具描述"""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_parameters(self) -> Dict:
|
||||||
|
"""返回工具参数(JSON Schema 格式)"""
|
||||||
|
return {"type": "object", "properties": {}}
|
||||||
|
|
||||||
|
def execute(self, **kwargs) -> str:
|
||||||
|
"""执行工具(抽象方法,必须实现)"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def execute_streaming(self, **kwargs):
|
||||||
|
"""流式执行(可选实现)"""
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 内置工具一览
|
||||||
|
|
||||||
|
| 工具名 | 类名 | 行号 | 功能 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `read_file` | `ReadFileTool` | L5762 | 读取文件(支持 head/tail/paging) |
|
||||||
|
| `write_file` | `WriteFileTool` | L5884 | 写入/追加文件 |
|
||||||
|
| `list_files` | `ListFilesTool` | L5961 | 目录列表 |
|
||||||
|
| `web` | `SimpleWebCrawlerTool` | L5637 | 网页抓取(标题/正文/链接) |
|
||||||
|
| `memory` | `MemoryTool` | L5280 | 记忆 CRUD(search/save/delete) |
|
||||||
|
| `memo` | `MemoTool` | L4753 | 任务 CRUD(add/complete/list/search) |
|
||||||
|
| `run_command` | `CommandLineTool` | L6150 | 系统命令执行(支持超时) |
|
||||||
|
| `search_skills` | `SearchSkillsTool` | L4076 | 搜索扩展技能 |
|
||||||
|
| `reload_skills` | `ReloadSkillsTool` | L4199 | 热重载技能 |
|
||||||
|
| `create_skill` | `CreateSkillTool` | L4409 | AI 自主创建技能 |
|
||||||
|
| `install_skill` | `InstallSkillTool` | L4306 | 技能安装指引 |
|
||||||
|
| `ask_user` | `AskUserTool` | L4275 | 向用户提问 |
|
||||||
|
| `update_identity` | `UpdateIdentityTool` | L4518 | 更新用户身份 |
|
||||||
|
| `update_soul` | `UpdateSoulTool` | L4582 | 更新 AI 灵魂 |
|
||||||
|
| `save_memory_and_end_conversation` | `SaveMemoryAndEndConversationTool` | L4646 | 保存记忆并结束对话 |
|
||||||
|
| `get_current_time` | `GetCurrentTimeTool` | L6055 | 获取当前时间 |
|
||||||
|
| `calculator` | `CalculatorTool` | L6095 | 数学计算 |
|
||||||
|
| `set_proxy` | `SetProxyTool` | L6415 | 设置 SOCKS5 代理 |
|
||||||
|
| `get_proxy` | `GetProxyTool` | L6453 | 查看代理配置 |
|
||||||
|
|
||||||
|
### 4.3 工具执行流程
|
||||||
|
|
||||||
|
```
|
||||||
|
AI 决定调用工具 → 生成 JSON 格式工具调用
|
||||||
|
↓
|
||||||
|
DeepSeekChat.process_tool_calls()
|
||||||
|
↓
|
||||||
|
┌─── 解析工具名 ───┬─── 解析参数 ───┬─── 注册工具 ───┐
|
||||||
|
└─────────────────┴───────────────┴─────────────────┘
|
||||||
|
↓
|
||||||
|
ToolRegistry.get_tool(name)
|
||||||
|
↓
|
||||||
|
tool.execute(**params)
|
||||||
|
↓
|
||||||
|
┌─── 结果处理 ───┬─── 错误处理 ───┬─── 长结果截断 ───┐
|
||||||
|
└─────────────────┴───────────────┴─────────────────┘
|
||||||
|
↓
|
||||||
|
返回结果给 AI → AI 继续生成响应
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、WebSocket 协议
|
||||||
|
|
||||||
|
### 5.1 客户端 → 服务器
|
||||||
|
|
||||||
|
| 类型 | 字段 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `message` | `content` | 普通对话文本 |
|
||||||
|
| `command` | `command` | 斜杠命令(如 `/new`, `/help`) |
|
||||||
|
| `task` | `action`, `title`, ... | 任务操作(add/complete/list) |
|
||||||
|
| `ping` | — | 心跳检测 |
|
||||||
|
|
||||||
|
### 5.2 服务器 → 客户端
|
||||||
|
|
||||||
|
| 类型 | 字段 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `chunk` | `content` | AI 响应片段(流式) |
|
||||||
|
| `complete` | `content` | AI 响应完成 |
|
||||||
|
| `reasoning` | `content` | AI 思考过程 |
|
||||||
|
| `error` | `content` | 错误信息 |
|
||||||
|
| `command_result` | `content` | 命令执行结果 |
|
||||||
|
| `task_status` | `task_id`, `status` | 任务状态更新 |
|
||||||
|
| `task_process` | `task_id`, `content` | 任务执行过程 |
|
||||||
|
| `dialogue_ended` | — | 对话结束 |
|
||||||
|
| `dialogue_reset` | — | 对话重置完成 |
|
||||||
|
| `info` | `content` | 系统信息 |
|
||||||
|
| `pong` | — | Ping 响应 |
|
||||||
|
|
||||||
|
### 5.3 消息流示例
|
||||||
|
|
||||||
|
```
|
||||||
|
// 普通对话
|
||||||
|
Client: {type: "message", content: "你好"}
|
||||||
|
Server: {type: "chunk", content: "你好!"}
|
||||||
|
Server: {type: "complete", content: "你好!有什么可以帮你的吗?"}
|
||||||
|
|
||||||
|
// 工具调用
|
||||||
|
Server: {type: "tool_call", tool: "web", args: {url: "..."}}
|
||||||
|
Server: {type: "chunk", content: "网页内容:..."}
|
||||||
|
Server: {type: "complete", content: "网页已获取"}
|
||||||
|
|
||||||
|
// 任务执行
|
||||||
|
Client: {type: "task", action: "add", title: "提醒", delay: 60}
|
||||||
|
Server: {type: "command_result", content: "任务已添加"}
|
||||||
|
// 60 秒后
|
||||||
|
Server: {type: "task_status", task_id: 1, status: "executing"}
|
||||||
|
Server: {type: "task_process", task_id: "1", content: "处理中..."}
|
||||||
|
Server: {type: "task_status", task_id: 1, status: "complete"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、任务系统
|
||||||
|
|
||||||
|
### 6.1 任务类型
|
||||||
|
|
||||||
|
| 类型 | 字段 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 一次性 | `reminder_time` | 指定执行时间 |
|
||||||
|
| 重复(daily) | `repeat_type: "daily"` | 每天执行 |
|
||||||
|
| 重复(weekly) | `repeat_type: "weekly"`, `weekday` | 每周指定日 |
|
||||||
|
| 重复(monthly) | `repeat_type: "monthly"`, `day` | 每月指定日 |
|
||||||
|
| 重复(custom) | `repeat_type: "custom"`, `interval`, `unit` | 自定义间隔 |
|
||||||
|
| 即时 | `is_immediate: true` | 立即执行 |
|
||||||
|
|
||||||
|
### 6.2 任务调度流程
|
||||||
|
|
||||||
|
```
|
||||||
|
MemoScheduler._run() # 后台线程
|
||||||
|
↓
|
||||||
|
检查所有未完成任务
|
||||||
|
↓
|
||||||
|
┌─── 到达时间? ───┬─── 未完成? ───┬─── 允许用户任务? ───┐
|
||||||
|
└─────────────────┴───────────────┴─────────────────┘
|
||||||
|
↓
|
||||||
|
创建新的 AI 对话(将任务内容作为用户输入)
|
||||||
|
↓
|
||||||
|
AI 处理任务(可能调用多个工具)
|
||||||
|
↓
|
||||||
|
标记任务已触发
|
||||||
|
↓
|
||||||
|
广播结果到所有客户端
|
||||||
|
↓
|
||||||
|
计算下一次执行时间(重复任务)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 数据库表结构
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- memories.db
|
||||||
|
CREATE TABLE memories (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT,
|
||||||
|
content TEXT,
|
||||||
|
importance INTEGER DEFAULT 2,
|
||||||
|
created_at TEXT,
|
||||||
|
updated_at TEXT,
|
||||||
|
embedding BLOB -- 预留向量嵌入
|
||||||
|
);
|
||||||
|
|
||||||
|
-- memos.db
|
||||||
|
CREATE TABLE memos (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT,
|
||||||
|
content TEXT,
|
||||||
|
reminder_time TEXT,
|
||||||
|
repeat_type TEXT,
|
||||||
|
repeat_interval INTEGER,
|
||||||
|
repeat_interval_unit TEXT,
|
||||||
|
is_immediate INTEGER DEFAULT 0,
|
||||||
|
status TEXT DEFAULT 'pending',
|
||||||
|
is_triggered INTEGER DEFAULT 0,
|
||||||
|
created_at TEXT,
|
||||||
|
user_id TEXT
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、记忆系统
|
||||||
|
|
||||||
|
### 7.1 记忆操作
|
||||||
|
|
||||||
|
| 操作 | AI 调用 | 说明 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 保存 | `memory(action="save", content=...)` | AI 主动保存重要信息 |
|
||||||
|
| 搜索 | `memory(action="search", query=...)` | 按关键词搜索记忆 |
|
||||||
|
| 列表 | `memory(action="list", page=...)` | 分页浏览所有记忆 |
|
||||||
|
| 删除 | `memory(action="delete", id=...)` | 删除指定记忆 |
|
||||||
|
|
||||||
|
### 7.2 自动记忆
|
||||||
|
|
||||||
|
- AI 在对话结束时(`/exit`)自动保存关键信息
|
||||||
|
- 可配置 `system.auto_save_memory` 开关
|
||||||
|
- 重要度(importance)1-5 级
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、对话压缩
|
||||||
|
|
||||||
|
### 8.1 压缩触发条件
|
||||||
|
|
||||||
|
| 条件 | 阈值(默认) | 配置项 |
|
||||||
|
|------|-------------|--------|
|
||||||
|
| 消息数超过 | 200 条 | `compress_message_threshold` |
|
||||||
|
| Token 数超过 | 120000 | `compress_token_threshold` |
|
||||||
|
|
||||||
|
### 8.2 压缩流程
|
||||||
|
|
||||||
|
```
|
||||||
|
检测到阈值触发
|
||||||
|
↓
|
||||||
|
保留最近 8 条消息(compress_keep_recent)
|
||||||
|
↓
|
||||||
|
对早期消息按时间衰减加权(weight_decay_factor=0.01)
|
||||||
|
↓
|
||||||
|
调用 AI 生成摘要(max_input_chars=6000)
|
||||||
|
↓
|
||||||
|
将摘要作为系统消息插入
|
||||||
|
↓
|
||||||
|
压缩后继续对话
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 压缩配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compressor": {
|
||||||
|
"min_messages_to_compress": 5,
|
||||||
|
"weight_decay_factor": 0.01,
|
||||||
|
"max_input_chars": 6000,
|
||||||
|
"llm_max_tokens": 800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、扩展技能系统
|
||||||
|
|
||||||
|
### 9.1 技能类型
|
||||||
|
|
||||||
|
| 类型 | 格式 | 存储位置 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 可执行脚本 | Python/Shell,支持 `--help` | `~/.aibox/skills/` |
|
||||||
|
| Markdown 技能 | `.md` 文件,首行为简介 | `~/.aibox/skills/` |
|
||||||
|
|
||||||
|
### 9.2 内置技能
|
||||||
|
|
||||||
|
| 技能名 | 文件 | 功能 |
|
||||||
|
|--------|------|------|
|
||||||
|
| hello-world | `skills/hello-world.py` | 示例技能 |
|
||||||
|
| bing_search | `skills/bing_search.py` | 必应搜索 |
|
||||||
|
| ssh-exec | `skills/ssh-exec.py` | SSH 远程执行 |
|
||||||
|
| sftp-transfer | `skills/sftp-transfer.py` | SFTP 文件传输 |
|
||||||
|
| facerec | `skills/facerec.py` | 人脸识别 |
|
||||||
|
| aes-crypto | `skills/aes-crypto.py` | AES 加密/解密 |
|
||||||
|
| gitclient | `skills/gitclient.py` | Git 操作封装 |
|
||||||
|
|
||||||
|
### 9.3 技能热重载
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 新技能放置到 ~/.aibox/skills/
|
||||||
|
# 调用 reload_skills 工具
|
||||||
|
# AI 下次对话时即可使用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.4 技能格式规范
|
||||||
|
|
||||||
|
详见 [SKILLS_FORMAT.md](file:///Users/qingsong/micro_servers/aibox/SKILLS_FORMAT.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、配置参考
|
||||||
|
|
||||||
|
### 10.1 完整配置项
|
||||||
|
|
||||||
|
#### system 段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `save_dir` | str | `~/.aibox` | 数据存储目录 |
|
||||||
|
| `datetime_format` | str | `%Y-%m-%d %H:%M:%S` | 日期时间格式 |
|
||||||
|
| `date_format` | str | `%Y%m%d_%H%M%S` | 文件名日期格式 |
|
||||||
|
| `debug_level` | int | `1` | 调试级别 0-3 |
|
||||||
|
| `load_history_by_default` | bool | `true` | 启动时加载历史对话 |
|
||||||
|
| `log_file` | str | `~/.aibox/ai.log` | 日志文件路径 |
|
||||||
|
| `log_max_size` | int | `10485760` | 日志最大字节数(10MB) |
|
||||||
|
| `log_backup_count` | int | `5` | 日志备份数量 |
|
||||||
|
| `websocket_host` | str | `localhost` | WebSocket 监听地址 |
|
||||||
|
| `websocket_port` | int | `8765` | WebSocket 监听端口 |
|
||||||
|
| `skills_dir` | str | `~/.aibox/skills` | 技能目录 |
|
||||||
|
| `prompts_dir` | str | `~/.aibox/prompts` | 提示词目录 |
|
||||||
|
|
||||||
|
#### scheduler 段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `enabled` | bool | `true` | 启用任务调度器 |
|
||||||
|
| `check_interval` | float | `1.0` | 检查间隔(秒) |
|
||||||
|
| `max_tasks_per_run` | int | `10` | 单次最大触发任务数 |
|
||||||
|
| `broadcast_enabled` | bool | `true` | 广播任务执行结果 |
|
||||||
|
|
||||||
|
#### api 段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `provider` | str | `deepseek` | 模型厂商,用于 `llm_client` 工厂路由 |
|
||||||
|
| `deepseek_api_url` | str | `https://api.deepseek.com/chat/completions` | API URL |
|
||||||
|
| `deepseek_model` | str | `deepseek-v4-flash` | 模型名 |
|
||||||
|
| `api_key_env` | str | `DEEPSEEK_API_KEY` | API Key 环境变量名 |
|
||||||
|
| `timeout` | int | `30` | 请求超时(秒) |
|
||||||
|
| `temperature` | float | `0.7` | 生成温度 0-1 |
|
||||||
|
|
||||||
|
#### context 段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `max_history_messages` | int | `500` | 历史消息上限 |
|
||||||
|
| `max_context_messages` | int | `120` | 上下文窗口 |
|
||||||
|
| `max_tokens` | int | `8192` | 最大生成 Token |
|
||||||
|
| `max_history_conversations` | int | `5` | 保留历史对话数 |
|
||||||
|
| `preserve_system_prompts` | bool | `true` | 保留系统提示词 |
|
||||||
|
| `compress_enabled` | bool | `true` | 启用对话压缩 |
|
||||||
|
| `compress_message_threshold` | int | `200` | 压缩消息阈值 |
|
||||||
|
| `compress_token_threshold` | int | `120000` | 压缩 Token 阈值 |
|
||||||
|
| `compress_ratio` | float | `0.2` | 压缩保留比例 |
|
||||||
|
| `compress_keep_recent` | int | `8` | 保留最近消息数 |
|
||||||
|
|
||||||
|
#### memo 段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `default_reminder_minutes` | int | `60` | 默认提醒分钟数 |
|
||||||
|
| `max_memos_per_check` | int | `10` | 单次最大触发任务数 |
|
||||||
|
| `allow_user_tasks` | bool | `true` | 允许用户创建任务 |
|
||||||
|
|
||||||
|
#### proxy 段
|
||||||
|
|
||||||
|
| 字段 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `type` | str | `""` | 代理类型(socks5/http) |
|
||||||
|
| `host` | str | `""` | 代理主机 |
|
||||||
|
| `port` | int | `0` | 代理端口 |
|
||||||
|
| `username` | str | `""` | 代理用户名 |
|
||||||
|
| `password` | str | `""` | 代理密码 |
|
||||||
|
|
||||||
|
### 10.2 环境变量
|
||||||
|
|
||||||
|
| 变量 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `DEEPSEEK_API_KEY` | DeepSeek API Key(必填) |
|
||||||
|
| `AI_CONFIG_PATH` | 自定义配置文件路径 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、命令行参数
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py [选项]
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `--verbose`, `-v` | flag | `false` | 显示详细日志 |
|
||||||
|
| `--debug` | int | `None` | 调试级别 0-3 |
|
||||||
|
| `--lang` | str | `None` | 强制语言 zh/en |
|
||||||
|
| `--host` | str | `None` | 覆盖 WebSocket 主机 |
|
||||||
|
| `--port` | int | `None` | 覆盖 WebSocket 端口 |
|
||||||
|
| `--no-scheduler` | flag | `false` | 禁用任务调度器 |
|
||||||
|
| `--scheduler-interval` | float | `1.0` | 调度器检查间隔 |
|
||||||
|
| `--config`, `-c` | str | `None` | 指定配置文件 |
|
||||||
|
| `--check-memo` | flag | `false` | 检查到期任务并退出 |
|
||||||
|
| `--memo-add` | str | `None` | 添加任务(CLI) |
|
||||||
|
| `--memo-list` | flag | `false` | 列出任务 |
|
||||||
|
| `--memo-complete` | int | `None` | 完成指定任务 |
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动服务器
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# 详细日志 + 指定端口
|
||||||
|
python main.py --verbose --port 9000
|
||||||
|
|
||||||
|
# 检查并执行到期任务(cron 场景)
|
||||||
|
python main.py --check-memo
|
||||||
|
|
||||||
|
# 添加任务
|
||||||
|
python main.py --memo-add "每日备份|执行数据库备份|+1h"
|
||||||
|
|
||||||
|
# 列出所有任务
|
||||||
|
python main.py --memo-list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、前端集成
|
||||||
|
|
||||||
|
### 12.1 前端文件
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `entry.html` | 简单入口页(重定向到 frontend.html) |
|
||||||
|
| `frontend.html` | 完整前端界面(对话/任务/设置) |
|
||||||
|
|
||||||
|
### 12.2 WebSocket 连接
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ws = new WebSocket('ws://localhost:8765');
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
content: '你好'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 接收消息
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
switch (data.type) {
|
||||||
|
case 'chunk':
|
||||||
|
appendToChat(data.content);
|
||||||
|
break;
|
||||||
|
case 'complete':
|
||||||
|
finishResponse();
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
showError(data.content);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十三、部署
|
||||||
|
|
||||||
|
### 13.1 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/micro_servers/aibox/
|
||||||
|
├── main.py
|
||||||
|
├── client.py
|
||||||
|
├── skills/
|
||||||
|
├── _run.sh # core 拉起入口
|
||||||
|
├── _set.sh # 首次安装
|
||||||
|
└── requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.2 core init.txt 配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# aibox 由 core 拉起
|
||||||
|
bash /opt/micro_servers/aibox/_run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 13.3 systemd 单元(可选)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=AIBOX AI Agent
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=deploy
|
||||||
|
WorkingDirectory=/opt/micro_servers/aibox
|
||||||
|
ExecStart=/usr/bin/python3 main.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=DEEPSEEK_API_KEY=xxx
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十四、已知限制
|
||||||
|
|
||||||
|
| ID | 描述 | 严重等级 |
|
||||||
|
|----|------|---------|
|
||||||
|
| L1 | 单实例运行,不支持多进程 | 🟡 Medium |
|
||||||
|
| L2 | 记忆库无向量搜索,仅关键词匹配 | 🟡 Medium |
|
||||||
|
| L3 | 对话压缩可能丢失早期重要信息 | 🟢 Low |
|
||||||
|
| L4 | 工具执行无超时控制(部分工具) | 🟡 Medium |
|
||||||
|
| L5 | 日志无远程推送,仅本地文件 | 🟢 Low |
|
||||||
|
| L6 | 无用户认证,WebSocket 连接可被滥用 | 🔴 High |
|
||||||
|
| L7 | API Key 以明文存储在环境变量或文件中 | 🟡 Medium |
|
||||||
|
|
||||||
|
### L6 详细说明
|
||||||
|
|
||||||
|
WebSocket 服务器**无任何认证**,任何能连接到 `ws://localhost:8765` 的客户端都可以:
|
||||||
|
- 与 AI 对话(消耗 API 额度)
|
||||||
|
- 执行系统命令(`run_command`)
|
||||||
|
- 操作文件系统(`read_file`/`write_file`)
|
||||||
|
- 创建/删除任务
|
||||||
|
|
||||||
|
**建议**:
|
||||||
|
1. 仅绑定 `localhost`(默认配置已这样做)
|
||||||
|
2. 添加 API Token 认证
|
||||||
|
3. 限制 `run_command` 可执行的命令范围
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十五、开发指南
|
||||||
|
|
||||||
|
### 15.1 添加新工具
|
||||||
|
|
||||||
|
1. 继承 `Tool` 基类
|
||||||
|
2. 实现 `get_name()`、`get_description()`、`get_parameters()`、`execute()`
|
||||||
|
3. 在 `DeepSeekChat._register_tools()` 中注册
|
||||||
|
4. 更新 README 文档
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyTool(Tool):
|
||||||
|
def get_name(self) -> str:
|
||||||
|
return "my_tool"
|
||||||
|
|
||||||
|
def get_description(self) -> str:
|
||||||
|
return "我的自定义工具"
|
||||||
|
|
||||||
|
def get_parameters(self) -> Dict:
|
||||||
|
return {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"param": {"type": "string", "description": "参数说明"}
|
||||||
|
},
|
||||||
|
"required": ["param"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute(self, param: str, **kwargs) -> str:
|
||||||
|
return f"执行结果: {param}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 15.2 添加新命令
|
||||||
|
|
||||||
|
在 `WebSocketHandler.handle_command()` 中添加:
|
||||||
|
|
||||||
|
```python
|
||||||
|
elif command == "/mycommand":
|
||||||
|
result = "命令执行结果"
|
||||||
|
await self.send_message(websocket, {
|
||||||
|
"type": "command_result",
|
||||||
|
"content": result
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 15.3 添加配置项
|
||||||
|
|
||||||
|
1. 在 `ConfigManager._get_default_config()` 中添加默认值
|
||||||
|
2. 在 README 文档中添加说明
|
||||||
|
3. 确保 `_merge_configs()` 能正确处理新增字段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十六、故障排查
|
||||||
|
|
||||||
|
### 16.1 常见问题
|
||||||
|
|
||||||
|
| 问题 | 排查步骤 |
|
||||||
|
|------|---------|
|
||||||
|
| API Key 无效 | 检查 `echo $DEEPSEEK_API_KEY` 或 `~/.cert/deepseek.key` |
|
||||||
|
| 技能不生效 | 确认技能文件在 `~/.aibox/skills/` 且有执行权限 |
|
||||||
|
| 任务不执行 | 检查调度器状态:`/tasks`;检查时间设置 |
|
||||||
|
| 对话压缩丢失信息 | 调整 `compress_message_threshold` 和 `compress_keep_recent` |
|
||||||
|
| WebSocket 断开 | 检查服务器进程:`ps aux | grep main.py`;查看日志 |
|
||||||
|
|
||||||
|
### 16.2 日志位置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看实时日志
|
||||||
|
tail -f ~/.aibox/ai.log
|
||||||
|
|
||||||
|
# 查看最近错误
|
||||||
|
grep "ERROR" ~/.aibox/ai.log | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### 16.3 数据库检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查记忆库
|
||||||
|
sqlite3 ~/.aibox/memories.db "SELECT * FROM memories LIMIT 10;"
|
||||||
|
|
||||||
|
# 检查任务库
|
||||||
|
sqlite3 ~/.aibox/memos.db "SELECT * FROM memos WHERE status='pending';"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十七、版本历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 主要变更 |
|
||||||
|
|------|------|---------|
|
||||||
|
| v2.0 | 当前 | 新增 `ask_user`、`update_soul`、`create_skill`、`install_skill`、`set_proxy`/`get_proxy` 等工具;支持 Markdown 技能;对话压缩支持 Token 阈值 |
|
||||||
|
| v1.0 | — | 初始版本:WebSocket 服务器、文件/网页/记忆操作、任务调度、扩展技能、多语言 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录
|
||||||
|
|
||||||
|
### A. 配置文件示例
|
||||||
|
|
||||||
|
完整默认配置见 [main.py:1100-1180](file:///Users/qingsong/micro_servers/aibox/main.py#L1100-L1180)
|
||||||
|
|
||||||
|
### B. 数据库 Schema
|
||||||
|
|
||||||
|
完整建表语句见:
|
||||||
|
- 记忆库:[main.py](file:///Users/qingsong/micro_servers/aibox/main.py) 中 `MemoryManager._init_db()`
|
||||||
|
- 任务库:[main.py](file:///Users/qingsong/micro_servers/aibox/main.py) 中 `MemoScheduler._init_db()`
|
||||||
|
|
||||||
|
### C. 参考文档
|
||||||
|
|
||||||
|
- [DeepSeek API 文档](https://platform.deepseek.com/docs)
|
||||||
|
- [websockets 库文档](https://websockets.readthedocs.io/)
|
||||||
|
- [SKILLS_FORMAT.md](file:///Users/qingsong/micro_servers/aibox/SKILLS_FORMAT.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文档由代码审查工具生成,供开发团队参考。*
|
||||||
+1
-1
@@ -3,6 +3,6 @@ cd "$(dirname "$0")" || exit
|
|||||||
source ./_setenv.sh
|
source ./_setenv.sh
|
||||||
source ./venv/bin/activate
|
source ./venv/bin/activate
|
||||||
export PATH=${PATH}:~/.aibox/skills
|
export PATH=${PATH}:~/.aibox/skills
|
||||||
exec python main.py $@
|
exec python aibox.py $@
|
||||||
|
|
||||||
|
|
||||||
+1758
-990
File diff suppressed because it is too large.
Load diff
+168
-10
@@ -9,11 +9,20 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import hashlib
|
import hashlib
|
||||||
|
from collections import OrderedDict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List
|
||||||
import websockets
|
import websockets
|
||||||
from colorama import init, Fore, Style as ColoramaStyle
|
from colorama import init, Fore, Style as ColoramaStyle
|
||||||
|
|
||||||
|
# rich 为可选依赖:有则 AI 回复渲染为 Markdown,无则回退纯文本流式输出
|
||||||
|
try:
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
_RICH = True
|
||||||
|
except ImportError:
|
||||||
|
_RICH = False
|
||||||
|
|
||||||
init(autoreset=True)
|
init(autoreset=True)
|
||||||
|
|
||||||
class Colors:
|
class Colors:
|
||||||
@@ -75,10 +84,15 @@ class WebSocketClient:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
self.callback("chunk", message)
|
self.callback("chunk", message)
|
||||||
except websockets.exceptions.ConnectionClosed:
|
except websockets.exceptions.ConnectionClosed:
|
||||||
self.connected = False
|
pass
|
||||||
self.callback("system", "❌ 连接已断开")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.callback("error", f"接收消息错误: {e}")
|
self.callback("error", f"接收消息错误: {e}")
|
||||||
|
finally:
|
||||||
|
# 统一在此通知断线;手动 disconnect() 已把 connected 置 False,不会误触发自动重连
|
||||||
|
was_connected = self.connected
|
||||||
|
self.connected = False
|
||||||
|
if was_connected:
|
||||||
|
self.callback("disconnected", "❌ 连接已断开")
|
||||||
|
|
||||||
async def _handle_message(self, data: dict):
|
async def _handle_message(self, data: dict):
|
||||||
msg_type = data.get('type', '')
|
msg_type = data.get('type', '')
|
||||||
@@ -134,13 +148,26 @@ class SimpleCLI:
|
|||||||
self.ai_full = ""
|
self.ai_full = ""
|
||||||
self.ai_prefix_shown = False
|
self.ai_prefix_shown = False
|
||||||
|
|
||||||
# 已显示消息的去重
|
# 已显示消息的去重(LRU 上限,防长对话内存无限增长)
|
||||||
self.displayed_messages: set = set()
|
self.DISPLAYED_MAX = 500
|
||||||
|
self.displayed_messages: OrderedDict = OrderedDict()
|
||||||
|
|
||||||
# 锁和状态
|
# 锁和状态
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.current_input = ""
|
self.current_input = ""
|
||||||
|
|
||||||
|
# 标准输入:单守护线程阻塞读 + asyncio.Queue 中转(避免 Ctrl+C 泄漏线程)
|
||||||
|
self._input_queue: asyncio.Queue = asyncio.Queue()
|
||||||
|
self._stdin_thread: Optional[threading.Thread] = None
|
||||||
|
|
||||||
|
# 自动重连状态
|
||||||
|
self._manual_disconnect = False # 用户主动 /disconnect、/quit、/connect 后置 True,抑制自动重连
|
||||||
|
self._reconnect_task: Optional[asyncio.Task] = None
|
||||||
|
|
||||||
|
# Markdown 渲染(rich 可用时启用)
|
||||||
|
self._console = Console() if _RICH else None
|
||||||
|
self._md_generating_shown = False # "生成中…"状态行是否已显示
|
||||||
|
|
||||||
# 流式输出状态
|
# 流式输出状态
|
||||||
self.streaming_line = False # 是否正在流式输出行
|
self.streaming_line = False # 是否正在流式输出行
|
||||||
self.in_tool_phase = False # 是否在工具调用阶段
|
self.in_tool_phase = False # 是否在工具调用阶段
|
||||||
@@ -242,6 +269,13 @@ class SimpleCLI:
|
|||||||
if key in self.displayed_messages:
|
if key in self.displayed_messages:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# rich 可用:流式期间只显示状态行并累计全文,complete 时一次性渲染
|
||||||
|
if _RICH:
|
||||||
|
self._md_show_generating()
|
||||||
|
self.ai_previous = clean
|
||||||
|
self.ai_full = clean
|
||||||
|
return
|
||||||
|
|
||||||
# 检测增量
|
# 检测增量
|
||||||
if self.ai_previous and clean.startswith(self.ai_previous):
|
if self.ai_previous and clean.startswith(self.ai_previous):
|
||||||
new_part = clean[len(self.ai_previous):]
|
new_part = clean[len(self.ai_previous):]
|
||||||
@@ -275,6 +309,39 @@ class SimpleCLI:
|
|||||||
self.ai_previous = clean
|
self.ai_previous = clean
|
||||||
self.ai_full = clean
|
self.ai_full = clean
|
||||||
|
|
||||||
|
# ── Markdown 渲染(rich)─────────────────────────────────────────────
|
||||||
|
def _md_show_generating(self):
|
||||||
|
"""流式期间显示一次性状态行(不实时渲染,避免 Live 闪烁/被卷走)。"""
|
||||||
|
if not _RICH:
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
if not self._md_generating_shown:
|
||||||
|
sys.stdout.write('\n')
|
||||||
|
sys.stdout.write(f"{Colors.AI}🤖 AI助手: 生成中…{Colors.RESET}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
self._md_generating_shown = True
|
||||||
|
self.streaming_line = True
|
||||||
|
self.prompt_shown = False
|
||||||
|
|
||||||
|
def _md_render_final(self, text: str):
|
||||||
|
"""complete 时一次性静态渲染最终 Markdown(先擦掉"生成中…"行)。"""
|
||||||
|
if not _RICH:
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
if self._md_generating_shown:
|
||||||
|
sys.stdout.write('\r' + ' ' * 40 + '\r')
|
||||||
|
self._md_generating_shown = False
|
||||||
|
self.streaming_line = False
|
||||||
|
if text:
|
||||||
|
self._console.print(Markdown(text))
|
||||||
|
|
||||||
|
def _remember_displayed(self, key: str):
|
||||||
|
"""记录已显示消息(LRU:超 DISPLAYED_MAX 淘汰最旧,防长对话内存膨胀)。"""
|
||||||
|
self.displayed_messages[key] = None
|
||||||
|
self.displayed_messages.move_to_end(key)
|
||||||
|
if len(self.displayed_messages) > self.DISPLAYED_MAX:
|
||||||
|
self.displayed_messages.popitem(last=False)
|
||||||
|
|
||||||
def display(self, msg_type: str, content: str, extra: Any = None):
|
def display(self, msg_type: str, content: str, extra: Any = None):
|
||||||
"""显示消息"""
|
"""显示消息"""
|
||||||
if msg_type == 'chunk':
|
if msg_type == 'chunk':
|
||||||
@@ -302,6 +369,11 @@ class SimpleCLI:
|
|||||||
elif msg_type == 'system':
|
elif msg_type == 'system':
|
||||||
self._finish_stream()
|
self._finish_stream()
|
||||||
self._print_line(f"{Colors.SYSTEM}{content}{Colors.RESET}")
|
self._print_line(f"{Colors.SYSTEM}{content}{Colors.RESET}")
|
||||||
|
elif msg_type == 'disconnected':
|
||||||
|
self.connected = False
|
||||||
|
self._finish_stream()
|
||||||
|
self._print_line(f"{Colors.SYSTEM}{content}{Colors.RESET}")
|
||||||
|
self._schedule_reconnect()
|
||||||
elif msg_type == 'info':
|
elif msg_type == 'info':
|
||||||
self._finish_stream()
|
self._finish_stream()
|
||||||
self._print_line(f"{Colors.INFO}ℹ️ {content}{Colors.RESET}")
|
self._print_line(f"{Colors.INFO}ℹ️ {content}{Colors.RESET}")
|
||||||
@@ -350,6 +422,25 @@ class SimpleCLI:
|
|||||||
|
|
||||||
def _handle_complete(self, content: str):
|
def _handle_complete(self, content: str):
|
||||||
"""处理完成事件 - 只负责结束流式输出,不重复显示内容"""
|
"""处理完成事件 - 只负责结束流式输出,不重复显示内容"""
|
||||||
|
# rich 渲染路径:擦掉"生成中…",一次性渲染最终 Markdown,恢复提示符
|
||||||
|
if _RICH:
|
||||||
|
clean = self._clean_content(content) if content else ""
|
||||||
|
self._md_render_final(clean)
|
||||||
|
if clean:
|
||||||
|
self._remember_displayed(clean[:200])
|
||||||
|
with self.lock:
|
||||||
|
self.in_tool_phase = False
|
||||||
|
self.waiting_for_response = False
|
||||||
|
self.streaming_line = False
|
||||||
|
self.prompt_shown = True
|
||||||
|
current = self.current_input
|
||||||
|
sys.stdout.write(self._get_prompt() + current)
|
||||||
|
sys.stdout.flush()
|
||||||
|
self.ai_previous = ""
|
||||||
|
self.ai_full = ""
|
||||||
|
self.ai_prefix_shown = False
|
||||||
|
return
|
||||||
|
|
||||||
# 如果之前在工具阶段,现在退出
|
# 如果之前在工具阶段,现在退出
|
||||||
if self.in_tool_phase:
|
if self.in_tool_phase:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
@@ -406,7 +497,7 @@ class SimpleCLI:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# 只有当内容没有被流式显示时才显示完整消息
|
# 只有当内容没有被流式显示时才显示完整消息
|
||||||
self.displayed_messages.add(key)
|
self._remember_displayed(key)
|
||||||
# 如果之前没有流式输出,现在显示完整消息
|
# 如果之前没有流式输出,现在显示完整消息
|
||||||
self._print_line(f"{Colors.AI}🤖 AI助手: {clean}")
|
self._print_line(f"{Colors.AI}🤖 AI助手: {clean}")
|
||||||
|
|
||||||
@@ -466,7 +557,7 @@ class SimpleCLI:
|
|||||||
clean = content.strip()
|
clean = content.strip()
|
||||||
key = clean[:200]
|
key = clean[:200]
|
||||||
if key not in self.displayed_messages:
|
if key not in self.displayed_messages:
|
||||||
self.displayed_messages.add(key)
|
self._remember_displayed(key)
|
||||||
self._print_line(f"{Colors.TASK}⚡ 任务 - {task_title}: {clean}")
|
self._print_line(f"{Colors.TASK}⚡ 任务 - {task_title}: {clean}")
|
||||||
del self._task_buffers[task_title]
|
del self._task_buffers[task_title]
|
||||||
self.streaming_line = False
|
self.streaming_line = False
|
||||||
@@ -519,10 +610,15 @@ class SimpleCLI:
|
|||||||
self.display("error", "用法: /connect ws://host:port")
|
self.display("error", "用法: /connect ws://host:port")
|
||||||
return
|
return
|
||||||
uri = parts[1]
|
uri = parts[1]
|
||||||
|
# 切换服务器:抑制旧连接的自动重连
|
||||||
|
self._manual_disconnect = True
|
||||||
|
if self._reconnect_task and not self._reconnect_task.done():
|
||||||
|
self._reconnect_task.cancel()
|
||||||
if self.client and self.client.connected:
|
if self.client and self.client.connected:
|
||||||
await self.client.disconnect()
|
await self.client.disconnect()
|
||||||
self.client = WebSocketClient(uri, self.display)
|
self.client = WebSocketClient(uri, self.display)
|
||||||
if await self.client.connect():
|
if await self.client.connect():
|
||||||
|
self._manual_disconnect = False
|
||||||
self.connected = True
|
self.connected = True
|
||||||
self.display("system", f"✅ 已连接到 {uri}")
|
self.display("system", f"✅ 已连接到 {uri}")
|
||||||
asyncio.create_task(self.client.receive_messages())
|
asyncio.create_task(self.client.receive_messages())
|
||||||
@@ -530,6 +626,9 @@ class SimpleCLI:
|
|||||||
self.client = None
|
self.client = None
|
||||||
self.connected = False
|
self.connected = False
|
||||||
elif cmd == '/disconnect':
|
elif cmd == '/disconnect':
|
||||||
|
self._manual_disconnect = True # 主动断开,不自动重连
|
||||||
|
if self._reconnect_task and not self._reconnect_task.done():
|
||||||
|
self._reconnect_task.cancel()
|
||||||
if self.client:
|
if self.client:
|
||||||
await self.client.disconnect()
|
await self.client.disconnect()
|
||||||
self.client = None
|
self.client = None
|
||||||
@@ -556,6 +655,9 @@ class SimpleCLI:
|
|||||||
elif cmd == '/quit':
|
elif cmd == '/quit':
|
||||||
self.display("system", "👋 再见!")
|
self.display("system", "👋 再见!")
|
||||||
self.running = False
|
self.running = False
|
||||||
|
self._manual_disconnect = True
|
||||||
|
if self._reconnect_task and not self._reconnect_task.done():
|
||||||
|
self._reconnect_task.cancel()
|
||||||
if self.client:
|
if self.client:
|
||||||
await self.client.disconnect()
|
await self.client.disconnect()
|
||||||
return
|
return
|
||||||
@@ -569,6 +671,35 @@ class SimpleCLI:
|
|||||||
self.display("error", f"未知命令: {cmd}")
|
self.display("error", f"未知命令: {cmd}")
|
||||||
self._print_prompt(clear_input=True)
|
self._print_prompt(clear_input=True)
|
||||||
|
|
||||||
|
def _schedule_reconnect(self):
|
||||||
|
"""启动自动重连(若未手动断开且当前无进行中的重连任务)。"""
|
||||||
|
if self._manual_disconnect or not self.running or not self.client:
|
||||||
|
return
|
||||||
|
if self._reconnect_task and not self._reconnect_task.done():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._reconnect_task = asyncio.create_task(self._auto_reconnect_loop(self.client))
|
||||||
|
except RuntimeError:
|
||||||
|
pass # 事件循环正在关闭(程序退出中),放弃重连
|
||||||
|
|
||||||
|
async def _auto_reconnect_loop(self, client: WebSocketClient):
|
||||||
|
"""断线自动重连:指数退避 2s→30s 封顶,直到成功、/disconnect 或退出。"""
|
||||||
|
delay = 2
|
||||||
|
while self.running and not self._manual_disconnect and self.client is client:
|
||||||
|
self.display("info", f"🔄 {delay}s 后自动重连 {client.uri} …(/disconnect 取消)")
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
if not self.running or self._manual_disconnect or self.client is not client:
|
||||||
|
return
|
||||||
|
if await client.connect():
|
||||||
|
self.connected = True
|
||||||
|
self.display("system", f"✅ 已自动重连到 {client.uri}")
|
||||||
|
asyncio.create_task(client.receive_messages())
|
||||||
|
return
|
||||||
|
delay = min(delay * 2, 30)
|
||||||
|
|
||||||
async def _send_interrupt(self):
|
async def _send_interrupt(self):
|
||||||
if self.client and self.client.connected:
|
if self.client and self.client.connected:
|
||||||
interrupt_msg = {"type": "interrupt", "content": "用户按下了 Ctrl+C 中断当前操作"}
|
interrupt_msg = {"type": "interrupt", "content": "用户按下了 Ctrl+C 中断当前操作"}
|
||||||
@@ -601,16 +732,43 @@ class SimpleCLI:
|
|||||||
await self.client.send_message(message)
|
await self.client.send_message(message)
|
||||||
# 不在这里显示提示符,等待AI响应结束后再显示
|
# 不在这里显示提示符,等待AI响应结束后再显示
|
||||||
|
|
||||||
|
def _stdin_reader(self, loop: asyncio.AbstractEventLoop):
|
||||||
|
"""独立守护线程:阻塞读 stdin,经 call_soon_threadsafe 投入队列。
|
||||||
|
|
||||||
|
进程生命周期只启动一次——替代原 run_in_executor 每行一个线程的方案,
|
||||||
|
避免 Ctrl+C 取消 await 后旧线程仍阻塞在 readline 上无限泄漏。
|
||||||
|
"""
|
||||||
|
def _put(item):
|
||||||
|
try:
|
||||||
|
loop.call_soon_threadsafe(self._input_queue.put_nowait, item)
|
||||||
|
except RuntimeError:
|
||||||
|
pass # 事件循环已关闭(程序退出中)
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
line = sys.stdin.readline()
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
if not line: # EOF (Ctrl+D)
|
||||||
|
_put(None)
|
||||||
|
break
|
||||||
|
_put(line)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
self.print_banner()
|
self.print_banner()
|
||||||
self._task_buffers = {}
|
self._task_buffers = {}
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
if self._stdin_thread is None:
|
||||||
|
self._stdin_thread = threading.Thread(
|
||||||
|
target=self._stdin_reader, args=(loop,),
|
||||||
|
daemon=True, name="stdin-reader")
|
||||||
|
self._stdin_thread.start()
|
||||||
|
|
||||||
while self.running:
|
while self.running:
|
||||||
try:
|
try:
|
||||||
line = await asyncio.get_event_loop().run_in_executor(
|
line = await self._input_queue.get()
|
||||||
None, lambda: sys.stdin.readline()
|
if line is None: # EOF (Ctrl+D)
|
||||||
)
|
|
||||||
if not line:
|
|
||||||
break
|
break
|
||||||
|
|
||||||
self.current_input = line.rstrip('\n')
|
self.current_input = line.rstrip('\n')
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
大模型客户端抽象层
|
||||||
|
|
||||||
|
为 aibox 提供统一的 LLM 调用接口,将「调用哪个厂商 / 哪个模型」与业务逻辑解耦。
|
||||||
|
业务层只依赖 `LLMClient` 抽象方法与统一的流式事件格式,切换厂商只需:
|
||||||
|
1. 新增一个 `LLMClient` 子类实现;
|
||||||
|
2. 在 `create_llm_client` 工厂里按 `api.provider` 进行路由。
|
||||||
|
|
||||||
|
当前内置实现:
|
||||||
|
- `DeepSeekClient`:兼容 OpenAI Chat Completions 协议(DeepSeek / Kimi / Qwen / GLM / Doubao 等绝大多数厂商均可复用)。
|
||||||
|
|
||||||
|
统一流式事件格式(`stream_chat` 逐条 yield 的 dict):
|
||||||
|
{"type": "content", "text": str} # 增量正文
|
||||||
|
{"type": "reasoning", "text": str} # 增量思考内容(可选,厂商支持时)
|
||||||
|
{"type": "tool_call", "index": int, "id": str, "name": str, "arguments": str} # 工具调用增量
|
||||||
|
{"type": "done"} # 流结束
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Dict, Iterator, List, Optional
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class LLMClient(ABC):
|
||||||
|
"""大模型客户端抽象基类。子类需实现流式/非流式两种调用,并归一化为统一事件格式。"""
|
||||||
|
|
||||||
|
def __init__(self, api_key: str, model: str, base_url: str,
|
||||||
|
timeout: int, config=None, logger=None):
|
||||||
|
self.api_key = api_key
|
||||||
|
self.model = model
|
||||||
|
self.base_url = base_url
|
||||||
|
self.timeout = timeout
|
||||||
|
self.config = config # 可选,用于读取代理等动态配置
|
||||||
|
self.logger = logger # 可选,用于日志输出
|
||||||
|
# 当前活动的 HTTP 响应引用,供 cancel() 强制关闭底层连接以中断阻塞读取
|
||||||
|
self._current_response = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""厂商/模型展示名,业务层可用于上下文(如 history 里的 assistant 名)。"""
|
||||||
|
return "LLM"
|
||||||
|
|
||||||
|
# ---------- 工具方法 ----------
|
||||||
|
|
||||||
|
def _resolve_proxies(self) -> Optional[Dict[str, str]]:
|
||||||
|
"""从 config 解析 requests 可用的 proxies 字典(动态代理)。"""
|
||||||
|
if self.config is not None and hasattr(self.config, "get_proxy"):
|
||||||
|
return self.config.get_proxy()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _log(self, level: str, msg: str):
|
||||||
|
if self.logger is not None and hasattr(self.logger, level):
|
||||||
|
try:
|
||||||
|
getattr(self.logger, level)(msg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
"""中断当前流式请求。线程安全,可从任意线程调用。"""
|
||||||
|
resp = self._current_response
|
||||||
|
self._current_response = None
|
||||||
|
if resp is not None:
|
||||||
|
try:
|
||||||
|
# 先关闭底层 raw socket,强制阻塞的 recv() 立即抛出异常
|
||||||
|
if getattr(resp, "raw", None) is not None:
|
||||||
|
resp.raw.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
resp.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ---------- 抽象接口 ----------
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def chat_completion(self, messages: List[Dict], temperature: float = 0.3,
|
||||||
|
max_tokens: int = 1024, **kwargs) -> str:
|
||||||
|
"""非流式对话,返回完整文本。用于对话压缩等离线摘要场景。"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def stream_chat(self, messages: List[Dict], temperature: float = 0.7,
|
||||||
|
max_tokens: int = 8192, tools: Optional[List[Dict]] = None,
|
||||||
|
tool_choice: Optional[str] = None,
|
||||||
|
reasoning_enabled: bool = False, **extra) -> Iterator[Dict]:
|
||||||
|
"""流式对话,逐条 yield 统一事件格式的 dict。业务层据此驱动输出/工具调用。"""
|
||||||
|
|
||||||
|
|
||||||
|
class DeepSeekClient(LLMClient):
|
||||||
|
"""DeepSeek 客户端(OpenAI Chat Completions 协议兼容)。
|
||||||
|
|
||||||
|
思考模式通过 `thinking: {"type": "enabled"}` 参数控制,思考内容字段为
|
||||||
|
`delta.reasoning_content`。二者均为 DeepSeek 特色,其他 OpenAI 兼容厂商
|
||||||
|
若字段不同,可在子类中覆写 `_parse_delta` 归一化。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, api_key: str, model: str, base_url: str,
|
||||||
|
timeout: int = 30, config=None, logger=None):
|
||||||
|
super().__init__(api_key, model, base_url, timeout, config, logger)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "DeepSeek"
|
||||||
|
|
||||||
|
def _headers(self) -> Dict[str, str]:
|
||||||
|
return {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
def chat_completion(self, messages: List[Dict], temperature: float = 0.3,
|
||||||
|
max_tokens: int = 1024, **kwargs) -> str:
|
||||||
|
payload = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": messages,
|
||||||
|
"temperature": temperature,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
}
|
||||||
|
payload.update(kwargs)
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
self.base_url,
|
||||||
|
headers=self._headers(),
|
||||||
|
json=payload,
|
||||||
|
timeout=30,
|
||||||
|
proxies=self._resolve_proxies(),
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
return result["choices"][0]["message"]["content"].strip()
|
||||||
|
self._log("error", f"LLM 压缩请求失败: HTTP {response.status_code}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
self._log("error", f"LLM 压缩请求异常: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def stream_chat(self, messages: List[Dict], temperature: float = 0.7,
|
||||||
|
max_tokens: int = 8192, tools: Optional[List[Dict]] = None,
|
||||||
|
tool_choice: Optional[str] = None,
|
||||||
|
reasoning_enabled: bool = False, **extra) -> Iterator[Dict]:
|
||||||
|
payload = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": messages,
|
||||||
|
"temperature": temperature,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"stream": True,
|
||||||
|
}
|
||||||
|
# 思考模式
|
||||||
|
if reasoning_enabled:
|
||||||
|
payload["thinking"] = {"type": "enabled"}
|
||||||
|
# 工具调用
|
||||||
|
if tools:
|
||||||
|
payload["tools"] = tools
|
||||||
|
payload["tool_choice"] = tool_choice or "auto"
|
||||||
|
payload.update(extra)
|
||||||
|
|
||||||
|
proxies = self._resolve_proxies()
|
||||||
|
response = requests.post(
|
||||||
|
self.base_url,
|
||||||
|
headers=self._headers(),
|
||||||
|
json=payload,
|
||||||
|
timeout=self.timeout,
|
||||||
|
stream=True,
|
||||||
|
proxies=proxies,
|
||||||
|
)
|
||||||
|
self._current_response = response
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"HTTP {response.status_code}: {response.text}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
for line in response.iter_lines():
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
line_text = line.decode("utf-8")
|
||||||
|
if not line_text.startswith("data: "):
|
||||||
|
continue
|
||||||
|
data = line_text[6:]
|
||||||
|
if data == "[DONE]":
|
||||||
|
yield {"type": "done"}
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
chunk = json.loads(data)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
self._log("error", f"JSON 解析错误: {e}, 数据: {data}")
|
||||||
|
continue
|
||||||
|
choices = chunk.get("choices", [])
|
||||||
|
if not choices:
|
||||||
|
continue
|
||||||
|
delta = choices[0].get("delta", {})
|
||||||
|
for event in self._parse_delta(delta, reasoning_enabled):
|
||||||
|
yield event
|
||||||
|
finally:
|
||||||
|
self._current_response = None
|
||||||
|
|
||||||
|
def _parse_delta(self, delta: Dict, reasoning_enabled: bool) -> Iterator[Dict]:
|
||||||
|
"""将单条 delta 归一化为统一事件。子类覆写以适配不同厂商字段。"""
|
||||||
|
# 思考内容
|
||||||
|
if reasoning_enabled and delta.get("reasoning_content"):
|
||||||
|
yield {"type": "reasoning", "text": delta["reasoning_content"]}
|
||||||
|
# 正文
|
||||||
|
if delta.get("content"):
|
||||||
|
yield {"type": "content", "text": delta["content"]}
|
||||||
|
# 工具调用(增量)
|
||||||
|
for tc in delta.get("tool_calls", []):
|
||||||
|
index = tc.get("index", 0)
|
||||||
|
function = tc.get("function", {})
|
||||||
|
yield {
|
||||||
|
"type": "tool_call",
|
||||||
|
"index": index,
|
||||||
|
"id": tc.get("id", ""),
|
||||||
|
"name": function.get("name", ""),
|
||||||
|
"arguments": function.get("arguments", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_api_key(api_key: Optional[str], config=None) -> str:
|
||||||
|
"""解析 API Key:优先显式传入,其次环境变量,最后 ~/.cert/deepseek.key。"""
|
||||||
|
if api_key:
|
||||||
|
return api_key
|
||||||
|
env_name = None
|
||||||
|
if config is not None and hasattr(config, "get"):
|
||||||
|
env_name = config.get("api.api_key_env")
|
||||||
|
env_name = env_name or "DEEPSEEK_API_KEY"
|
||||||
|
env_key = os.environ.get(env_name)
|
||||||
|
if env_key:
|
||||||
|
return env_key
|
||||||
|
try:
|
||||||
|
home = os.environ.get("HOME", os.environ["USERPROFILE"])
|
||||||
|
key_path = os.path.join(home, ".cert", "deepseek.key")
|
||||||
|
with open(key_path, "r") as f:
|
||||||
|
return f.read().strip()
|
||||||
|
except Exception:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"请设置环境变量 {env_name} 或在配置文件中设置 API Key")
|
||||||
|
|
||||||
|
|
||||||
|
def create_llm_client(config, api_key: Optional[str] = None,
|
||||||
|
logger=None, model: Optional[str] = None) -> LLMClient:
|
||||||
|
"""根据配置创建对应厂商的 LLM 客户端。可通过 `api.provider` 切换厂商。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: 配置对象(需提供 get 方法)。
|
||||||
|
api_key: 显式传入的 API Key;为 None 时按 resolve_api_key 解析。
|
||||||
|
logger: 可选日志对象。
|
||||||
|
model: 可选模型名覆盖;为 None 时使用配置中的 `api.deepseek_model`。
|
||||||
|
子代理等场景可通过此参数显式指定与主代理不同的模型。
|
||||||
|
"""
|
||||||
|
provider = (config.get("api.provider", "deepseek") or "deepseek").lower()
|
||||||
|
base_url = config.get("api.deepseek_api_url")
|
||||||
|
model = model or config.get("api.deepseek_model", "deepseek-v4-flash")
|
||||||
|
timeout = config.get("api.timeout", 30)
|
||||||
|
|
||||||
|
resolved_key = resolve_api_key(api_key, config)
|
||||||
|
|
||||||
|
if provider in ("deepseek", "deepseek-chat"):
|
||||||
|
return DeepSeekClient(
|
||||||
|
api_key=resolved_key,
|
||||||
|
model=model,
|
||||||
|
base_url=base_url,
|
||||||
|
timeout=timeout,
|
||||||
|
config=config,
|
||||||
|
logger=logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 其他 OpenAI 兼容厂商:可直接复用 DeepSeekClient(继承其协议解析),
|
||||||
|
# 或在此接入专有实现。新增厂商时在此扩展即可。
|
||||||
|
raise ValueError(f"未知的模型提供商: {provider}")
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
# backend
|
# backend
|
||||||
websockets
|
websockets
|
||||||
requests
|
requests
|
||||||
beautifulsoup4
|
|
||||||
|
|
||||||
# frontend
|
# frontend
|
||||||
websockets>=11.0
|
websockets>=11.0
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# sysinfo - 获取系统运行时间、CPU、内存、磁盘、网络、负载、温度、电池等状态信息的技能
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
本技能用于查询 Linux 服务器/本机的系统状态信息。它通过 `psutil` 库和系统命令采集数据,支持按需获取单项或组合信息,支持文本和 JSON 两种输出格式。
|
||||||
|
|
||||||
|
**依赖要求**:需要 Python3 环境,并安装 `psutil` 库(`pip install psutil`)。若未安装,可改用 `/proc`、`free`、`df`、`uptime`、`top` 等系统命令替代(见下文备选方案)。
|
||||||
|
|
||||||
|
## 信息类型及对应获取方法
|
||||||
|
|
||||||
|
### 1. 系统基本信息 (system)
|
||||||
|
- 系统名称、主机名、内核版本、硬件架构、处理器型号、Python 版本
|
||||||
|
- 命令:`uname -a`、`hostname`、`python3 --version`
|
||||||
|
|
||||||
|
### 2. 运行时间 (uptime)
|
||||||
|
- 开机时间、已运行时长(天/时/分/秒)
|
||||||
|
- 命令:`uptime -s`(开机时间)、`uptime`(运行时长)
|
||||||
|
|
||||||
|
### 3. CPU 信息 (cpu)
|
||||||
|
- 物理核心数、逻辑核心数、实时使用率、当前频率
|
||||||
|
- 命令:`lscpu`、`nproc`、`top -bn1 | head -5`、`cat /proc/cpuinfo`
|
||||||
|
|
||||||
|
### 4. 内存信息 (memory)
|
||||||
|
- 总内存、可用内存、已用内存、内存使用率、交换分区大小及使用率
|
||||||
|
- 命令:`free -h`
|
||||||
|
|
||||||
|
### 5. 指定磁盘信息 (disk)
|
||||||
|
- 指定路径(默认 `/`)的总空间、已用、可用、使用率
|
||||||
|
- 命令:`df -h /路径`
|
||||||
|
|
||||||
|
### 6. 所有磁盘分区 (disks)
|
||||||
|
- 所有挂载点的文件系统、总空间、已用、可用、使用率
|
||||||
|
- 命令:`df -hT`
|
||||||
|
|
||||||
|
### 7. 进程信息 (process)
|
||||||
|
- 进程总数、运行中进程数、睡眠进程数
|
||||||
|
- 命令:`ps -e | wc -l`、`ps -eo stat | grep -c '^R'`、`ps -eo stat | grep -c '^S'`
|
||||||
|
|
||||||
|
### 8. 网络信息 (network)
|
||||||
|
- 累计发送/接收的数据量(MB)、发送/接收包数
|
||||||
|
- 命令:`cat /proc/net/dev`、`ip -s link`
|
||||||
|
|
||||||
|
### 9. 系统负载 (load)
|
||||||
|
- 1/5/15 分钟负载及每核心负载
|
||||||
|
- 命令:`cat /proc/loadavg`、`uptime`
|
||||||
|
|
||||||
|
### 10. 温度信息 (temperature)
|
||||||
|
- CPU/主板等传感器温度(仅支持部分硬件)
|
||||||
|
- 命令:`sensors`(需安装 lm-sensors)、`cat /sys/class/thermal/thermal_zone*/temp`
|
||||||
|
|
||||||
|
### 11. 电池信息 (battery)
|
||||||
|
- 电量百分比、是否充电中、剩余时间(仅笔记本)
|
||||||
|
- 命令:`cat /sys/class/power_supply/BAT0/capacity`、`cat /sys/class/power_supply/BAT0/status`
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 方式一:直接运行原版 Python 脚本(若旧版技能仍可用)
|
||||||
|
```bash
|
||||||
|
# 获取系统基本信息
|
||||||
|
python3 /opt/micro_servers/aibox/skills/info.py --info system
|
||||||
|
|
||||||
|
# 获取 CPU + 内存 + 磁盘信息
|
||||||
|
python3 /opt/micro_servers/aibox/skills/info.py --info cpu --info memory --info disk
|
||||||
|
|
||||||
|
# 指定磁盘路径
|
||||||
|
python3 /opt/micro_servers/aibox/skills/info.py --info disk --disk-path /home
|
||||||
|
|
||||||
|
# 获取所有信息(JSON 格式,便于程序解析)
|
||||||
|
python3 /opt/micro_servers/aibox/skills/info.py --info all --format json
|
||||||
|
|
||||||
|
# 获取负载 + 温度 + 电池
|
||||||
|
python3 /opt/micro_servers/aibox/skills/info.py --info load --info temperature --info battery
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式二:使用系统命令(无 psutil 时的备选方案)
|
||||||
|
```bash
|
||||||
|
# 综合系统信息
|
||||||
|
uname -a && hostname && uptime && free -h && df -hT && nproc
|
||||||
|
```
|
||||||
|
|
||||||
|
## 参数说明(原版脚本)
|
||||||
|
| 参数 | 简写 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `--info` / `-i` | 可多次 | 信息类型:system/uptime/cpu/memory/disk/disks/process/network/load/temperature/battery/all |
|
||||||
|
| `--disk-path` / `-d` | 默认 `/` | 磁盘检查路径 |
|
||||||
|
| `--format` / `-f` | 默认 text | 输出格式:text 或 json |
|
||||||
|
| `--help` / `-h` | - | 显示帮助与示例 |
|
||||||
|
|
||||||
|
## 输出示例(文本格式)
|
||||||
|
```
|
||||||
|
系统信息:
|
||||||
|
系统: Linux
|
||||||
|
主机名: my-server
|
||||||
|
架构: x86_64
|
||||||
|
CPU信息:
|
||||||
|
物理核心数: 4
|
||||||
|
逻辑核心数: 8
|
||||||
|
使用率: 12.5%
|
||||||
|
内存信息:
|
||||||
|
总内存: 15.58 GB
|
||||||
|
可用内存: 8.21 GB
|
||||||
|
内存使用率: 45.2%
|
||||||
|
```
|
||||||
|
|
||||||
|
## 典型使用场景
|
||||||
|
1. **运维巡检**:配合定时任务,每日采集系统资源快照,监控负载与内存趋势
|
||||||
|
2. **故障排查**:磁盘空间告警时,快速定位使用率最高的挂载点
|
||||||
|
3. **性能评估**:获取 CPU 核心数、内存大小、网络吞吐等硬件基线数据
|
||||||
|
4. **上报集成**:使用 `--format json` 输出结构化数据,便于脚本解析和上报
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
- 温度/电池信息仅在支持对应硬件的系统上可用,服务器通常无电池
|
||||||
|
- CPU 使用率为瞬时采样(间隔 1 秒),如需平均值建议多次采样
|
||||||
|
- 网络数据为累计值,对比时需记录时间差
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# tech-manual - 生成Linux命令中文技术手册的技能(输入命令名,输出完整Markdown手册)
|
||||||
|
|
||||||
|
## 用途
|
||||||
|
|
||||||
|
当用户需要了解某个 Linux 命令的用法时,本技能指导智能体生成该命令的**完整中文技术手册**(Markdown 格式),涵盖语法、全部常用选项、环境变量、退出状态、实战示例和相关命令,内容基于 GNU coreutils / man pages 官方文档,准确实用。
|
||||||
|
|
||||||
|
## 适用场景
|
||||||
|
|
||||||
|
- 用户问:"帮我看看 xx 命令怎么用"
|
||||||
|
- 用户说:"生成 xx 命令的技术手册"
|
||||||
|
- 用户问:"xx 命令有哪些选项/参数"
|
||||||
|
- 用户想深入学习某个命令(ls、find、grep、tar、awk、sed、du、df、ps、top、netstat、ss 等)
|
||||||
|
|
||||||
|
## 执行步骤
|
||||||
|
|
||||||
|
1. **确认命令名**:从用户输入中提取目标命令名(如 `ls`、`find`)。若用户未指定,可用 ask_user 询问。
|
||||||
|
2. **收集资料**(可选):若对命令不熟悉,可运行 `man <命令> | col -b | head -200` 或 `info <命令>` 获取权威信息;也可用 `run_command` 执行 `命令 --help` 查看选项。
|
||||||
|
3. **生成手册内容**:按以下固定结构编写 Markdown 手册:
|
||||||
|
|
||||||
|
### 手册结构(8 个章节)
|
||||||
|
|
||||||
|
```
|
||||||
|
# <命令> 命令技术手册
|
||||||
|
|
||||||
|
## 1. 名称(NAME)
|
||||||
|
<命令> — 一句话功能说明
|
||||||
|
|
||||||
|
## 2. 语法(SYNOPSIS)
|
||||||
|
```bash
|
||||||
|
<命令> [选项]... [参数]...
|
||||||
|
```
|
||||||
|
给出标准调用形式,说明缺省参数时的行为。
|
||||||
|
|
||||||
|
## 3. 描述(DESCRIPTION)
|
||||||
|
命令的功能、工作原理、关键特性(3-6 条要点,可用列表)。
|
||||||
|
|
||||||
|
## 4. 选项(OPTIONS)
|
||||||
|
分类列出所有常用与关键选项(至少 20 个),每个选项格式:
|
||||||
|
#### `-x, --long-option`
|
||||||
|
作用说明(一句话)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<命令> -x
|
||||||
|
```
|
||||||
|
可选:典型输出示例。
|
||||||
|
|
||||||
|
分类建议(按命令特点调整):
|
||||||
|
- 基础选项
|
||||||
|
- 排序/过滤选项
|
||||||
|
- 输出格式选项
|
||||||
|
- 时间/状态选项
|
||||||
|
- 长格式字段说明(如 ls -l、ps -ef 的列含义)
|
||||||
|
- 权限/特殊位解析(如适用)
|
||||||
|
|
||||||
|
## 5. 环境变量(ENVIRONMENT)
|
||||||
|
影响该命令行为的环境变量,如 LS_COLORS、COLUMNS、TZ、LANG/LC_* 等。
|
||||||
|
|
||||||
|
## 6. 退出状态(EXIT STATUS)
|
||||||
|
| 状态码 | 含义 |
|
||||||
|
|--------|------|
|
||||||
|
| 0 | 成功 |
|
||||||
|
| 1 | 轻微错误(如文件不存在) |
|
||||||
|
| 2 | 严重错误(如参数错误) |
|
||||||
|
|
||||||
|
## 7. 示例(EXAMPLES)
|
||||||
|
至少 15 个真实场景示例,按类别分组(日常使用/排序查找/递归/时间/格式/管道脚本/实用技巧),每个示例用 ```bash 代码块并附注释。
|
||||||
|
|
||||||
|
## 8. 相关命令(SEE ALSO)
|
||||||
|
以表格列出相关命令及其用途。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文档基于 GNU coreutils/man pages 编写,适用于大多数 Linux 发行版。BSD/macOS 部分选项有差异,以 man 手册为准。*
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **写入文件**:将手册写入工作区文件 `~/.aibox/workspace/<命令名>_manual.md`。
|
||||||
|
- 内容较长时**分多次追加写入**(每次 200-400 行),避免 heredoc 超限。
|
||||||
|
- 写入后校验:`wc -l <文件>` 和 `grep -E "^#{1,3} " <文件>` 检查行数和章节完整性。
|
||||||
|
5. **汇报结果**:告知用户文件路径、总行数、章节概览,并询问是否需要进一步操作(如安装为技能、补充其他命令)。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 内容必须**准确**,基于 GNU coreutils / 官方 man pages,不要臆造选项。
|
||||||
|
- 代码示例统一用 ` ```bash ` 代码块。
|
||||||
|
- 若命令是 shell 内建(如 cd、echo)或非 coreutils 命令,结构可适当调整,但保持 8 章节框架。
|
||||||
|
- 写入工作区使用 `~/.aibox/workspace/` 目录,遵守工作目录规范。
|
||||||
|
|
||||||
|
## 典型示例
|
||||||
|
|
||||||
|
用户:"帮我生成 find 命令的技术手册"
|
||||||
|
→ 执行本技能,输出 `~/.aibox/workspace/find_manual.md`
|
||||||
|
|
||||||
|
用户:"grep 有哪些常用选项?"
|
||||||
|
→ 执行本技能,生成 grep 手册并重点展示选项章节
|
||||||
|
|
||||||
|
用户:"看看 tar 命令怎么打包解压"
|
||||||
|
→ 执行本技能,生成 tar 手册并重点展示示例章节
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# 数据库备份输出目录
|
||||||
|
# 容器内备份会输出到此目录,可直接复制走
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# prjs
|
||||||
|
nn/
|
||||||
|
bot
|
||||||
|
bot?
|
||||||
|
test_*
|
||||||
|
|
||||||
|
.git/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
#
|
||||||
|
*.so
|
||||||
|
*.o
|
||||||
|
*.cjo
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.lib
|
||||||
|
*.exp
|
||||||
|
main
|
||||||
|
|
||||||
|
# cangjie
|
||||||
|
build/
|
||||||
|
target/
|
||||||
|
output/
|
||||||
|
module-resolve.json
|
||||||
|
module-lock.json
|
||||||
|
cjpm.lock
|
||||||
|
.cjpm-history
|
||||||
|
*.macrocall
|
||||||
|
*.gcda
|
||||||
|
*.gcno
|
||||||
|
|
||||||
|
|
||||||
|
# 其他
|
||||||
|
log
|
||||||
|
report
|
||||||
|
tmp
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 卿松
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
MODE ?= cpu
|
||||||
|
VERSION := 0.0.2
|
||||||
|
APP_NAME := bot
|
||||||
|
CC:= gcc
|
||||||
|
CXX:= g++
|
||||||
|
NVCC:= nvcc
|
||||||
|
LIBS := -lpthread
|
||||||
|
FLAGS := -O3 -pipe #-Wall -Werror
|
||||||
|
|
||||||
|
#-----------
|
||||||
|
all: libi2c4cj.so libsnd4cj.so libmt4cj.so libfft4cj.so #libcnn4cj.so
|
||||||
|
|
||||||
|
# I2C
|
||||||
|
libi2c4cj.so: src/cpp/I2C.c
|
||||||
|
$(CC) $(FLAGS) -shared -fPIC $^ -o $@
|
||||||
|
|
||||||
|
# SOUND
|
||||||
|
libsnd4cj.so: src/cpp/snd.cpp
|
||||||
|
$(CC) $(FLAGS) -shared -fPIC $^ -o $@
|
||||||
|
|
||||||
|
# cpu nn
|
||||||
|
ifeq ($(MODE), cpu)
|
||||||
|
libmt4cj.so: src/cpp/Matrix.cpp
|
||||||
|
$(CC) $(FLAGS) -shared -fPIC $^ -o $@
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifeq ($(MODE), cuda)# GPU矩阵
|
||||||
|
libmt4cj.so: src/cuda/matrix.cu
|
||||||
|
#$(NVCC) -arch=compute_61 -code=sm_61 -O3 -Xcompiler -fPIC -shared src/cuda/matrix.cu -o libmt4cj.so
|
||||||
|
$(NVCC) -fPIC -shared src/cuda/matrix.cu -o libmt4cj.so
|
||||||
|
|
||||||
|
endif
|
||||||
|
|
||||||
|
# 快速傅里叶变换fft
|
||||||
|
libfft4cj.so: src/cpp/fft1.c src/cpp/fft.cpp
|
||||||
|
$(CC) $(FLAGS) -shared -fPIC $^ -lm -o $@
|
||||||
|
|
||||||
|
# 卷积神经网络cnn
|
||||||
|
libcnn4cj.so: src/cpp/cnn/cnn_i.cpp conv.o pooling.o ga_pooling.o flatten.o imgl.o
|
||||||
|
$(CC) $(FLAGS) -std=c++17 -shared -fPIC $^ -lm -o $@
|
||||||
|
|
||||||
|
conv.o: src/cpp/cnn/conv.cpp
|
||||||
|
$(CC) $(FLAGS) -shared -fPIC -c $^ -o $@
|
||||||
|
|
||||||
|
pooling.o: src/cpp/cnn/pooling.cpp
|
||||||
|
$(CC) $(FLAGS) -shared -fPIC -c $^ -o $@
|
||||||
|
|
||||||
|
ga_pooling.o: src/cpp/cnn/ga_pooling.cpp
|
||||||
|
$(CC) $(FLAGS) -shared -fPIC -c $^ -o $@
|
||||||
|
|
||||||
|
flatten.o: src/cpp/cnn/flatten.cpp
|
||||||
|
$(CC) $(FLAGS) -shared -fPIC -c $^ -o $@
|
||||||
|
|
||||||
|
imgl.o: src/cpp/cnn/imgl.cpp
|
||||||
|
$(CC) $(FLAGS) -shared -fPIC -c $^ -o $@
|
||||||
|
|
||||||
|
|
||||||
|
clear:
|
||||||
|
rm -f *.o
|
||||||
|
rm -f *.so
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# bot4cj
|
||||||
|
|
||||||
|
bot4cj是基于仓颉和 Linux I2C 接口的机器人硬件脚本解析器。
|
||||||
|
|
||||||
|
## 依赖库
|
||||||
|
libc、libpthread、liblua-5.4
|
||||||
|
|
||||||
|
You may need this
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
sudo ln -s /usr/lib/aarch64-linux-gnu/liblua5.4.so /usr/lib/aarch64-linux-gnu/liblua.so
|
||||||
|
sudo ln -s /usr/lib/x86_64-linux-gnu/liblua5.4.so /usr/lib/x86_64-linux-gnu/liblua.so
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目介绍
|
||||||
|
想要得到一个机器人软件通常比一个普通的软件更为困难,这是因为这个世界上的机器人五花八门,所以它们的软件往往都是独特的,所以我想写一种用于描述机器人硬件的通用的脚本。
|
||||||
|
|
||||||
|
`bot4cj` 基于仓颉编程语言和 Linux I2C 接口,提供了便捷的硬件控制能力,支持pca9685、mpu6050,我通常使用树莓派作为宿主机。
|
||||||
|
|
||||||
|
## 解析器
|
||||||
|
|
||||||
|
树莓派通过I2C接口连接pca9685后使用如下方法使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat ss/test.mt | cjpm run # 蜘蛛机器人脚本
|
||||||
|
```
|
||||||
|
|
||||||
|
## 动作脚本
|
||||||
|
|
||||||
|
项目中包含的动作脚本在ss文件夹下面,这些动作脚本的适配机器人型号如下
|
||||||
|
|
||||||
|
```textplain
|
||||||
|
# PCA9685
|
||||||
|
1号舵机(s1) => 左下水平关节
|
||||||
|
2号舵机(s2) => 左下垂直关节
|
||||||
|
3号舵机(s3) => 右下水平关节
|
||||||
|
4号舵机(s4) => 右下垂直关节
|
||||||
|
5号舵机(s5) => 右上水平关节
|
||||||
|
6号舵机(s6) => 右上垂直关节
|
||||||
|
7号舵机(s7) => 左上水平关节
|
||||||
|
8号舵机(s8) => 左上垂直关节
|
||||||
|
9号舵机(s9) => 云台1
|
||||||
|
10号舵机(s10) => 云台2
|
||||||
|
扬声器(spk) => 仅负责延时
|
||||||
|
```
|
||||||
|
|
||||||
|
适用的机器人建模文件[仓库链接](https://github.com/gopsc/Dashun)
|
||||||
|
|
||||||
|
### 机器人脚本语法(.mt)
|
||||||
|
|
||||||
|
**.bot文件是旧版动作脚本**
|
||||||
|
|
||||||
|
机器人脚本是行列式表格,每列之间以一个或多个空格、制表符分隔,每行以分号(;)结尾。
|
||||||
|
|
||||||
|
每个矩阵对象以@name 行开头,@ 行结束。
|
||||||
|
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```mt
|
||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
|
||||||
|
# 定义I2C硬件
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
|
||||||
|
# 定义pca9685硬件
|
||||||
|
@PCA9685
|
||||||
|
name I2C path;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
|
||||||
|
# 定义mpu6050硬件
|
||||||
|
@MPU6050
|
||||||
|
name I2C path;
|
||||||
|
mpu6050-0 i2c-1 0x69;
|
||||||
|
mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
# 定义扬声器硬件
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
# 定义舵机硬件
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵为硬件变量输入参数
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
90 - - - - - - - - - - 20; # left-behind
|
||||||
|
|
||||||
|
45 - - - - - - - - - 0 500; # stand
|
||||||
|
- - 90 - - - 45 - - - - 20; # right-behind
|
||||||
|
|
||||||
|
- - 45 - - - - - - - 200 500; # stand
|
||||||
|
- - - - 90 - - - - - 0 20; # right-front
|
||||||
|
|
||||||
|
- - - - 45 - - - - - - 500; # stand
|
||||||
|
- - - - - - 90 - - - - 20; # left-front
|
||||||
|
|
||||||
|
- - - - - - 45 - - - - 500; # stand
|
||||||
|
- - - - - - - - - - - 20; # stand
|
||||||
|
@
|
||||||
|
|
||||||
|
```
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
# bot4cj — 机器人硬件脚本解析器
|
||||||
|
|
||||||
|
基于**仓颉(Cangjie)** 编程语言和 Linux I2C 接口的机器人动作脚本解析器。用一种矩阵式脚本来描述机器人的硬件构成与动作序列,解析后驱动真实硬件执行。
|
||||||
|
|
||||||
|
## 架构分层
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ .mt 矩阵脚本(硬件定义 + 动作序列) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ MatrixParser(P.cj) — FSM 逐字符解析 → MatrixObject 链表 │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ RobotStruct(r.cj) — 从矩阵对象构建硬件变量(createHardVars) │
|
||||||
|
│ MatrixVars_Hardware 基类 + 闭包回调: │
|
||||||
|
│ · build() → rise 回调(打开硬件、获取资源) │
|
||||||
|
│ · play() → things 回调(执行动作,参数/返回值均为 JsonValue) │
|
||||||
|
│ · free() → free 回调(释放资源) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ 硬件实现:hd/*.cj │
|
||||||
|
│ PCA9685(I2C 0x40, 50Hz PWM, 角度→脉宽 0.5~2.5ms) │
|
||||||
|
│ Servo(舵机:max/min/offset 校准 + 角度翻转) │
|
||||||
|
│ MPU6050(陀螺+加速度,±500°/s ±8g,后台 10ms 采样线程+卡尔曼) │
|
||||||
|
│ Speaker(正弦波发声,5ms 淡入淡出) │
|
||||||
|
│ fb.cj(外部硬件扩展口) │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ 系统库:cpp/ + cuda/ │
|
||||||
|
│ libi2c4cj.so — I2C glibc 封装 + 20 槽锁数组 │
|
||||||
|
│ libmt4cj.so — FP32 矩阵运算(CPU / CUDA 二选一) │
|
||||||
|
│ libfft4cj.so — 基-2 Cooley-Tukey FFT │
|
||||||
|
│ libcnn4cj.so — Conv/Pool/GA-Pool/Flatten + Adam/Momentum │
|
||||||
|
│ libsnd4cj.so — WaveAPI / ALSA 音频播放 │
|
||||||
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
│ 数学/NN 库:math/*.cj │
|
||||||
|
│ Matrix1(二维矩阵)+ Tensor + 6 种激活函数 + Xavier 初始化 │
|
||||||
|
│ Nn1 全连接层(前向/反向/权重更新) │
|
||||||
|
│ Af(Sigmoid/Tanh/ReLU/Leaky_ReLU/Swish/Softmax) │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
| 层 | 技术 | 说明 |
|
||||||
|
|----|------|------|
|
||||||
|
| 应用层 | **仓颉 Cangjie** | 主程序、解析器、机器人结构、数学库、HTTP 服务 |
|
||||||
|
| 系统层 | C / C++ | I2C 驱动、矩阵运算、FFT、CNN、音频 |
|
||||||
|
| 加速层 | CUDA(可选) | `MODE=cpu/cuda` 切换 |
|
||||||
|
| 脚本层 | `.mt` 矩阵脚本 | 硬件定义 + 动作序列 |
|
||||||
|
| 依赖包 | ctype4cj / pthread4cj / spi4cj / lua4cj | 仓颉官方扩展包 |
|
||||||
|
| 构建 | Makefile(C/C++ 共享库)+ cjpm.toml(仓颉包管理) | — |
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
bot4cj/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.cj # 入口:CLI 参数解析(-i/-o/--test)+ stdin 管道执行
|
||||||
|
│ ├── P.cj # 矩阵脚本解析器(FSM 逐字符解析)
|
||||||
|
│ ├── r.cj # RobotStruct + MatrixVars_Hardware 基类
|
||||||
|
│ ├── testCNN.cj # CNN 测试入口
|
||||||
|
│ ├── testCNN1.cj # 几何 CNN 测试
|
||||||
|
│ ├── hd/
|
||||||
|
│ │ ├── I2C.cj # I2C 封装 + 20 槽锁数组
|
||||||
|
│ │ ├── PCA9685.cj # PCA9685 舵机控制板(已标记由 Lua 替代)
|
||||||
|
│ │ ├── Servo.cj # 舵机:校准 + 角度翻转
|
||||||
|
│ │ ├── MPU6050.cj # MPU6050 陀螺+加速度+卡尔曼滤波
|
||||||
|
│ │ ├── snd.cj # 扬声器
|
||||||
|
│ │ ├── fb.cj # 外部硬件扩展
|
||||||
|
│ │ └── extern/
|
||||||
|
│ │ └── macro_ExternHardware.cj
|
||||||
|
│ ├── math/
|
||||||
|
│ │ ├── Matrix.cj # 二维矩阵(+运算符重载)
|
||||||
|
│ │ ├── Tensor.cj # 张量
|
||||||
|
│ │ ├── Af.cj # 6 种激活函数
|
||||||
|
│ │ ├── Ini.cj # Xavier/Kaiming 初始化
|
||||||
|
│ │ ├── nn.cj # 全连接层 Nn1
|
||||||
|
│ │ ├── nns.cj # 旧版神经网络(XNOR 门测试)
|
||||||
|
│ │ ├── cnn.cj # CNN 仓颉绑定
|
||||||
|
│ │ ├── pooling.cj # 池化
|
||||||
|
│ │ ├── flatten.cj # Flatten
|
||||||
|
│ │ ├── fft.cj # FFT 仓颉绑定
|
||||||
|
│ │ ├── kalman.cj # 卡尔曼滤波
|
||||||
|
│ │ └── Adam.cj # Adam 优化器
|
||||||
|
│ ├── cpp/ # C/C++ 系统库源
|
||||||
|
│ │ ├── I2C.c # I2C glibc 封装
|
||||||
|
│ │ ├── Matrix.cpp # CPU 矩阵运算
|
||||||
|
│ │ ├── fft1.c # FFT 单实序列
|
||||||
|
│ │ ├── fft.cpp # FFT 双实序列 + 基-2 Cooley-Tukey
|
||||||
|
│ │ ├── kalman.c # 卡尔曼滤波
|
||||||
|
│ │ ├── snd.cpp # 音频播放(WaveAPI/ALSA)
|
||||||
|
│ │ └── cnn/ # 纯 C++ CNN(Conv/Pool/GA-Pool/Flatten)
|
||||||
|
│ └── cuda/
|
||||||
|
│ ├── matrix.cu # GPU 矩阵运算 kernel
|
||||||
|
│ └── error.cuh # CUDA 错误检查宏
|
||||||
|
├── modules/ # Lua 硬件模块(ADS1115/ADS1256/PCA9685/SPI)
|
||||||
|
├── scripts/ # .mt 动作脚本(std/jump/lie-down/tilt-* 等)
|
||||||
|
├── test/ # C/Lua 测试脚本
|
||||||
|
├── docs/Reports/ # 深度分析报告
|
||||||
|
├── cjpm.toml # 仓颉包配置
|
||||||
|
├── Makefile # C/C++ 共享库构建
|
||||||
|
├── _set.sh # 编译(cjpm update + build)
|
||||||
|
├── _run.sh # 运行(stdin 管道)
|
||||||
|
├── compile.ps1 # Windows 编译脚本
|
||||||
|
├── run.ps1 # Windows 运行脚本
|
||||||
|
├── README.md # 旧版说明
|
||||||
|
└── README.robot.md # 本文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 编译 C/C++ 共享库(一次性)
|
||||||
|
make MODE=cpu # CPU 模式(默认)
|
||||||
|
# 或
|
||||||
|
make MODE=cuda # GPU 模式(需要 nvcc)
|
||||||
|
|
||||||
|
# 2. 编译仓颉程序
|
||||||
|
bash _set.sh
|
||||||
|
|
||||||
|
# 3. 运行动作脚本(stdin 管道)
|
||||||
|
cat scripts/std.mt | bash _run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
> **硬件要求**:树莓派(aarch64)或其他 Linux I2C 设备。x86_64 开发板需自行验证 I2C 参数。Windows 暂不支持(依赖 glibc)。
|
||||||
|
|
||||||
|
## CLI 参数
|
||||||
|
|
||||||
|
```
|
||||||
|
./bot # 从 stdin 读取 .mt 脚本执行
|
||||||
|
./bot --help # 打印帮助
|
||||||
|
./bot --test nn # 运行神经网络与门测试
|
||||||
|
./bot --test cnn # 运行 CNN 测试
|
||||||
|
./bot --test cnn1 # 运行几何 CNN 测试
|
||||||
|
./bot --test matrix_o # 解析 ss/test.mt 并保存为 ss/output.mt
|
||||||
|
./bot --test matrix_2 # 解析 ss/output.mt → 执行 → 再次保存
|
||||||
|
./bot -i model.json -o out.json # 加载/保存神经网络模型(DRL 预留)
|
||||||
|
```
|
||||||
|
|
||||||
|
## .mt 脚本语法
|
||||||
|
|
||||||
|
行列式表格,每列以空格或制表符分隔,每行以分号 `;` 结尾。
|
||||||
|
|
||||||
|
### 块结构
|
||||||
|
|
||||||
|
```
|
||||||
|
@BlockName
|
||||||
|
column1 column2 column3;
|
||||||
|
value1 value2 value3;
|
||||||
|
@
|
||||||
|
```
|
||||||
|
|
||||||
|
每个 `@` 块由 `@Name` 行开始,`@` 行结束。支持的块类型:
|
||||||
|
|
||||||
|
| 块 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| `@Alias` | 列名别名(如 `A` → `angle`、`F` → `frequency`) |
|
||||||
|
| `@I2C` | 定义 I2C 设备(name + path) |
|
||||||
|
| `@PCA9685` | 定义 PCA9685 舵机控制板(name + I2C 引用 + 地址) |
|
||||||
|
| `@MPU6050` | 定义 MPU6050 传感器(name + I2C 引用 + 地址) |
|
||||||
|
| `@Speaker` | 定义扬声器(channels + sample_rate + volume + max_f + min_f) |
|
||||||
|
| `@Servo` | 定义舵机(name + max + min + offset + controller + channel) |
|
||||||
|
| `@Action` | **动作矩阵**:列头为硬件变量的属性,行为每帧的参数 |
|
||||||
|
|
||||||
|
### 占位符
|
||||||
|
|
||||||
|
`-` 表示"沿用上一行同列的值",当前行该列不产生调用。
|
||||||
|
|
||||||
|
### 完整示例
|
||||||
|
|
||||||
|
```mt
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0;
|
||||||
|
s2 180 - - - 1;
|
||||||
|
s3 -90 - - - 2;
|
||||||
|
s4 -180 - - - 3;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A spk.F spk.delay;
|
||||||
|
45 - 45 - 10 200; # 站立
|
||||||
|
90 - - - - 20; # 左转
|
||||||
|
@
|
||||||
|
```
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
```
|
||||||
|
stdin.read()
|
||||||
|
↓
|
||||||
|
P.MatrixParser (FSM 逐字符解析)
|
||||||
|
↓
|
||||||
|
LinkedList<MatrixObject> 链表
|
||||||
|
↓
|
||||||
|
RobotStruct.createHardVars() # 按 @I2C/@PCA9685/@Servo/@Speaker/@MPU6050 构建硬件变量
|
||||||
|
↓
|
||||||
|
robot.active() # build():逐个硬件调用 rise 回调(打开 I2C、初始化 PWM 等)
|
||||||
|
↓
|
||||||
|
robot.run() # 逐行遍历 @Action 矩阵,每列调用对应硬件的 play() 回调
|
||||||
|
↓
|
||||||
|
robot.deactive() # free():倒序释放资源
|
||||||
|
```
|
||||||
|
|
||||||
|
`play()` 的输入输出均为 `JsonValue`,实现了硬件与脚本之间的统一通信协议。
|
||||||
|
|
||||||
|
## 已支持的硬件
|
||||||
|
|
||||||
|
### PCA9685(I2C 舵机控制板)
|
||||||
|
- 地址:默认 `0x40`
|
||||||
|
- PWM 频率:50Hz(舵机标准 20ms 周期)
|
||||||
|
- 舵机脉宽映射:0°→0.5ms,180°→2.5ms
|
||||||
|
- 角度映射到 12 位 PWM 值(0-4095)
|
||||||
|
- **注意**:PCA9685.cj 代码已标记"由 Lua 脚本取代",保留作备用
|
||||||
|
|
||||||
|
### Servo(舵机)
|
||||||
|
- 支持 `max` / `min` 角度限制与 `offset` 漂移校准
|
||||||
|
- `max < 0` 时自动翻转角度(反向安装的舵机)
|
||||||
|
- 通过闭包回调与实际硬件(PCA9685/Lua 模块)解耦
|
||||||
|
|
||||||
|
### MPU6050(6 轴传感器)
|
||||||
|
- 陀螺仪:±500°/s 量程
|
||||||
|
- 加速度计:±8g 量程
|
||||||
|
- 后台 10ms 采样线程(pthread_create + pthread_cancel)
|
||||||
|
- 卡尔曼滤波融合数据
|
||||||
|
- **已知问题**:`ADDR_0` 与 `ADDR_1` 常量均为 `0x68`([MPU6050.cj:14-15](src/hd/MPU6050.cj#L14-L15)),第二个地址应改为 `0x69`
|
||||||
|
|
||||||
|
### Speaker(扬声器)
|
||||||
|
- 支持正弦波发声(指定频率 `F` + 延时 `delay`)
|
||||||
|
- 5ms 淡入淡出,避免"咔哒"声
|
||||||
|
- 通道数/采样率/音量可在 `.mt` 中配置
|
||||||
|
- **已知问题**:`play()` 仅 sleep 指定延时,真实发声依赖底层 C++ 实现
|
||||||
|
|
||||||
|
## 机器人型号适配
|
||||||
|
|
||||||
|
### Dashun 蜘蛛机器人(默认)
|
||||||
|
10 舵机 + 2 云台 + 扬声器,建模文件见 [gopsc/Dashun](https://github.com/gopsc/Dashun):
|
||||||
|
|
||||||
|
| 舵机 | 关节 | 舵机 | 关节 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| s1 | 左下水平 (rb0) | s6 | 右上垂直 (lf1) |
|
||||||
|
| s2 | 左下垂直 (rb1) | s7 | 左上水平 (rf0) |
|
||||||
|
| s3 | 右下水平 (lb0) | s8 | 左上垂直 (rf1) |
|
||||||
|
| s4 | 右下垂直 (lb1) | s9 | 云台1 (cam0) |
|
||||||
|
| s5 | 右上水平 (lf0) | s10 | 云台2 (cam1) |
|
||||||
|
|
||||||
|
### 可用脚本
|
||||||
|
|
||||||
|
`scripts/` 目录下预写了多种动作脚本:
|
||||||
|
- `std.mt` — 站立姿态
|
||||||
|
- `jump.mt` — 跳跃
|
||||||
|
- `lie-down.mt` — 趴下
|
||||||
|
- `left-hand.mt` / `rght-hand.mt` — 单手动作
|
||||||
|
- `left-turn.mt` / `tilt-f.mt` / `tilt-b.mt` / `tilt-l.mt` / `tilt-r.mt` — 转向与倾斜
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
### 依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 仓颉 1.1.3
|
||||||
|
# 仓颉官方扩展包:ctype4cj / pthread4cj / spi4cj / lua4cj
|
||||||
|
# libc / libpthread
|
||||||
|
# liblua-5.4(可选,Lua 硬件模块支持)
|
||||||
|
sudo ln -s /usr/lib/aarch64-linux-gnu/liblua5.4.so /usr/lib/aarch64-linux-gnu/liblua.so
|
||||||
|
sudo ln -s /usr/lib/x86_64-linux-gnu/liblua5.4.so /usr/lib/x86_64-linux-gnu/liblua.so
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 构建 C/C++ 共享库
|
||||||
|
make MODE=cpu
|
||||||
|
# 产出:libi2c4cj.so libmt4cj.so libfft4cj.so libsnd4cj.so
|
||||||
|
|
||||||
|
# 2. (可选)构建 CNN
|
||||||
|
make libcnn4cj.so
|
||||||
|
|
||||||
|
# 3. (可选)CUDA 模式需 NVIDIA CUDA Toolkit
|
||||||
|
make MODE=cuda
|
||||||
|
|
||||||
|
# 4. 仓颉主程序
|
||||||
|
bash _set.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
[_set.sh](_set.sh) 执行 `cjpm update` + `cjpm build`,自动解析 cjpm.toml 依赖。
|
||||||
|
|
||||||
|
### 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash _run.sh scripts/std.mt
|
||||||
|
# 或
|
||||||
|
cat scripts/std.mt | bash _run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
[_run.sh](_run.sh) 以 `cjStackSize=64MB` / `cjHeapSize=64MB` 启动。
|
||||||
|
|
||||||
|
## 神经网络支持
|
||||||
|
|
||||||
|
项目内置了全连接层 Nn1([math/nn.cj](src/math/nn.cj))与 CNN([math/cnn.cj](src/math/cnn.cj) + [cpp/cnn/](src/cpp/cnn/)):
|
||||||
|
|
||||||
|
- **Nn1**:多层感知机,支持前向传播、反向传播、权重更新
|
||||||
|
- **CNN**:Conv → ReLU → Pool → GA-Pool → Flatten → FC
|
||||||
|
- **优化器**:Adam / Momentum
|
||||||
|
- **激活函数**:Sigmoid / Tanh / ReLU / Leaky_ReLU / Swish / Softmax
|
||||||
|
- **初始化**:Xavier / Kaiming
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bot --test nn # 与门训练(2 输入 → 1 输出)
|
||||||
|
./bot --test cnn # CNN 前向/反向测试
|
||||||
|
./bot --test cnn1 # 几何分类 CNN 测试
|
||||||
|
```
|
||||||
|
|
||||||
|
### DRL 预留(未完成)
|
||||||
|
|
||||||
|
main.cj 中预留了 DRL 流程(注释状态):加载预训练权重 → 执行动作 → 保存微调模型。通过 `-i` / `-o` 参数指定输入/输出路径。
|
||||||
|
|
||||||
|
## 安全与限制
|
||||||
|
|
||||||
|
### I2C 总线保护
|
||||||
|
- [hd/I2C.cj](src/hd/I2C.cj) 内置 **20 槽锁数组**,多硬件共享 I2C 总线时自动互斥
|
||||||
|
- 每个 I2C fd 独立加锁,避免并发写入冲突
|
||||||
|
|
||||||
|
### 硬件关闭时机
|
||||||
|
- `MatrixVars_Hardware` 析构依赖仓颉 GC,**不保证**在确定性时机调用 `free()`
|
||||||
|
- 机器人使用 `try/finally` 在 [main.cj:80-87](src/main.cj#L80) 中显式调用 `deactive()`,确保硬件资源正确释放
|
||||||
|
|
||||||
|
### 线程安全
|
||||||
|
- MPU6050 采样通过 `pthread_create` 启动后台线程
|
||||||
|
- `pthread_cancel` 依赖系统定义的取消点(sleep / 系统调用),可能在非安全点中断
|
||||||
|
|
||||||
|
### 平台支持
|
||||||
|
- ✅ Linux aarch64(树莓派)/ x86_64
|
||||||
|
- ❌ Windows(依赖 glibc)
|
||||||
|
|
||||||
|
## 已知问题
|
||||||
|
|
||||||
|
| ID | 描述 | 影响 |
|
||||||
|
|----|------|------|
|
||||||
|
| D1 | 仓颉 GC 不保证析构器调用,I2C fd 可能泄漏 | 长时间运行后 I2C 资源耗尽 |
|
||||||
|
| D2 | Speaker `play()` 仅 sleep,真实发声未接入 C++ | 扬声器无实际声音 |
|
||||||
|
| D3 | MPU6050 `pthread_cancel` 可能在非安全点中断 | 传感器数据不一致 |
|
||||||
|
| D4 | PCA9685 初始化被标记为"由 Lua 取代",但仍保留主路径 | 代码存在双实现 |
|
||||||
|
| D5 | `_set.sh` 会删除 `a.out` | 误删用户产物 |
|
||||||
|
| D6 | Softmax 雅可比矩阵实现错误(math/Af.cj) | NN 训练收敛异常 |
|
||||||
|
| D7 | Tensor 乘法仅支持 2D,3D+ 会 panic | 限制模型复杂度 |
|
||||||
|
| D8 | Tanh 导数实现错误(使用 `1 - y^2` 而非 `1 - tanh^2(x)`) | NN 训练收敛异常 |
|
||||||
|
| D9 | CNN `testGeometryCNN` 使用随机数据 | 测试无实际意义 |
|
||||||
|
| D10 | MPU6050 `ADDR_0` 与 `ADDR_1` 均为 `0x68` | 无法同时使用两个 MPU6050 |
|
||||||
|
| D11 | `@Action` 与 `@Action_Auto` 并存未合并 | 脚本语法冗余 |
|
||||||
|
|
||||||
|
## 相关资源
|
||||||
|
|
||||||
|
- [Dashun 蜘蛛机器人建模](https://github.com/gopsc/Dashun)
|
||||||
|
- [深度分析报告](docs/Reports/analysis_20260525_00.md)(28 章节,1280 行)
|
||||||
|
- [仓颉编程语言](https://cangjie-lang.org/)
|
||||||
|
- [PCA9685 数据手册](https://www.nxp.com/docs/en/data-sheet/PCA9685.pdf)
|
||||||
|
- [MPU6050 数据手册](https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Datasheet1.pdf)
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")" || exit
|
||||||
|
export cjStackSize=64MB
|
||||||
|
export cjHeapSize=64MB
|
||||||
|
exec cjpm run --run-args="$@"
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")" || exit
|
||||||
|
export cjHeapSize=16MB
|
||||||
|
cjpm update
|
||||||
|
exec cjpm build
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
[package]
|
||||||
|
cjc-version = "1.1.3"
|
||||||
|
name = "bot4cj"
|
||||||
|
description = "nothing here"
|
||||||
|
version = "0.0.3"
|
||||||
|
target-dir = ""
|
||||||
|
src-dir = ""
|
||||||
|
output-type = "executable"
|
||||||
|
compile-option = "-l c -l pthread -l lua" # Linux
|
||||||
|
override-compile-option = ""
|
||||||
|
link-option = ""
|
||||||
|
package-configuration = {}
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
#ctype4cj = { git = "https://gitcode.com/qingss0/ctype4cj.git", branch="main" }
|
||||||
|
ctype4cj = "1.0.3"
|
||||||
|
#pthread4cj = { git = "https://gitcode.com/qingss0/pthread4cj.git", branch="main"}
|
||||||
|
pthread4cj = "1.0.0"
|
||||||
|
#spi4cj = { git = "https://gitcode.com/qingss0/spi4cj.git", branch="main"}
|
||||||
|
spi4cj = "1.0.0"
|
||||||
|
#lua4cj = { git = "https://gitcode.com/qingss0/lua4cj.git", branch="main" }
|
||||||
|
lua4cj = "1.0.0"
|
||||||
|
# 这种x86机器人开发板感觉不经常使用,我没用过,不知道里面的i2c调用是不是参数是一样的
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
[target.x86_64-unknown-linux-gnu.dependencies]
|
||||||
|
[target.x86_64-unknown-linux-gnu.bin-dependencies]
|
||||||
|
path-option = ["${CANGJIE_STDX_PATH}/linux_x86_64_cjnative/static/stdx"]
|
||||||
|
|
||||||
|
[target.aarch64-unknown-linux-gnu]
|
||||||
|
[target.aarch64-unknown-linux-gnu.dependencies]
|
||||||
|
[target.aarch64-unknown-linux-gnu.bin-dependencies]
|
||||||
|
path-option = ["${CANGJIE_STDX_PATH}/linux_aarch64_cjnative/static/stdx"]
|
||||||
|
|
||||||
|
# 为什么不支持Windows了?
|
||||||
|
# 因为新版我们加载了Linux的glibc,通常Windows中没有这个
|
||||||
|
#[target.x86_64-w64-mingw32]
|
||||||
|
# [target.x86_64-w64-mingw32.bin-dependencies]
|
||||||
|
# path-option = ["${CANGJIE_STDX_PATH}\\windows_x86_64_cjnative\\dynamic\\stdx"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
nvcc -shared -o libmt4cj.dll src/cuda/matrix.cu -allow-unsupported-compiler
|
||||||
|
#nvcc -shared -o libmt4cj.dll src/cpp/Matrix.cpp -allow-unsupported-compiler
|
||||||
|
nvcc -shared -o libi2c4cj.dll src/cpp/I2C.c -allow-unsupported-compiler
|
||||||
|
nvcc -shared -o libfft4cj.dll src/cpp/fft.cpp src/cpp/Matrix.cpp src/cpp/fft1.c -allow-unsupported-compiler
|
||||||
|
nvcc -shared -o conv.lib -c src/cpp/cnn/conv.cpp -allow-unsupported-compiler
|
||||||
|
nvcc -shared -o imgl.lib -c src/cpp/cnn/imgl.cpp -allow-unsupported-compiler
|
||||||
|
nvcc -shared -o pooling.lib -c src/cpp/cnn/pooling.cpp -allow-unsupported-compiler
|
||||||
|
nvcc -shared -o ga_pooling.lib -c src/cpp/cnn/ga_pooling.cpp -allow-unsupported-compiler
|
||||||
|
nvcc -shared -o flatten.lib -c src/cpp/cnn/flatten.cpp -allow-unsupported-compiler
|
||||||
|
nvcc -shared --std=c++17 -o libcnn4cj.dll src/cpp/cnn/cnn_i.cpp conv.lib imgl.lib pooling.lib ga_pooling.lib flatten.lib -allow-unsupported-compiler
|
||||||
|
nvcc -shared -o libsnd4cj.dll src/cpp/snd.cpp -allow-unsupported-compiler
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
-- ADS1115模数转换器 硬件脚本(测试)
|
||||||
|
|
||||||
|
|
||||||
|
-- 这个函数是辅助用的,它能够打印一张表出来
|
||||||
|
function print_table(t, indent)
|
||||||
|
indent = indent or 0
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
if type(v) == 'table' then
|
||||||
|
print(string.rep(' ', indent) .. k .. ":")
|
||||||
|
print_table(v, indent + 2)
|
||||||
|
else
|
||||||
|
print(string.rep(' ', indent) .. k .. ": " .. tostring(v))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ADS1115_ADDR = 0x48 -- ADS1115的I2C地址
|
||||||
|
ADS1115_REG_CONVERSION = 0x00 -- 转换结果(只读)
|
||||||
|
ADS1115_REG_CONFIG = 0x01 -- 配置寄存器(读/写)
|
||||||
|
ADS1115_REG_LO_THRESH = 0x02 -- 低阈值比较
|
||||||
|
ADS1115_REG_HI_THRESH = 0x03 -- 高阈值比较
|
||||||
|
-- ====================================
|
||||||
|
-- ADS1115 配置寄存器 bit 定义(16位)
|
||||||
|
-- ====================================
|
||||||
|
-- BIT15: OS[0] => 单次转换启动位(1=开始)
|
||||||
|
-- BIT14-12: MUX[2:0] => 输入多路选择
|
||||||
|
-- BIT11-9: PGA[2:0] => 可编程增益放大器
|
||||||
|
-- BIT8: MODE => 0=连续转换,1=单次(省电)
|
||||||
|
-- BIT7-5: DR[2:0] => 数据速率
|
||||||
|
-- BIT4: COMP_MODE => 比较模式(0=传统)
|
||||||
|
-- BIT3: COMP_POL => 比较极性(0=低有效)
|
||||||
|
-- BIT2: COMP_LAT => 比较锁存(0=不锁存)
|
||||||
|
-- BIT1-0: COMP_QUE => 比较器队列(11=禁用)
|
||||||
|
--
|
||||||
|
-- ==== MUX 输入通道 ====
|
||||||
|
ADS1115_MUX_AIN0_AIN1 = 0x0000 -- 差分:P=AIN0,N=AIN1(默认)
|
||||||
|
ADS1115_MUX_AIN0_AIN3 = 0x1000 -- 差分:P=AIN0,N=AIN3
|
||||||
|
ADS1115_MUX_AIN1_AIN3 = 0x2000 -- 差分:P=AIN1,N=AIN3
|
||||||
|
ADS1115_MUX_AIN2_AIN3 = 0x3000 -- 差分:P=AIN2,N=AIN3
|
||||||
|
ADS1115_MUX_AIN0_GND = 0x4000 -- 单端:AIN0
|
||||||
|
ADS1115_MUX_AIN1_GND = 0x5000 -- 单端:AIN1
|
||||||
|
ADS1115_MUX_AIN2_GND = 0x6000 -- 单端:AIN2
|
||||||
|
ADS1115_MUX_AIN3_GND = 0x7000 -- 单端:AIN3
|
||||||
|
|
||||||
|
-- ==== PGA 增益(满量程范围)====
|
||||||
|
ADS1115_PGA_6_144V = 0x0000 -- ±6.144V (1LSB=0.1875mV)
|
||||||
|
ADS1115_PGA_4_096V = 0x0200 -- ±4.096V (1LSB=0.125mV)
|
||||||
|
ADS1115_PGA_2_048V = 0x0400 -- ±2.048V (1LSB=0.0625mV)(默认)
|
||||||
|
ADS1115_PGA_1_024V = 0x0600 -- ±1.024V (1LSB=0.03125mV)
|
||||||
|
ADS1115_PGA_0_512V = 0x0800 -- ±0.512V (1LSB=0.015625mV)
|
||||||
|
ADS1115_PGA_0_256V = 0x0A00 -- ±0.256V (1LSB=0.0078125mV)
|
||||||
|
|
||||||
|
-- ==== 数据速率 ====
|
||||||
|
ADS1115_DR_8SPS = 0x0000 -- 8 SPS
|
||||||
|
ADS1115_DR_16SPS = 0x0020 -- 16 SPS
|
||||||
|
ADS1115_DR_32SPS = 0x0040 -- 32 SPS
|
||||||
|
ADS1115_DR_64SPS = 0x0060 -- 64 SPS
|
||||||
|
ADS1115_DR_128SPS = 0x0080 -- 128 SPS (默认)
|
||||||
|
ADS1115_DR_250SPS = 0x00A0 -- 250 SPS
|
||||||
|
ADS1115_DR_475SPS = 0x00C0 -- 475 SPS
|
||||||
|
ADS1115_DR_860SPS = 0x00E0 -- 860 SPS
|
||||||
|
|
||||||
|
-- ==== 比较器队列(COMP_QUE)====
|
||||||
|
ADS1115_COMP_QUE_1 = 0x0000 -- 1次后触发
|
||||||
|
ADS1115_COMP_QUE_2 = 0x0001 -- 2次后触发
|
||||||
|
ADS1115_COMP_QUE_4 = 0x0002 -- 4次后触发
|
||||||
|
ADS1115_COMP_QUE_DISABLE = 0x0003 -- 禁用比较器(默认)
|
||||||
|
|
||||||
|
-- ==== 操作模式 ====
|
||||||
|
ADS1115_MODE_CONTINUOUS = 0x0000 -- 连续转换
|
||||||
|
ADS1115_MODE_SINGLE = 0x0100 -- 单次转换(默认,省电)
|
||||||
|
|
||||||
|
-- ==== 启动转换 ====
|
||||||
|
ADS1115_OS_START = 0x8000 -- 启动单次转换
|
||||||
|
|
||||||
|
-- ADS1115 配置上下文
|
||||||
|
ads1115_t = {
|
||||||
|
addr = 0x00,
|
||||||
|
config = 0x00, -- 16bits
|
||||||
|
fs_valtage = 0.0 -- 满量程电压(取决于PGA)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- PGA对应的满量程电压(单位:V)
|
||||||
|
pga_fs = {
|
||||||
|
6.144, -- ADS1115_PGA_6_144V [0]
|
||||||
|
4.096, -- ADS1115_PGA_4_096V [1]
|
||||||
|
2.048, -- ADS1115_PGA_2_048V [2]
|
||||||
|
1.024, -- ADS1115_PGA_1_024V [3]
|
||||||
|
0.512, -- ADS1115_PGA_0_512V [4]
|
||||||
|
0.256 -- ADS1115_PGA_0_256V [5]
|
||||||
|
}
|
||||||
|
|
||||||
|
-- PGA 增益对应的满量程电压名
|
||||||
|
pga_names = {
|
||||||
|
"±6.144V", -- [0]
|
||||||
|
"±4.096V", -- [1]
|
||||||
|
"±2.048V", -- [2]
|
||||||
|
"±1.024V", -- [3]
|
||||||
|
"±0.512V", -- [4]
|
||||||
|
"±0.256V" -- [5]
|
||||||
|
}
|
||||||
|
|
||||||
|
-- 数据速率字符串
|
||||||
|
dr_names = {
|
||||||
|
"8 SPS", -- [0]
|
||||||
|
"16 SPS", -- [1]
|
||||||
|
"32 SPS", -- [2]
|
||||||
|
"64 SPS", -- [3]
|
||||||
|
"128 SPS", -- [4]
|
||||||
|
"250 SPS", -- [5]
|
||||||
|
"475 SPS", -- [6]
|
||||||
|
"860 SPS" -- [7]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- I2C写2字节(大端)
|
||||||
|
function i2c_write_word(fd, reg, val)
|
||||||
|
i2c_write_reg(fd, reg + 0, (val >> 8) | 0xFF)
|
||||||
|
i2c_write_reg(fd, reg + 1, val & 0xFF)
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- 【配置脚本】
|
||||||
|
-- FIXME(20260602-qing): 异常处理
|
||||||
|
function build(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- Lua builder func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print("----------------------------------------------")
|
||||||
|
local fd = data.fd
|
||||||
|
|
||||||
|
-- 检查文件句柄参数是否存在
|
||||||
|
if fd == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: fd"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local addr = data.address
|
||||||
|
|
||||||
|
-- 检查地址参数是否存在
|
||||||
|
if addr == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: addr"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
addr = tonumber(addr)
|
||||||
|
|
||||||
|
-- 写进配置表
|
||||||
|
ads1115.addr = addr
|
||||||
|
|
||||||
|
|
||||||
|
-- 开始配置PCA9685
|
||||||
|
i2c_set_addr(fd, addr)
|
||||||
|
|
||||||
|
-- 重启PCA9685
|
||||||
|
i2c_write_reg(fd, 0x00, 0x80)
|
||||||
|
|
||||||
|
-- 等待振荡器启动
|
||||||
|
milli_sleep(5)
|
||||||
|
|
||||||
|
i2c_write_reg(fd, 0x00, 0x10) -- 进入SLEEP模式(允许改频率)
|
||||||
|
i2c_write_reg(fd, 0xFE, math.floor(prescale))
|
||||||
|
i2c_write_reg(fd, 0x00, 0x20) -- 退出SLEEP模式
|
||||||
|
|
||||||
|
-- 等待振荡器启动
|
||||||
|
milli_sleep(5)
|
||||||
|
|
||||||
|
-- 返回成功信号
|
||||||
|
return {
|
||||||
|
result = "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 【销毁PCA9685的脚本】
|
||||||
|
-- 重置PCA9685
|
||||||
|
-- NOTE(20260612-qing): 其实可以不需要,因为我们只是清空设置
|
||||||
|
function destroy(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- Lua ender func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print("--------------------------------------------")
|
||||||
|
|
||||||
|
local fd = data.fd
|
||||||
|
|
||||||
|
-- 检查fd参数是否存在
|
||||||
|
if fd == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: fd"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local addr = data.address
|
||||||
|
|
||||||
|
-- 检查address参数是否存在
|
||||||
|
if addr == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: addr"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
addr = tonumber(addr)
|
||||||
|
|
||||||
|
-- 准备配置
|
||||||
|
i2c_set_addr(fd, addr)
|
||||||
|
|
||||||
|
-- 重启PCA9685
|
||||||
|
i2c_write_reg(fd, 0x00, 0x80)
|
||||||
|
|
||||||
|
-- 返回成功信号
|
||||||
|
return {
|
||||||
|
result = "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- 【调用PCA9685】
|
||||||
|
-- 将一个舵机通道转动到一个角度
|
||||||
|
-- NOTE(20260612-qing): 接收一个参数(表),返回一个结果(表)
|
||||||
|
function call(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- Lua caller func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print("---------------------------------------------")
|
||||||
|
|
||||||
|
|
||||||
|
local fd = data.fd
|
||||||
|
|
||||||
|
-- 检查fd参数是否存在
|
||||||
|
if fd == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Argument: fd"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local addr = data.address
|
||||||
|
|
||||||
|
-- 检查address参数是否存在
|
||||||
|
if addr == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Argument: addr"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
addr = tonumber(addr)
|
||||||
|
|
||||||
|
local channel = data.channel
|
||||||
|
|
||||||
|
-- 检查channel参数是否存在
|
||||||
|
if channel == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Argument: channel"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local angle = data.angle
|
||||||
|
|
||||||
|
-- 检查angle参数是否存在
|
||||||
|
if angle == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Argument: angle"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print(string.format("Lua hardware drive set angle: %d", angle))
|
||||||
|
|
||||||
|
-- 设置I2C地址
|
||||||
|
i2c_set_addr(fd, addr)
|
||||||
|
|
||||||
|
-- 计算需要的参数
|
||||||
|
local min = 0.5 -- 0度对应0.5ms
|
||||||
|
local max = 2.5 -- 180度对应2.5ms
|
||||||
|
local width = min + (max - min) * (angle / 180.0)
|
||||||
|
local offval = math.floor(width * 4096.0 / 20.0)
|
||||||
|
local reg = 0x06 + 4 * channel
|
||||||
|
|
||||||
|
-- 写I2C寄存器
|
||||||
|
i2c_write_reg(fd, reg + 0, (0 >> 0) & 0xFF) -- 1 * 8
|
||||||
|
i2c_write_reg(fd, reg + 1, (0 >> 8) & 0xFF) -- 2 * 8
|
||||||
|
i2c_write_reg(fd, reg + 2, (offval >> 0) & 0xFF) -- 3 * 8
|
||||||
|
i2c_write_reg(fd, reg + 3, (offval >> 8) & 0xFF) -- 4 * 8
|
||||||
|
|
||||||
|
-- 返回字典
|
||||||
|
return {
|
||||||
|
result = "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- ADS1256 模数转换器 - SPI接口
|
||||||
|
|
||||||
|
function build(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
function destroy(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
function call(data)
|
||||||
|
end
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
-- PCA9685 PWM控制板 硬件脚本(测试)
|
||||||
|
|
||||||
|
|
||||||
|
-- 这个函数是辅助用的,它能够打印一张表出来
|
||||||
|
function print_table(t, indent)
|
||||||
|
indent = indent or 0
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
if type(v) == 'table' then
|
||||||
|
print(string.rep(' ', indent) .. k .. ":")
|
||||||
|
print_table(v, indent + 2)
|
||||||
|
else
|
||||||
|
print(string.rep(' ', indent) .. k .. ": " .. tostring(v))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- 【配置脚本】
|
||||||
|
-- 该脚本负责初始化PCA9685
|
||||||
|
-- FIXME(20260602-qing): 异常处理
|
||||||
|
function build(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- Lua builder func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print("----------------------------------------------")
|
||||||
|
local fd = data.fd
|
||||||
|
|
||||||
|
-- 检查文件句柄参数是否存在
|
||||||
|
if fd == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: fd"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local addr = data.address
|
||||||
|
|
||||||
|
-- 检查地址参数是否存在
|
||||||
|
if addr == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: addr"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
addr = tonumber(addr)
|
||||||
|
|
||||||
|
-- 设置PWM频率
|
||||||
|
local freq = 50.0
|
||||||
|
local prescale_val = 25000000.0 / (4096.0 * freq) - 1.0 -- 25MHz主频
|
||||||
|
local prescale = prescale_val + 0.5
|
||||||
|
|
||||||
|
-- 开始配置PCA9685
|
||||||
|
i2c_set_addr(fd, addr)
|
||||||
|
|
||||||
|
-- 重启PCA9685
|
||||||
|
i2c_write_reg(fd, 0x00, 0x80)
|
||||||
|
|
||||||
|
-- 等待振荡器启动
|
||||||
|
milli_sleep(5)
|
||||||
|
|
||||||
|
i2c_write_reg(fd, 0x00, 0x10) -- 进入SLEEP模式(允许改频率)
|
||||||
|
i2c_write_reg(fd, 0xFE, math.floor(prescale))
|
||||||
|
i2c_write_reg(fd, 0x00, 0x20) -- 退出SLEEP模式
|
||||||
|
|
||||||
|
-- 等待振荡器启动
|
||||||
|
milli_sleep(5)
|
||||||
|
|
||||||
|
-- 返回成功信号
|
||||||
|
return {
|
||||||
|
result = "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 【销毁PCA9685的脚本】
|
||||||
|
-- 重置PCA9685
|
||||||
|
-- NOTE(20260612-qing): 其实可以不需要,因为我们只是清空设置
|
||||||
|
function destroy(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- Lua ender func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print("--------------------------------------------")
|
||||||
|
|
||||||
|
local fd = data.fd
|
||||||
|
|
||||||
|
-- 检查fd参数是否存在
|
||||||
|
if fd == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: fd"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local addr = data.address
|
||||||
|
|
||||||
|
-- 检查address参数是否存在
|
||||||
|
if addr == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: addr"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
addr = tonumber(addr)
|
||||||
|
|
||||||
|
-- 准备配置
|
||||||
|
i2c_set_addr(fd, addr)
|
||||||
|
|
||||||
|
-- 重启PCA9685
|
||||||
|
i2c_write_reg(fd, 0x00, 0x80)
|
||||||
|
|
||||||
|
-- 返回成功信号
|
||||||
|
return {
|
||||||
|
result = "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- 【调用PCA9685】
|
||||||
|
-- 将一个舵机通道转动到一个角度
|
||||||
|
-- NOTE(20260612-qing): 接收一个参数(表),返回一个结果(表)
|
||||||
|
function call(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- Lua caller func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print("---------------------------------------------")
|
||||||
|
|
||||||
|
|
||||||
|
local fd = data.fd
|
||||||
|
|
||||||
|
-- 检查fd参数是否存在
|
||||||
|
if fd == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Argument: fd"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local addr = data.address
|
||||||
|
|
||||||
|
-- 检查address参数是否存在
|
||||||
|
if addr == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Argument: addr"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
addr = tonumber(addr)
|
||||||
|
|
||||||
|
local channel = data.channel
|
||||||
|
|
||||||
|
-- 检查channel参数是否存在
|
||||||
|
if channel == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Argument: channel"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local angle = data.angle
|
||||||
|
|
||||||
|
-- 检查angle参数是否存在
|
||||||
|
if angle == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Argument: angle"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print(string.format("Lua hardware drive set angle: %d", angle))
|
||||||
|
|
||||||
|
-- 设置I2C地址
|
||||||
|
i2c_set_addr(fd, addr)
|
||||||
|
|
||||||
|
-- 计算需要的参数
|
||||||
|
local min = 0.5 -- 0度对应0.5ms
|
||||||
|
local max = 2.5 -- 180度对应2.5ms
|
||||||
|
local width = min + (max - min) * (angle / 180.0)
|
||||||
|
local offval = math.floor(width * 4096.0 / 20.0)
|
||||||
|
local reg = 0x06 + 4 * channel
|
||||||
|
|
||||||
|
-- 写I2C寄存器
|
||||||
|
i2c_write_reg(fd, reg + 0, (0 >> 0) & 0xFF) -- 1 * 8
|
||||||
|
i2c_write_reg(fd, reg + 1, (0 >> 8) & 0xFF) -- 2 * 8
|
||||||
|
i2c_write_reg(fd, reg + 2, (offval >> 0) & 0xFF) -- 3 * 8
|
||||||
|
i2c_write_reg(fd, reg + 3, (offval >> 8) & 0xFF) -- 4 * 8
|
||||||
|
|
||||||
|
-- 返回字典
|
||||||
|
return {
|
||||||
|
result = "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
-- SPI 硬件
|
||||||
|
|
||||||
|
--
|
||||||
|
SPI_SUCCESS = 0
|
||||||
|
SPI_FAILED = 1
|
||||||
|
|
||||||
|
-- 全局文件句柄
|
||||||
|
fd = 0
|
||||||
|
|
||||||
|
-- 这个函数是辅助用的,它能够打印一张表出来
|
||||||
|
function print_table(t, indent)
|
||||||
|
indent = indent or 0
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
if type(v) == 'table' then
|
||||||
|
print(string.rep(' ', indent) .. k .. ":")
|
||||||
|
print_table(v, indent + 2)
|
||||||
|
else
|
||||||
|
print(string.rep(' ', indent) .. k .. ": " .. tostring(v))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function build(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- SPI.Lua builder func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print("--------------------------------------------------")
|
||||||
|
local path = data.path
|
||||||
|
|
||||||
|
-- 检查文件句柄参数是否存在
|
||||||
|
if path == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: path"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 模式
|
||||||
|
local spi_mode = data.spi_mode
|
||||||
|
if spi_mode == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: spi_mode"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
print(spi_mode)
|
||||||
|
spi_mode = tonumber(spi_mode)
|
||||||
|
|
||||||
|
-- 比特
|
||||||
|
local bits = data.bits
|
||||||
|
if bits == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: bits"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
bits = tonumber(bits)
|
||||||
|
|
||||||
|
|
||||||
|
-- 速率
|
||||||
|
local speed = data.speed
|
||||||
|
if speed == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: speed"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
speed = tonumber(speed)
|
||||||
|
|
||||||
|
--
|
||||||
|
local delay = data.delay
|
||||||
|
if delay == nil then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: delay"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
delay = tonumber(delay)
|
||||||
|
|
||||||
|
-- 开始执行
|
||||||
|
fd = spi_open(path)
|
||||||
|
if fd <= 0 then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Failed to open SPI file"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local res = spi_setup(fd, spi_mode, bits, speed, delay)
|
||||||
|
if res ~= 0 then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Failed to setup SPI device"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
result = "OK",
|
||||||
|
fd = fd
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function destroy(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- SPI.Lua ender func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print(string.format("now fd: %i", fd))
|
||||||
|
print("------------------------------------------------")
|
||||||
|
|
||||||
|
spi_close(fd)
|
||||||
|
fd = -1
|
||||||
|
|
||||||
|
return {
|
||||||
|
result = "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function call(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- SPI.Lua caller func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print("-------------------------------------------------")
|
||||||
|
|
||||||
|
return {
|
||||||
|
fd = fd
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
-- PCA9685 PWM控制板 硬件脚本(测试)
|
||||||
|
|
||||||
|
-- 这个函数是辅助用的,它能够打印一张表出来
|
||||||
|
function print_table(t, indent)
|
||||||
|
indent = indent or 0
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
if type(v) == 'table' then
|
||||||
|
print(string.rep(' ', indent) .. k .. ":")
|
||||||
|
print_table(v, indent + 2)
|
||||||
|
else
|
||||||
|
print(string.rep(' ', indent) .. k .. ": " .. tostring(v))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- 【配置脚本】
|
||||||
|
function build(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- Lua builder func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print("----------------------------------------------")
|
||||||
|
|
||||||
|
-- 返回成功信号
|
||||||
|
return {
|
||||||
|
result = "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 【销毁用的脚本】
|
||||||
|
function destroy(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- Lua ender func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print("--------------------------------------------")
|
||||||
|
|
||||||
|
|
||||||
|
-- 返回成功信号
|
||||||
|
return {
|
||||||
|
result = "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- 【调用的脚本】
|
||||||
|
function call(data)
|
||||||
|
|
||||||
|
-- 检查该参数是否是一张表
|
||||||
|
if type(data) ~= "table" then
|
||||||
|
return {
|
||||||
|
result = "ERR",
|
||||||
|
message = "Invalid Arguments: table"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
print("-------- Lua caller func get a table --------")
|
||||||
|
print_table(data)
|
||||||
|
print("---------------------------------------------")
|
||||||
|
|
||||||
|
-- 返回字典
|
||||||
|
return {
|
||||||
|
result = "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
cjpm run --run-args="$($args -join ' ')"
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@MPU6050_BIND
|
||||||
|
name I2C addr;
|
||||||
|
#mpu6050-0 i2c-1 0x69;
|
||||||
|
#mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
- 10 - 10 - 10 - 10 - - - 20;
|
||||||
|
- 20 - 20 - 20 - 20 - - - 20;
|
||||||
|
- 30 - 30 - 30 - 30 - - - 20;
|
||||||
|
45 0 45 0 45 0 45 0 90 90 10 200; # stand
|
||||||
|
@
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@MPU6050_BIND
|
||||||
|
name I2C addr;
|
||||||
|
#mpu6050-0 i2c-1 0x69;
|
||||||
|
#mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
- - - - - - - - - - 400 200;
|
||||||
|
0 - - - - - - 150 - - 1000 1000;
|
||||||
|
45 - - - - - - 0 - - 10 20;
|
||||||
|
@
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@MPU6050_BIND
|
||||||
|
name I2C addr;
|
||||||
|
#mpu6050-0 i2c-1 0x69;
|
||||||
|
#mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
- 45 - - - 45 - - - - - 100;
|
||||||
|
90 - - - 90 - - - - - - 50; # stand
|
||||||
|
#
|
||||||
|
- 0 - - - 0 - - - - - 50;
|
||||||
|
- - - 45 - - - 45 - - - 100;
|
||||||
|
45 - - - 45 - - - - - - 10;
|
||||||
|
- - 0 - - - 0 - - - - 50;
|
||||||
|
#
|
||||||
|
- - - 0 - - - 0 - - - 50;
|
||||||
|
- 45 - - - 45 - - - - - 100;
|
||||||
|
- - 45 - - - 45 - - - - 10;
|
||||||
|
- 0 - - - 0 - - - - - 50;
|
||||||
|
@
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# ONE STEP
|
||||||
|
#
|
||||||
|
# legs | neck | free | speaker | delay
|
||||||
|
~s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 - - - - - - spk delay; # column name
|
||||||
|
!90 180 -90 -180 90 180 -90 -180 180 180 - - - - - - 5000 1000; # 4-legs spider robot
|
||||||
|
!- - - - - - - - - 90 - - - - - - - -; # 4-legs spider robot
|
||||||
|
!- - - - - - - - - - - - - - - - - -; # offset
|
||||||
|
45 - 45 - 45 - 45 - 90 90 - - - - - - 1000 100; # stand
|
||||||
|
- - - - - - - - - - - - - - - - 1000 -; # stand
|
||||||
|
|
||||||
|
- - - 30 - - - 30 - - - - - - - - 0 50; # right behind up & left front up
|
||||||
|
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
|
||||||
|
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
|
||||||
|
0 - 90 - 90 - 0 - - - - - - - - - - -; # right front forward & left behind forward
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -; # right front down & left behind down
|
||||||
|
|
||||||
|
90 - - 30 40 - - 30 - - - - - - - - - -; # right behind up & left front up
|
||||||
|
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
|
||||||
|
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
|
||||||
|
0 - 90 - 90 - 0 - - - - - - - - - - -; #
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -; #
|
||||||
|
|
||||||
|
90 - - 30 40 - - 30 - - - - - - - - - -; # right behind up & left front up
|
||||||
|
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
|
||||||
|
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
|
||||||
|
0 - 90 - 90 - 0 - - - - - - - - - - -; #
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -; #
|
||||||
|
|
||||||
|
90 - - 30 40 - - 30 - - - - - - - - - -; # right behind up & left front up
|
||||||
|
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
|
||||||
|
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
|
||||||
|
0 - 90 - 90 - 0 - - - - - - - - - - -; #
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -; #
|
||||||
|
|
||||||
|
90 - - 30 40 - - 30 - - - - - - - - - -; # right behind up & left front up
|
||||||
|
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
|
||||||
|
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
|
||||||
|
0 - 90 - 90 - 0 - - - - - - - - - - -; #
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -; #
|
||||||
|
|
||||||
|
90 - - 30 40 - - 30 - - - - - - - - - -; # right behind up & left front up
|
||||||
|
- - 40 - - - 90 - - - - - - - - - - -; # right behind forward & left front forward
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -; # right behind down & left front down
|
||||||
|
- 30 - - - 30 - - - - - - - - - - - -; # right front up & left behind up
|
||||||
|
45 - 45 - 45 - 45 - - - - - - - - - - -; # right front forward & left behind forward
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -; # right front down & left behind down
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@MPU6050_BIND
|
||||||
|
name I2C addr;
|
||||||
|
#mpu6050-0 i2c-1 0x69;
|
||||||
|
#mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
- 10 - 10 - 10 - 10 - - - 20;
|
||||||
|
- 15 - 15 - 15 - 15 - - - 20;
|
||||||
|
- 20 - 20 - 20 - 20 - - - 20;
|
||||||
|
- 25 - 25 - 25 - 25 - - - 20;
|
||||||
|
- 30 - 30 - 30 - 30 - - - 20;
|
||||||
|
@
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# SNNEAK
|
||||||
|
# legs | neck | free | delay
|
||||||
|
~s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 - - - - - - spk delay; # column name
|
||||||
|
!90 180 -90 -180 90 180 -90 -180 180 180 - - - - - - 5000 1000; # 4-legs spider robot
|
||||||
|
! - - - - - - - - - - - - - - - - - -; # 4-legs spider robot
|
||||||
|
! - - - - - - - - - - - - - - - - - -10; # offset
|
||||||
|
# 俯卧撑
|
||||||
|
#
|
||||||
|
90 0 90 0 90 0 90 0 90 90 - - - - - - 1000 200;
|
||||||
|
- 5 - 5 - 5 - 5 - - - - - - - - 0 50;
|
||||||
|
- 10 - 10 - 10 - 10 - - - - - - - - - -;
|
||||||
|
- 15 - 15 - 15 - 15 - - - - - - - - - -;
|
||||||
|
- 20 - 20 - 20 - 20 - - - - - - - - - -;
|
||||||
|
- 25 - 25 - 25 - 25 - - - - - - - - - -;
|
||||||
|
- 30 - 30 - 30 - 30 - - - - - - - - - -;
|
||||||
|
- 35 - 35 - 35 - 35 - - - - - - - - - -;
|
||||||
|
|
||||||
|
#r
|
||||||
|
- 35 - 35 - 35 - 35 - - - - - - - - 1000 200;
|
||||||
|
- 30 - 30 - 30 - 30 - - - - - - - - 0 50;
|
||||||
|
- 25 - 25 - 25 - 25 - - - - - - - - - -;
|
||||||
|
- 20 - 20 - 20 - 20 - - - - - - - - - -;
|
||||||
|
- 15 - 15 - 15 - 15 - - - - - - - - - -;
|
||||||
|
- 10 - 10 - 10 - 10 - - - - - - - - - -;
|
||||||
|
- 5 - 5 - 5 - 5 - - - - - - - - - -;
|
||||||
|
- 0 - 0 - 0 - 0 - - - - - - - - - -;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@MPU6050_BIND
|
||||||
|
name I2C addr;
|
||||||
|
#mpu6050-0 i2c-1 0x69;
|
||||||
|
#mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
- - - - - 0 - - - - 400 200;
|
||||||
|
- - 0 - - 150 - - - - 1000 1000;
|
||||||
|
- - 45 - - 0 - - - - 10 20;
|
||||||
|
@
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# ONE STEP
|
||||||
|
# legs | neck | free | delay
|
||||||
|
~s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 - - - - - - spk delay; # column name
|
||||||
|
!90 180 -90 -180 90 180 -90 -180 180 180 - - - - - - 5000 1000; # 4-legs spider robot
|
||||||
|
! - - - - - - - - - - - - - - - - - -; # 4-legs spider robot
|
||||||
|
! - - - - - - - - - - - - - - - - - -; # offset
|
||||||
|
|
||||||
|
|
||||||
|
45 - 45 - 45 - 45 - 90 90 - - - - - - 1000 100; # stand
|
||||||
|
- - - - - - - - - - - - - - - - - -; # stand
|
||||||
|
|
||||||
|
#----------------------------------------------------------------
|
||||||
|
- 30 - - - 25 40 - - - - - - - - - 0 50;
|
||||||
|
0 - - - 90 - - - - - - - - - - - - -;
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -;
|
||||||
|
- - - 25 - - - 30 - - - - - - - - - -;
|
||||||
|
50 - 0 - 0 - 90 - - - - - - - - - - -;
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -;
|
||||||
|
|
||||||
|
#----------------------------------------------------------------
|
||||||
|
|
||||||
|
- 30 90 - - 25 40 - - - - - - - - - - -;
|
||||||
|
0 - - - 90 - - - - - - - - - - - - -;
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -;
|
||||||
|
- - - 25 - - - 30 - - - - - - - - - -;
|
||||||
|
50 - 0 - 0 - 90 - - - - - - - - - - -;
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -;
|
||||||
|
|
||||||
|
- 30 90 - - 25 40 - - - - - - - - - - -;
|
||||||
|
0 - - - 90 - - - - - - - - - - - - -;
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -;
|
||||||
|
- - - 25 - - - 30 - - - - - - - - - -;
|
||||||
|
50 - 0 - 0 - 90 - - - - - - - - - - -;
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -;
|
||||||
|
|
||||||
|
- 30 90 - - 25 40 - - - - - - - - - - -;
|
||||||
|
0 - - - 90 - - - - - - - - - - - - -;
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -;
|
||||||
|
- - - 25 - - - 30 - - - - - - - - - -;
|
||||||
|
50 - 0 - 0 - 90 - - - - - - - - - - -;
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -;
|
||||||
|
|
||||||
|
- 30 90 - - 25 40 - - - - - - - - - - -;
|
||||||
|
0 - - - 90 - - - - - - - - - - - - -;
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -;
|
||||||
|
- - - 25 - - - 30 - - - - - - - - - -;
|
||||||
|
50 - 0 - 0 - 90 - - - - - - - - - - -;
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -;
|
||||||
|
|
||||||
|
#----------------------------------------------------------------
|
||||||
|
- 30 90 - - 25 40 - - - - - - - - - - -;
|
||||||
|
0 - - - 90 - - - - - - - - - - - - -;
|
||||||
|
- 0 - - - 0 - - - - - - - - - - - -;
|
||||||
|
- - - 25 - - - 30 - - - - - - - - - -;
|
||||||
|
45 - 45 - 45 - 45 - - - - - - - - - - -;
|
||||||
|
- - - 0 - - - 0 - - - - - - - - - -;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@MPU6050_BIND
|
||||||
|
name I2C addr;
|
||||||
|
#mpu6050-0 i2c-1 0x69;
|
||||||
|
#mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
@
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@MPU6050_BIND
|
||||||
|
name I2C addr;
|
||||||
|
#mpu6050-0 i2c-1 0x69;
|
||||||
|
#mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
- 10 - 10 - 10 - 10 - - - 50;
|
||||||
|
- 20 - 20 - 20 - 20 - - - -;
|
||||||
|
- 30 - 30 - 30 - 30 - - - -;
|
||||||
|
- 20 - 20 - 20 - 20 - - - -;
|
||||||
|
- 10 - 10 - 10 - 10 - - - -;
|
||||||
|
- 0 - 0 - 0 - 0 - - - 200; # stand
|
||||||
|
@
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
#i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
90 - - - - - - - - - - 20; # left-behind
|
||||||
|
|
||||||
|
45 - - - - - - - - - 0 500; # stand
|
||||||
|
- - 90 - - - 45 - - - - 20; # right-behind
|
||||||
|
|
||||||
|
- - 45 - - - - - - 45 200 500; # stand
|
||||||
|
- - - - 90 - - - - - 0 20; # right-front
|
||||||
|
|
||||||
|
- - - - 45 - - - - - - 500; # stand
|
||||||
|
- - - - - - 90 - - - - 20; # left-front
|
||||||
|
|
||||||
|
- - - - - - 45 - - - - 500; # stand
|
||||||
|
- - - - - - - - - 90 - 20; # stand
|
||||||
|
@
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 无硬件情况下的测试脚本
|
||||||
|
|
||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@TEST
|
||||||
|
name parameter0 patameter1;
|
||||||
|
t0 p0 p1;
|
||||||
|
t1 p2 p3;
|
||||||
|
@
|
||||||
|
|
||||||
|
|
||||||
|
@Action
|
||||||
|
t0.A t1.F;
|
||||||
|
10 20;
|
||||||
|
@
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@MPU6050_BIND
|
||||||
|
name I2C addr;
|
||||||
|
#mpu6050-0 i2c-1 0x69;
|
||||||
|
#mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
- 10 - 10 - - - - - - - 50;
|
||||||
|
- 20 - 20 - - - - - - - -;
|
||||||
|
- 30 - 30 - - - - - - - -;
|
||||||
|
- 20 - 20 - - - - - - - -;
|
||||||
|
- 10 - 10 - - - - - - - -;
|
||||||
|
- 0 - 0 - - - - - - - 200; # stand
|
||||||
|
@
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@MPU6050_BIND
|
||||||
|
name I2C addr;
|
||||||
|
#mpu6050-0 i2c-1 0x69;
|
||||||
|
#mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
- - - - - 10 - 10 - - - 50;
|
||||||
|
- - - - - 20 - 20 - - - -;
|
||||||
|
- - - - - 30 - 30 - - - -;
|
||||||
|
- - - - - 20 - 20 - - - -;
|
||||||
|
- - - - - 10 - 10 - - - -;
|
||||||
|
- - - - - 0 - 0 - - - 200; # stand
|
||||||
|
@
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@MPU6050_BIND
|
||||||
|
name I2C addr;
|
||||||
|
#mpu6050-0 i2c-1 0x69;
|
||||||
|
#mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
- 10 - - - - - 10 - - - 50;
|
||||||
|
- 20 - - - - - 20 - - - -;
|
||||||
|
- 30 - - - - - 30 - - - -;
|
||||||
|
- 20 - - - - - 20 - - - -;
|
||||||
|
- 10 - - - - - 10 - - - -;
|
||||||
|
- 0 - - - - - 0 - - - 200; # stand
|
||||||
|
@
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
# 在 @Action 矩阵中,硬件变量的参数可以使用别名
|
||||||
|
@Alias
|
||||||
|
name alias;
|
||||||
|
A angle;
|
||||||
|
F frequency;
|
||||||
|
@
|
||||||
|
|
||||||
|
@I2C
|
||||||
|
name path;
|
||||||
|
i2c-0 /dev/i2c-0;
|
||||||
|
i2c-1 /dev/i2c-1;
|
||||||
|
@
|
||||||
|
|
||||||
|
@PCA9685
|
||||||
|
name I2C address;
|
||||||
|
pca9685-0 i2c-1 0x40;
|
||||||
|
@
|
||||||
|
|
||||||
|
@MPU6050_BIND
|
||||||
|
name I2C addr;
|
||||||
|
#mpu6050-0 i2c-1 0x69;
|
||||||
|
#mpu6050-1 i2c-1 0x68;
|
||||||
|
#mpu6050-2 i2c-0 0x68;
|
||||||
|
#mpu6050-3 i2c-0 0x69;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Speaker
|
||||||
|
name channels sample_rate volumn max_f min_f;
|
||||||
|
spk 2 48000 0.6 5000 -;
|
||||||
|
@
|
||||||
|
|
||||||
|
@Servo
|
||||||
|
name max min offset controller channel;
|
||||||
|
s1 90 - - pca9685-0 0; # rb0
|
||||||
|
s2 180 - - - 1; # rb1
|
||||||
|
s3 -90 - - - 2; # lb0
|
||||||
|
s4 -180 - - - 3; # lb1
|
||||||
|
s5 90 - - - 4; # lf0
|
||||||
|
s6 180 - - - 5; # lf1
|
||||||
|
s7 -90 - - - 6; # rf0
|
||||||
|
s8 -180 - - - 7; # rf1
|
||||||
|
s9 180 - - - 8; # cam0
|
||||||
|
s10 180 - - - 9; # cam1
|
||||||
|
@
|
||||||
|
|
||||||
|
# 行动矩阵
|
||||||
|
@Action
|
||||||
|
s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; # column name
|
||||||
|
45 - 45 - 45 - 45 - 90 90 10 200; # stand
|
||||||
|
- - - 10 - 10 - - - - - 50;
|
||||||
|
- - - 20 - 20 - - - - - -;
|
||||||
|
- - - 30 - 30 - - - - - -;
|
||||||
|
- - - 20 - 20 - - - - - -;
|
||||||
|
- - - 10 - 10 - - - - - -;
|
||||||
|
- - - 0 - 0 - - - - - 200; # stand
|
||||||
|
@
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# SNNEAK
|
||||||
|
# legs | neck | free | delay
|
||||||
|
#lb rb rf lf
|
||||||
|
~s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 - - - - - - spk delay; # column name
|
||||||
|
!90 180 -90 -180 90 180 -90 -180 180 180 - - - - - - 5000 1000; # 4-legs spider robot
|
||||||
|
! - - - - - - - - - - - - - - - - - -; # 4-legs spider robot
|
||||||
|
! - - - - - - - - - - - - - - - - - -; # 4-legs spider robot
|
||||||
|
|
||||||
|
# 俯卧撑
|
||||||
|
#
|
||||||
|
- 0 - 0 - 0 - 0 90 90 - - - - - - 1000 200;
|
||||||
|
- 5 - 5 - 5 - 5 - - - - - - - - 0 50;
|
||||||
|
- 10 - 10 - 10 - 10 - - - - - - - - - -;
|
||||||
|
- 15 - 15 - 15 - 15 - - - - - - - - - -;
|
||||||
|
- 20 - 20 - 20 - 20 - - - - - - - - - -;
|
||||||
|
- 25 - 25 - 25 - 25 - - - - - - - - - -;
|
||||||
|
- 30 - 30 - 30 - 30 - - - - - - - - - -;
|
||||||
|
- 35 - 35 - 35 - 35 - - - - - - - - - -;
|
||||||
|
|
||||||
|
#r
|
||||||
|
- 35 - 35 - 35 - 35 - - - - - - - - 1000 200;
|
||||||
|
- 30 - 30 - 30 - 30 - - - - - - - - 0 50;
|
||||||
|
- 25 - 25 - 25 - 25 - - - - - - - - - -;
|
||||||
|
- 20 - 20 - 20 - 20 - - - - - - - - - -;
|
||||||
|
- 15 - 15 - 15 - 15 - - - - - - - - - -;
|
||||||
|
- 10 - 10 - 10 - 10 - - - - - - - - - -;
|
||||||
|
- 5 - 5 - 5 - 5 - - - - - - - - - -;
|
||||||
|
- 0 - 0 - 0 - 0 - - - - - - - - - -;
|
||||||
|
|
||||||
+263
@@ -0,0 +1,263 @@
|
|||||||
|
package bot4cj
|
||||||
|
import std.io.*
|
||||||
|
import std.math.*
|
||||||
|
import std.convert.*
|
||||||
|
import std.collection.*
|
||||||
|
import bot4cj.math.*
|
||||||
|
|
||||||
|
/* -------- 矩阵编程语言 - 基本对象 --------
|
||||||
|
* TODO:增加储存到文件系统的功能
|
||||||
|
* TODO:神经网络层转换为矩阵对象
|
||||||
|
-------- */
|
||||||
|
class MatrixObject {
|
||||||
|
private var name: String = "" /* -------- 名字 -------- */
|
||||||
|
private var tmp = ArrayList<Rune>() /* ---- rune buf 字符缓冲区 ---- */
|
||||||
|
private var grp = ArrayList<String>() /* ---- one line 整行缓冲区 ---- */
|
||||||
|
private var values = ArrayList<ArrayList<String>>() /* ---- 原始矩阵 ---- */
|
||||||
|
|
||||||
|
/* -------- 是否已命名 -------- */
|
||||||
|
public func hasName(): Bool { !(name == "") }
|
||||||
|
|
||||||
|
/* --------- 获得名字 --------- */
|
||||||
|
public func getName(): String { return name }
|
||||||
|
|
||||||
|
/* ------- 取动作的长度 ------- */
|
||||||
|
public mut prop size: Int64 {
|
||||||
|
get() { values.size }
|
||||||
|
set(_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 取动作的列数 -------- */
|
||||||
|
public mut prop col: Int64 {
|
||||||
|
get() {
|
||||||
|
if (this.size > 0) {
|
||||||
|
return values[0].size }
|
||||||
|
else { return 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
set(_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- 获取矩阵元素,只能取,不能修改 ------- */
|
||||||
|
public operator func [](row: Int64, col: Int64): String {
|
||||||
|
return values[row][col]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** -- 在某一行中寻找某一个字符串的列号 -- */
|
||||||
|
public func indexOfCol(row: Int64, key: String): Int64 {
|
||||||
|
for (col in 0..values[row].size) {
|
||||||
|
if (values[row][col] == key) {
|
||||||
|
return col }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 没找到 ---- */
|
||||||
|
throw IllegalArgumentException("The key does not found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----- 储存对象 ----- */
|
||||||
|
public func save(output: StringWriter<OutputStream>) {
|
||||||
|
output.writeln("@${getName()}")
|
||||||
|
for (vec in values) {
|
||||||
|
for (item in vec) {
|
||||||
|
output.write(item)
|
||||||
|
output.write(" ")
|
||||||
|
}
|
||||||
|
output.writeln(";")
|
||||||
|
}
|
||||||
|
output.writeln("@")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 添加字符 -------- */
|
||||||
|
public func addRune(c: Rune) { tmp.add(c) }
|
||||||
|
|
||||||
|
/* -------- 提交名字 -------- */
|
||||||
|
public func submitName() {
|
||||||
|
if (tmp.size == 0) { return }
|
||||||
|
let s = String(tmp)
|
||||||
|
tmp.clear()
|
||||||
|
name = s
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 提交元素 -------- */
|
||||||
|
public func submitItem() {
|
||||||
|
if (tmp.size == 0) { return }
|
||||||
|
let s = String(tmp)
|
||||||
|
tmp = ArrayList<Rune>()
|
||||||
|
grp.add(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 提交行 -------- */
|
||||||
|
public func submitLine(count: Int64) {
|
||||||
|
if (grp.size == 0) { return }
|
||||||
|
if (!values.size == 0 && !grp.size == values[values.size - 1].size)
|
||||||
|
{ throw DoesNotSync(count) }
|
||||||
|
values.add(grp)
|
||||||
|
grp = ArrayList<String>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 对Nn1进行扩展 ---------------- */
|
||||||
|
extend Nn1 {
|
||||||
|
/* -------- 转换为MatrixObject对象 -------- */
|
||||||
|
public func toMto(): MatrixObject {
|
||||||
|
let mto = MatrixObject()
|
||||||
|
return mto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 矩阵式解析器 --------
|
||||||
|
* TODO(20260514-qing): 这个类输入流数据,输出MatrixObject对象
|
||||||
|
* 它是与其深度绑定的,我倾向于将其与MatrixObject融合
|
||||||
|
* 即持有HashMap<String, ArrayList<ArrayList<String>>> */
|
||||||
|
class MatrixParser {
|
||||||
|
|
||||||
|
/* ---- 静态判断Option<Rune>是否有值 ----
|
||||||
|
- 该方法用于文本文件遍历时判空 */
|
||||||
|
private static func hasValue(co: Option<Rune>): Bool {
|
||||||
|
match(co) {
|
||||||
|
case Some(_) => true
|
||||||
|
case None => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 主初始化器(构造函数) --------
|
||||||
|
- 设置目标输入流 */
|
||||||
|
private var input: StringReader<InputStream>
|
||||||
|
public init(input: StringReader<InputStream>) {
|
||||||
|
this.input = input }
|
||||||
|
|
||||||
|
/* -------- 切换目标输入流 -------- */
|
||||||
|
public func reset(input: StringReader<InputStream>) {
|
||||||
|
this.input = input }
|
||||||
|
|
||||||
|
/* -------- 解析器 - 循环不变式 ----------------------
|
||||||
|
* -------- 初始化: ss是一个动作脚本的输入流 --------
|
||||||
|
* -------- 保持: ss是动作脚本剩余部分的输入流 ------
|
||||||
|
* -------- 终止: 当输入流为空时,循环终止 ---------- */
|
||||||
|
private var obj = MatrixObject()
|
||||||
|
public func parse(): Option<MatrixObject> {
|
||||||
|
var count = 1i64 /* 行计数器始于1 */
|
||||||
|
flag = false /* 重置结束标志 */
|
||||||
|
this.obj = MatrixObject() /* 创建一个新的对象,一次只解析一个对象 */
|
||||||
|
var so = input.read() /* 获取初始状态 */
|
||||||
|
while (!flag && hasValue(so)) {
|
||||||
|
try {
|
||||||
|
let s = so.getOrThrow()
|
||||||
|
print(s) /* debug */
|
||||||
|
count = _next(s, count) /* 进行解析解析 */
|
||||||
|
so = input.read() /* 为下一次循环提供条件 */
|
||||||
|
}
|
||||||
|
catch (exp: BufOverTheLine) { throw exp }
|
||||||
|
catch (exp: IllegalArgumentException) { throw IllegalArgumentException("Parse failed at line ${count}") }
|
||||||
|
catch (exp: Exception) { throw UnknowExcept(count) } /* FIXME: 还是暴露出原来的错误位置来更好? */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 如果没有新的矩阵式对象(意外终止)则返回空 */
|
||||||
|
if (!obj.hasName()) { return None }
|
||||||
|
return obj /* 否则为解析成功正常退出,返回解析到的矩阵式对象 */
|
||||||
|
}
|
||||||
|
|
||||||
|
private var flag = false /* 结束标志 */
|
||||||
|
private var nl: Bool = true /* new line */
|
||||||
|
private var cm: Bool = false /* comment */
|
||||||
|
private var nm: Bool = false /* name-line */
|
||||||
|
private func _next(c: Rune, ct: Int64): Int64 {
|
||||||
|
var count = ct
|
||||||
|
/* ------------ COMMENTS ------------ */
|
||||||
|
if (!cm && c == r'#') { cm = true } /* 注释开始时 */
|
||||||
|
else if (cm && c == r'\n') { /* 注释结束时 */
|
||||||
|
nl = true
|
||||||
|
cm = false
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
else if (cm && c != r'\n') { ; } /* 注释进行时 */
|
||||||
|
/* FIXME: EOF情况 */
|
||||||
|
/* ------------ new line ------------ */
|
||||||
|
else if (nl && (c == r' ' || c == r'\t' || c == r'\r')) { ; } /* 新行空字符,维持现状 */
|
||||||
|
else if (nl && c == r'\n') { /* 新行空行 */
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
else if (nl && c == r'@' && !obj.hasName()) {
|
||||||
|
nl = false
|
||||||
|
nm = true
|
||||||
|
}
|
||||||
|
else if (nl && c == r'@' && obj.hasName()) {
|
||||||
|
flag =true
|
||||||
|
nl = false
|
||||||
|
nm = true
|
||||||
|
}
|
||||||
|
else if (nl && c != r'@') { /* 新行内容 */
|
||||||
|
nl = false
|
||||||
|
obj.addRune(c)
|
||||||
|
}
|
||||||
|
/* ------------- not nl ------------- */
|
||||||
|
else if (!nl && nm && (c == r' ' || c == r'\t' || c == r'\n')) { /* 添加名字 */
|
||||||
|
obj.submitName()
|
||||||
|
nl = true
|
||||||
|
nm = false
|
||||||
|
}
|
||||||
|
else if (!nl && !nm && (c == r' ' || c == r'\t' || c == r"\n")) { /* 添加命名项 */
|
||||||
|
obj.submitItem()
|
||||||
|
if (c == r"\n") { count += 1 }
|
||||||
|
}
|
||||||
|
else if (!nl && c == r';') { /* 普通行结束 */
|
||||||
|
obj.submitItem()
|
||||||
|
obj.submitLine(count)
|
||||||
|
nl = true
|
||||||
|
}
|
||||||
|
else if (!nl) { // 非空格、非制表符、非分号
|
||||||
|
obj.addRune(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 返回行号 -------- */
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 矩阵式储存器 --------
|
||||||
|
* - 接收矩阵式的哈希表(HashMap<String, MatrixObject>)
|
||||||
|
* - 以及一个输出流(在构造函数)。 */
|
||||||
|
class MatrixSaver {
|
||||||
|
private let stringWriter: StringWriter<OutputStream>
|
||||||
|
public init(output: OutputStream) {
|
||||||
|
stringWriter = StringWriter(output) }
|
||||||
|
public func save(C: LinkedList<MatrixObject>) {
|
||||||
|
for (item in C) {
|
||||||
|
item.save(stringWriter)
|
||||||
|
stringWriter.writeln() /* 对象之间空一行 */
|
||||||
|
stringWriter.flush() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --------------------------------------------------------- */
|
||||||
|
/* --------------------------------------------------------- */
|
||||||
|
/* TODO(20260516-qing): 继续完善异常种类 */
|
||||||
|
|
||||||
|
/* -------- 解析异常 -------- */
|
||||||
|
open class ParseExcept <: Exception {
|
||||||
|
public ParseExcept(msg: String, line: Int64) {
|
||||||
|
super('${msg}. we got line: ${line}') }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 缓冲区过长 -------- */
|
||||||
|
class BufOverTheLine <: ParseExcept {
|
||||||
|
BufOverTheLine(count: Int64) {
|
||||||
|
super('Buffer length over the line', count) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 行没对齐 -------- */
|
||||||
|
class DoesNotSync <: ParseExcept {
|
||||||
|
DoesNotSync(count: Int64) {
|
||||||
|
super("Matrix Line Does Not Sync", count) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 未知异常 -------- */
|
||||||
|
class UnknowExcept <: ParseExcept {
|
||||||
|
UnknowExcept(count: Int64) {
|
||||||
|
super("I don't understand what happened", count) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* i2c - 用户界面
|
||||||
|
*
|
||||||
|
* 这里已经提供了I2C通用接口,可以将上层代码移动到仓颉下面
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#ifdef __linux__
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/ioctl.h>
|
||||||
|
#include <linux/i2c-dev.h> /* 其中宏的取值可能具有平台特异性 */
|
||||||
|
#include <i2c/smbus.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "bot4cj.h" /* 有没有可能在仓颉代码中解析C头文件取得宏 */
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 初始化i2c设备
|
||||||
|
*
|
||||||
|
* 动态指定设备文件
|
||||||
|
*
|
||||||
|
* 返回打开的文件句柄或者错误码
|
||||||
|
*/
|
||||||
|
#ifdef __linux__
|
||||||
|
int16_t I2C_init(const char* filepath) {
|
||||||
|
int16_t fd;
|
||||||
|
if ((fd = open(filepath, O_RDWR)) < 0) {
|
||||||
|
perror("Failed to open I2C bus");
|
||||||
|
return BOT4CJ_I2C_OPENED_FAILED;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef _WIN64
|
||||||
|
int16_t I2C_init(const char* filepath) {
|
||||||
|
return BOT4CJ_I2C_OPENED_FAILED;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* 释放I2C设备文件 */
|
||||||
|
#ifdef __linux__
|
||||||
|
void I2C_free(int16_t fd) {
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef _WIN64
|
||||||
|
void I2C_free(int16_t fd) {
|
||||||
|
/* Windows平台下无需操作 */
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 设置I2C地址 */
|
||||||
|
#ifdef __linux__
|
||||||
|
int I2C_setAddr(int16_t fd, uint8_t addr) {
|
||||||
|
if (ioctl(fd, I2C_SLAVE, addr) < 0) {
|
||||||
|
perror("Failed to set I2C address");
|
||||||
|
return BOT4CJ_I2C_ERR;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return BOT4CJ_I2C_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef _WIN64
|
||||||
|
int I2C_setAddr(int16_t fd, uint8_t addr) {
|
||||||
|
return BOT4CJ_I2C_ERR;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* 写寄存器 */
|
||||||
|
#ifdef __linux__
|
||||||
|
int I2C_writeReg(int16_t fd, uint8_t reg, uint8_t val) {
|
||||||
|
/*uint8_t buf[] = {reg, val};
|
||||||
|
if (write(fd, buf, 2) != 2) {
|
||||||
|
perror("I2C write error");
|
||||||
|
return BOT4CJ_I2C_ERR;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return BOT4CJ_I2C_OK;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
return i2c_smbus_write_byte_data(fd, reg, val);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef _WIN64
|
||||||
|
int I2C_writeReg(int16_t fd, uint8_t reg, uint8_t val) {
|
||||||
|
return BOT4CJ_I2C_ERR;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* 读寄存器 失败返回0 */
|
||||||
|
#ifdef __linux__
|
||||||
|
uint8_t I2C_readReg(int16_t fd, uint8_t reg) {
|
||||||
|
|
||||||
|
uint8_t value;
|
||||||
|
|
||||||
|
/*if (write(fd, ®, 1) != 1) {
|
||||||
|
perror("I2C read error");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (read(fd, &value, 1)!= 1) {
|
||||||
|
perror("I2C read error");
|
||||||
|
return 0;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
//return value;
|
||||||
|
|
||||||
|
int8_t a = i2c_smbus_read_byte_data(fd, reg);
|
||||||
|
return *(uint8_t *)& a;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef _WIN64
|
||||||
|
uint8_t I2C_readReg(int16_t fd, uint8_t reg) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* 读寄存器 - 有符号
|
||||||
|
* 失败返回0 */
|
||||||
|
#ifdef __linux__
|
||||||
|
int8_t I2C_readRegFlag(int16_t fd, uint8_t reg) {
|
||||||
|
|
||||||
|
int8_t value;
|
||||||
|
|
||||||
|
//if (write(fd, ®, 1) != 1) {
|
||||||
|
//perror("I2C read error");
|
||||||
|
//return 0;
|
||||||
|
//}
|
||||||
|
|
||||||
|
//if (read(fd, &value, 1)!= 1) {
|
||||||
|
//perror("I2C read error");
|
||||||
|
//return 0;
|
||||||
|
//}
|
||||||
|
|
||||||
|
//return value;
|
||||||
|
|
||||||
|
return i2c_smbus_read_byte_data(fd, reg);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef _WIN64
|
||||||
|
int8_t I2C_readRegFlag(int16_t fd, uint8_t reg) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,516 @@
|
|||||||
|
#include <random>
|
||||||
|
#include <algorithm>
|
||||||
|
#include "Matrix.hh"
|
||||||
|
|
||||||
|
namespace qing {
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
Matrx<T>::Matrx(): row(0), col(0) {}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
Matrx<T>::Matrx(const uint64_t row, const uint64_t col)
|
||||||
|
: row(row), col(col) { /* 该构造函数用于从头开始构造 */
|
||||||
|
arr = std::vector<T>(col * row);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
Matrx<T>::Matrx(const uint64_t row, const uint64_t col, const std::vector<T>& src)
|
||||||
|
: row(row), col(col) { /* 该构造函数用于通过已有的vector进行构造 */
|
||||||
|
arr = src;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
const T& Matrx<T>::get(uint64_t row, uint64_t col) const { /* 访问器 - 访问矩阵元素 */
|
||||||
|
return arr[col * this->row + row];
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T& Matrx<T>::witch(uint64_t row, uint64_t col) { /* 访问器 - 访问矩阵元素 */
|
||||||
|
return arr[col * this->row + row];
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void Matrx<T>::set(uint64_t row, uint64_t col, T v) { /* 设置矩阵元素 */
|
||||||
|
arr[col * this->row + row] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
const std::vector<T>& Matrx<T>::get_data() const{ /* 访问器 - 用于获取原始数据 */
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
const uint64_t Matrx<T>::get_col() const { /* 获取列数 */
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
const uint64_t Matrx<T>::get_row() const { /* 获取行数 */
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
bool Matrx<T>::has_same_shape(const Matrx<T>& other) { /* 两个矩阵是否具有相同形状 */
|
||||||
|
return other.get_col() == this->get_col() && other.get_row() == this->get_row();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
bool Matrx<T>::has_dot_shape(const Matrx<T>& other) { /* 具有矩阵乘法的形状 */
|
||||||
|
return this->get_col() == other.get_row();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void Matrx<T>::assert_same_shape(const Matrx<T>& other) { /* 断言两个矩阵具有相同形状 */
|
||||||
|
if (!has_same_shape(other))
|
||||||
|
throw std::invalid_argument("The shapes of matrix are not same.");
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void Matrx<T>::assert_dot_shape(const Matrx<T>& other) { /* 断言两个矩阵具有点积前置形状 */
|
||||||
|
if (!has_dot_shape(other)) {
|
||||||
|
throw std::invalid_argument("The shapes of matrix can not be dot.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
Matrx<T> Matrx<T>::operator+(const Matrx<T>& other) { /* 加法操作符重定义 - 矩阵加法 */
|
||||||
|
//assert_same_shape(other); /* 检查两个矩阵形状是否一致 */
|
||||||
|
std::vector<T> v0 = this->get_data();
|
||||||
|
std::vector<T> vec = other.get_data();
|
||||||
|
for (auto it = v0.begin(), it_or = vec.begin(); it != v0.end(); ++it, ++it_or )
|
||||||
|
*it += *it_or;
|
||||||
|
return Matrx<T>{this->get_row(), this->get_col(), v0};
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
Matrx<T> Matrx<T>::operator+(T& item) { /* 矩阵加法广播 */
|
||||||
|
auto len = this->get_data().size();
|
||||||
|
auto v0 = std::vector<T> (len, item);
|
||||||
|
auto matrix = (*this) + Matrx(get_row(), get_col(), v0);
|
||||||
|
|
||||||
|
//auto matrix = *this;
|
||||||
|
//for (auto it = arr.begin(); it != arr.end(); ++it)
|
||||||
|
// *it += item;
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
Matrx<T> Matrx<T>::operator-(const Matrx<T>& other) { /* 操作符重载 - 矩阵减法 */
|
||||||
|
//assert_same_shape(other); /* 检查两个矩阵形状是否一致 */
|
||||||
|
std::vector<T> v0 = this->get_data();
|
||||||
|
std::vector <T> vec = other.get_data();
|
||||||
|
for (auto it = v0.begin(), it_or = vec.begin(); it != v0.end(); ++it, ++it_or )
|
||||||
|
*it -= *it_or;
|
||||||
|
return Matrx{this->get_row(), this->get_col(), v0};
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
Matrx<T> Matrx<T>::operator-(T& item) { /* 操作符重载 - 矩阵减法广播 */
|
||||||
|
auto len = this->get_data().size();
|
||||||
|
auto v0 = std::vector<T> (len, item);
|
||||||
|
auto matrix = (*this) - Matrx(get_row(), get_col(), v0);
|
||||||
|
|
||||||
|
//Matrx<T> matrix = *this;
|
||||||
|
//for (auto it = arr.begin(); it != arr.end(); ++it)
|
||||||
|
// *it -= item;
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
Matrx<T> Matrx<T>::dot(const Matrx<T>& other) { /* 操作符重载 - 矩阵乘法 */
|
||||||
|
//assert_dot_shape(other); /* 它们的形状是否能够点积 */
|
||||||
|
auto matrix = Matrx<T>(this->get_row(), other.get_col());
|
||||||
|
for (uint64_t i=0; i<this->get_row(); ++i) /* 点积运算 */
|
||||||
|
for (uint64_t j=0; j<other.get_col(); ++j) {
|
||||||
|
T sum{0};
|
||||||
|
for (uint64_t k=0; k<this->get_col(); ++k)
|
||||||
|
sum += this->get(i, k) * other.get(k, j);
|
||||||
|
matrix.witch(i, j) = sum;
|
||||||
|
}
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
Matrx<T> Matrx<T>::operator*(T& item) { /* 操作符重载 - 矩阵乘法广播 */
|
||||||
|
std::vector<T> vec = this->get_data();
|
||||||
|
for (auto it = vec.begin(); it != vec.end(); ++it)
|
||||||
|
*it *= item;
|
||||||
|
return Matrx<T>{this->get_row(), this->get_col(), vec};
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
Matrx<T> Matrx<T>::operator*(const Matrx<T>& other) { /* 加法操作符重定义 - 矩阵加法 */
|
||||||
|
//assert_same_shape(other); /* 检查两个矩阵形状是否一致 */
|
||||||
|
std::vector<T> v0 = this->get_data();
|
||||||
|
std::vector<T> vec = other.get_data();
|
||||||
|
for (auto it = v0.begin(), it_or = vec.begin(); it != v0.end(); ++it, ++it_or )
|
||||||
|
*it *= *it_or;
|
||||||
|
return Matrx<T>{this->get_row(), this->get_col(), v0};
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
Matrx<T> Matrx<T>::operator/(T& item) { /* 操作符重载 - 矩阵除法广播 */
|
||||||
|
std::vector<T> vec = this->get_data();
|
||||||
|
for (auto it = vec.begin(); it != vec.end(); ++it)
|
||||||
|
*it /= item;
|
||||||
|
return Matrx<T>(this->get_row(), this->get_col(), vec);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
Matrx<T> Matrx<T>::operator/(const Matrx<T>& other) { /* 加法操作符重定义 - 矩阵加法 */
|
||||||
|
//assert_same_shape(other); /* 检查两个矩阵形状是否一致 */
|
||||||
|
std::vector<T> v0 = this->get_data();
|
||||||
|
std::vector<T> vec = other.get_data();
|
||||||
|
for (auto it = v0.begin(), it_or = vec.begin(); it != v0.end(); ++it, ++it_or )
|
||||||
|
*it /= *it_or;
|
||||||
|
return Matrx<T>{this->get_row(), this->get_col(), v0};
|
||||||
|
}
|
||||||
|
|
||||||
|
template class Matrx<float>; /* 显式实例化 */
|
||||||
|
template class Matrx<double>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FIXME: 形状不合法时返回空指针 */
|
||||||
|
using namespace qing;
|
||||||
|
extern "C" {
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* createMatrix(uint64_t row, uint64_t col) {
|
||||||
|
return (void *) new Matrx<float>(row, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void freeMatrix(void* M) {
|
||||||
|
delete (Matrx<float>*) M;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
float* witchMatrix(void *m, uint64_t row, uint64_t col) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
return &(M->witch(row, col));
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
float getMatrix(void *m, uint64_t row, uint64_t col) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
return M->get(row, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void setMatrix(void *m, uint64_t row, uint64_t col, float v) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
M->set(row, col, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
uint64_t getrowMatrix(void *m) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
return M->get_row();
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
uint64_t getcolMatrix(void *m) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
return M->get_col();
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
bool sameShapeMatrix(void *m, void *s) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
Matrx<float> *S = (Matrx<float>*) s;
|
||||||
|
return M->has_same_shape(*S);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
bool dotShapeMatrix(void *m, void *s) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
Matrx<float> *S = (Matrx<float>*) s;
|
||||||
|
return M->has_dot_shape(*S);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* plusMatrix(void *m, void *s) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
Matrx<float> *S = (Matrx<float>*) s;
|
||||||
|
auto a = new Matrx<float>((*M) + (*S));
|
||||||
|
return (void *) a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* plusBroadcastMatrix(void *m, float t) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
auto a = new Matrx<float> ((*M) + t);
|
||||||
|
return (void*) a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* subMatrix(void *m, void *s) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
Matrx<float> *S = (Matrx<float>*) s;
|
||||||
|
auto a = new Matrx<float>((*M) - (*S));
|
||||||
|
return (void *) a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void * subBroadcastMatrix(void *m, float t) {
|
||||||
|
Matrx<float> *M = (Matrx<float> *) m;
|
||||||
|
auto a = new Matrx<float> ((*M) - t);
|
||||||
|
return (void *) a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* multiMatrix(void *m, void *s) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
Matrx<float> *S = (Matrx<float>*) s;
|
||||||
|
auto a = new Matrx<float>((*M) * (*S));
|
||||||
|
return (void *) a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* multiBroadcastMatrix(void *m, float t) {
|
||||||
|
Matrx<float> *M = (Matrx<float> *) m;
|
||||||
|
auto a = new Matrx<float>((*M) * t);
|
||||||
|
return (void*) a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* divMatrix(void *m, void *s) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
Matrx<float> *S = (Matrx<float>*) s;
|
||||||
|
auto a = new Matrx<float>((*M) / (*S));
|
||||||
|
return (void *) a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* divBroadcastMatrix(void *m, float t) {
|
||||||
|
Matrx<float> *M = (Matrx<float> *) m;
|
||||||
|
auto a = new Matrx<float>((*M) / t);
|
||||||
|
return (void*) a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* dotMatrix(void *m, void *s) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
Matrx<float> *S = (Matrx<float>*) s;
|
||||||
|
auto a = new Matrx<float>(M->dot(*S));
|
||||||
|
return (void *) a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* transposeMatrix(void *m) {
|
||||||
|
Matrx<float> *M = (Matrx<float> *)m;
|
||||||
|
auto N= new Matrx<float>(M->get_col(), M->get_row());
|
||||||
|
for (uint64_t row = 0; row< N->get_row(); row++) {
|
||||||
|
for (uint64_t col = 0; col < N->get_col(); col++) {
|
||||||
|
N->witch(row, col) = M->get(col, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (void *)N;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 正态分布Xavier初始化 */
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* xavierNormalInit(uint64_t fan_in, uint64_t fan_out)
|
||||||
|
{
|
||||||
|
/* 构建空数组 */
|
||||||
|
std::vector<float> weights(fan_in * fan_out);
|
||||||
|
|
||||||
|
/* 高质量的随机数生成器 */
|
||||||
|
static std::mt19937 generator(std::random_device{}());
|
||||||
|
|
||||||
|
/* 计算标准差 */
|
||||||
|
float sigma = std::sqrt(2.0f / (fan_in + fan_out));
|
||||||
|
|
||||||
|
/* 创建正态分布对象,均值为0,标准差为sigma */
|
||||||
|
std::normal_distribution<float> distribution(0.0f, sigma);
|
||||||
|
|
||||||
|
/* 遍历权重矩阵,进行赋值 */
|
||||||
|
uint64_t k=0;
|
||||||
|
for (uint64_t i=0; i<fan_in; ++i)
|
||||||
|
for (uint64_t j=0; j<fan_out; ++j)
|
||||||
|
weights[k++] = distribution(generator);
|
||||||
|
|
||||||
|
/* 构建矩阵 */
|
||||||
|
auto matrix = new Matrx<float>(fan_in, fan_out, weights);
|
||||||
|
return (void *)matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 均匀分布Xavier初始化函数 */
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* xavierUniformInit(uint64_t fan_in, uint64_t fan_out)
|
||||||
|
{
|
||||||
|
/* 构建空的数组 */
|
||||||
|
auto weights = std::vector<float>(fan_in * fan_out);
|
||||||
|
|
||||||
|
/* 使用高质量的随机数生成器 */
|
||||||
|
static std::mt19937 generator(std::random_device{}());
|
||||||
|
|
||||||
|
/* 计算区间范围:sqrt(2 / (fan_in + fan_out)) */
|
||||||
|
float scale = std::sqrt(2.0f / (fan_in + fan_out));
|
||||||
|
|
||||||
|
/* 创建均匀分布对象,范围为[-scale, scale] */
|
||||||
|
std::uniform_real_distribution<float> distribution(-scale, scale);
|
||||||
|
|
||||||
|
/* 遍历权重矩阵,进行赋值 */
|
||||||
|
uint64_t k=0;
|
||||||
|
for (uint64_t i=0; i<fan_in; ++i)
|
||||||
|
for (uint64_t j=0; j<fan_out; ++j)
|
||||||
|
weights[k++] = distribution(generator);
|
||||||
|
|
||||||
|
/* 构建矩阵对象 */
|
||||||
|
auto matrix = new Matrx<float> (fan_in, fan_out, weights);
|
||||||
|
return (void *)matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* Sigmoid_do(void *m) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
std::vector<float> v = M->get_data();
|
||||||
|
for (float& x: v) {
|
||||||
|
x = 1.0 / (1.0 + std::exp(-x));
|
||||||
|
}
|
||||||
|
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
|
||||||
|
return (void *)N;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* Sigmoid_dot(void *m) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
std::vector<float> v = M->get_data();
|
||||||
|
for (float& o: v) {
|
||||||
|
o = o * (1.0 - o);
|
||||||
|
}
|
||||||
|
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
|
||||||
|
return (void *)N;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* ReLU_do(void *m) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
std::vector<float> v = M->get_data();
|
||||||
|
for (float& x: v) {
|
||||||
|
x = x > 0 ? x : 0;
|
||||||
|
}
|
||||||
|
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
|
||||||
|
return (void *)N;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* ReLU_dot(void *m) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
std::vector<float> v = M->get_data();
|
||||||
|
for (float& x: v) {
|
||||||
|
x = x > 0 ? 1.0f : 0.0f;
|
||||||
|
}
|
||||||
|
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
|
||||||
|
return (void *)N;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* Leaky_ReLU_do(void *m, float alpha) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
std::vector<float> v = M->get_data();
|
||||||
|
for (float& x: v) {
|
||||||
|
x = x > 0 ? x : alpha * x;
|
||||||
|
}
|
||||||
|
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
|
||||||
|
return (void *)N;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* Leaky_ReLU_dot(void *m, float alpha) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
std::vector<float> v = M->get_data();
|
||||||
|
for (float& x: v) {
|
||||||
|
x = x > 0 ? 1.0 : alpha;
|
||||||
|
}
|
||||||
|
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
|
||||||
|
return (void *)N;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* Tanh_do(void *m) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
std::vector<float> v = M->get_data();
|
||||||
|
for (float& x: v) {
|
||||||
|
x = std::tanh(x);
|
||||||
|
}
|
||||||
|
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
|
||||||
|
return (void *)N;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
_declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
void* Tanh_dot(void *m) {
|
||||||
|
Matrx<float> *M = (Matrx<float>*) m;
|
||||||
|
std::vector<float> v = M->get_data();
|
||||||
|
for (float& x: v) {
|
||||||
|
if (x > 20.0) x = 0.0; // tanh(x) ≈ 1, 导数≈0
|
||||||
|
else if (x < -20.0) x = 0.0; // tanh(x) ≈ -1, 导数≈0
|
||||||
|
else {
|
||||||
|
float t = tanh(x);
|
||||||
|
x = 1.0 - t * t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto N = new Matrx<float> (M->get_row(), M->get_col(), v);
|
||||||
|
return (void *)N;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
___col___
|
||||||
|
/ \
|
||||||
|
+---------+ \
|
||||||
|
| + + + + | | row - 第一维度
|
||||||
|
| + + + + | |
|
||||||
|
+---------+ /
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <cstdint>
|
||||||
|
namespace qing {
|
||||||
|
template <class T>
|
||||||
|
class Matrx {
|
||||||
|
public:
|
||||||
|
Matrx();
|
||||||
|
Matrx(const uint64_t row, const uint64_t col);
|
||||||
|
Matrx(const uint64_t row, const uint64_t col, const std::vector<T>& src);
|
||||||
|
const T& get(uint64_t row, uint64_t col) const;
|
||||||
|
//-------------旧接口----------------
|
||||||
|
T& witch(uint64_t row, uint64_t col);
|
||||||
|
//-------------新接口----------------
|
||||||
|
void set(uint64_t row, uint64_t col, T v);
|
||||||
|
//-----------------------------------
|
||||||
|
const std::vector<T>& get_data() const;
|
||||||
|
const uint64_t get_col() const;
|
||||||
|
const uint64_t get_row() const;
|
||||||
|
bool has_same_shape(const Matrx<T>& other);
|
||||||
|
bool has_dot_shape(const Matrx<T>& other);
|
||||||
|
void assert_same_shape(const Matrx<T>& other);
|
||||||
|
void assert_dot_shape(const Matrx<T>& other);
|
||||||
|
Matrx<T> operator+(const Matrx<T>& other);
|
||||||
|
Matrx<T> operator+(T& item);
|
||||||
|
Matrx<T> operator-(const Matrx<T>& other);
|
||||||
|
Matrx<T> operator-(T& item);
|
||||||
|
Matrx<T> operator*(const Matrx<T>& other);
|
||||||
|
Matrx<T> operator*(T& item);
|
||||||
|
Matrx<T> operator/(T& item);
|
||||||
|
Matrx<T> operator/(const Matrx<T>& other);
|
||||||
|
Matrx<T> dot(const Matrx<T>& other);
|
||||||
|
|
||||||
|
void save(std::ostream& out) {
|
||||||
|
out << row << " " << col;
|
||||||
|
for (const auto& item : arr) {
|
||||||
|
out << " ";
|
||||||
|
out << item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint64_t row, col;
|
||||||
|
std::vector<T> arr;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/* I2C操作库 - PCA9685 - MPU6050 */
|
||||||
|
#ifndef QING_I2C
|
||||||
|
#define QING_I2C
|
||||||
|
/* I2C */
|
||||||
|
#define BOT4CJ_I2C_OK 0
|
||||||
|
#define BOT4CJ_I2C_ERR 1
|
||||||
|
#define BOT4CJ_I2C_OPENED_FAILED -1
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
#define DLLEXPORT __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
#define DLLEXPORT
|
||||||
|
#endif
|
||||||
|
|
||||||
|
DLLEXPORT int16_t I2C_init(const char* filepath);
|
||||||
|
DLLEXPORT void I2C_free(int16_t fd);
|
||||||
|
DLLEXPORT int I2C_setAddr(int16_t fd, uint8_t addr);
|
||||||
|
DLLEXPORT int I2C_writeReg(int16_t fd, uint8_t reg, uint8_t val);
|
||||||
|
DLLEXPORT uint8_t I2C_readReg(int16_t fd, uint8_t reg);
|
||||||
|
DLLEXPORT int8_t I2C_readRegFlag(int16_t fd, uint8_t reg);
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#define _USE_MATH_DEFINES
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
#include "fft.h"
|
||||||
|
#include "Matrix.hh"
|
||||||
|
using namespace qing;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void functf1d(float A[DN])
|
||||||
|
{
|
||||||
|
unsigned long int i;
|
||||||
|
float dt, tt;
|
||||||
|
float p = 0.1;
|
||||||
|
float freq = 10.0; // 10Hz
|
||||||
|
float fs = DN; // 采样率65536Hz
|
||||||
|
|
||||||
|
dt = 1.0 / fs;
|
||||||
|
|
||||||
|
for(i = 0; i < DN; i++)
|
||||||
|
{
|
||||||
|
tt = (float)i * dt;
|
||||||
|
A[i] = sin(2.0 * M_PI * freq * tt) * p; // 正弦波
|
||||||
|
//A[1][i] = 0.0; // 虚部为0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 初始化FFT数组
|
||||||
|
static void fft_init(float A[2][DN], Matrx<float>& M)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < DN; i++) {
|
||||||
|
float a = M.witch(i, 0);
|
||||||
|
A[0][i] = a;
|
||||||
|
A[1][i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算FFT结果
|
||||||
|
static void fft_result(std::vector<float>& R, float A[2][DN])
|
||||||
|
{
|
||||||
|
for (int i = 0; i < DN/2; i++) {
|
||||||
|
float a = pow(A[0][i], 2) + pow(A[1][i], 2);
|
||||||
|
R[i] = sqrt(a) / (DN/2);
|
||||||
|
//std::cout << i << ":\t" << R[i] << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
// 生成正弦波
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* gen_wave()
|
||||||
|
#else
|
||||||
|
void* gen_wave()
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
std::vector<float> A(DN);
|
||||||
|
functf1d(A.data());
|
||||||
|
return (void *)new Matrx<float>(DN, 1, A);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始FFT
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* start_fft(void* M)
|
||||||
|
#else
|
||||||
|
void* start_fft(void* M)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
float A[2][DN];
|
||||||
|
std::vector<float> R(N);
|
||||||
|
fft_init(A, *(Matrx<float> *)M);
|
||||||
|
//fft1(A, 1); // 复序列基2 FFT
|
||||||
|
fft1r1(A, 1); // 单实序列
|
||||||
|
fft_result(R, A);
|
||||||
|
return (void*) new Matrx<float>(N/2, 1, R);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#ifndef FFT_H
|
||||||
|
#define FFT_H
|
||||||
|
|
||||||
|
#ifndef N
|
||||||
|
#define N 32768
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef DN
|
||||||
|
#define DN (N*2)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define eps 1.0e-9
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void initial(void);
|
||||||
|
void fft1(float A[2][N], int ifft);
|
||||||
|
unsigned long int inverseq(unsigned long int i1,
|
||||||
|
unsigned long int imax);
|
||||||
|
void fft1r2(float A[2][N], float B[2][N], int ifft);
|
||||||
|
void fft1r1(float A[2][DN], int ifft);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
/* fft1.c: fast Fourier transform base on 2 for one dimensional
|
||||||
|
complex array, direct and inverse,
|
||||||
|
This program is contributed by Jiang Yong, 2003. 1. */
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include "fft.h"
|
||||||
|
static int ifirst=0, sign=-1;
|
||||||
|
static unsigned M, jup[64], kup[64];
|
||||||
|
static float pi, wr[N], wi[N];
|
||||||
|
/********* initial **************/
|
||||||
|
void initial(void)
|
||||||
|
{
|
||||||
|
unsigned long int i, j, ni;
|
||||||
|
float unit, tt;
|
||||||
|
ni = N;
|
||||||
|
M = 0;
|
||||||
|
while (ni > 1) { ni=ni/2; M=M+1; }
|
||||||
|
ni = 1;
|
||||||
|
for(j=0; j<M; j++) ni = ni * 2;
|
||||||
|
if ((ni-N) != 0)
|
||||||
|
{
|
||||||
|
printf("N isn't compatible! (N => %i, ni -> %lu)\n", N, ni);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
kup[0] = N/2; /* kup[l] = 2 ^ (M - l - 1) */
|
||||||
|
jup[0] =1;
|
||||||
|
for (j=1;j<M;j++)
|
||||||
|
{
|
||||||
|
kup[j]=kup[j-1]/2;
|
||||||
|
jup[j]=jup[j-1]*2;
|
||||||
|
}
|
||||||
|
pi=4.0*atan((float)1.0);
|
||||||
|
unit=2.0*pi/((float)N);
|
||||||
|
tt=0.0;
|
||||||
|
for(i=0;i<N;i++)
|
||||||
|
{
|
||||||
|
wr[i] = cos(tt);
|
||||||
|
wi[i] = sign * sin(tt);
|
||||||
|
tt = tt + unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** ifft=1<-->FFT ** ifft=-1<-->IFFT **/
|
||||||
|
void fft1(float A[2][N], int ifft)
|
||||||
|
{
|
||||||
|
unsigned long int l, k, i, j, jl0, jl0k, jl1k, reseq, ij;
|
||||||
|
float wrc, wis, ar, ai, x[2][N];
|
||||||
|
if (ifirst == 0) { initial(), ifirst=1; }
|
||||||
|
if (ifft == -1)
|
||||||
|
for(i=0;i<N;i++) A[1][i] = -A[1][i];
|
||||||
|
for (l=0; l<M; l++)
|
||||||
|
{
|
||||||
|
for (j=0; j<jup[l]; j++)
|
||||||
|
{
|
||||||
|
reseq=inverseq(j, l);
|
||||||
|
ij=kup[l]*reseq;
|
||||||
|
jl0=2*kup[l]*j;
|
||||||
|
wrc = wr[ij];
|
||||||
|
wis = wi[ij];
|
||||||
|
for (k=0; k<kup[l]; k++)
|
||||||
|
{
|
||||||
|
jl0k = jl0 + k;
|
||||||
|
jl1k = jl0 + kup[l] + k;
|
||||||
|
ar = A[0][jl1k] * wrc - A[1][jl1k] * wis;
|
||||||
|
ai = A[0][jl1k] * wis + A[1][jl1k] * wrc;
|
||||||
|
A[0][jl1k] = A[0][jl0k] - ar;
|
||||||
|
A[1][jl1k] = A[1][jl0k] - ai;
|
||||||
|
A[0][jl0k] = A[0][jl0k] + ar;
|
||||||
|
A[1][jl0k] = A[1][jl0k] + ai;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i=0; i<N; i++)
|
||||||
|
{
|
||||||
|
j=inverseq(i, M);
|
||||||
|
x[0][i] = A[0][j];
|
||||||
|
x[1][i] = A[1][j];
|
||||||
|
}
|
||||||
|
for (i=0; i<N; i++)
|
||||||
|
{
|
||||||
|
A[0][i] = x[0][i];
|
||||||
|
A[1][i] = x[1][i];
|
||||||
|
}
|
||||||
|
if (ifft == -1)
|
||||||
|
for (i=0; i<N; i++)
|
||||||
|
{
|
||||||
|
A[0][i] = A[0][i]/((float)N);
|
||||||
|
A[1][i] = -A[1][i]/((float)N);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*********************************/
|
||||||
|
unsigned long int inverseq(unsigned long int i1,
|
||||||
|
unsigned long int imax)
|
||||||
|
{
|
||||||
|
unsigned long int j, ii, i0, inv;
|
||||||
|
i0 = i1;
|
||||||
|
inv = 0;
|
||||||
|
for (j=0; j < imax; j++)
|
||||||
|
{
|
||||||
|
ii=i0%2;
|
||||||
|
inv=2*inv+ii;
|
||||||
|
i0=(i0-ii)/2;
|
||||||
|
}
|
||||||
|
return (inv);
|
||||||
|
}
|
||||||
|
/* ifft=1 <--> FFT ** ifft=-1 <--> IFFT */
|
||||||
|
void fft1r2(float A[2][N], float B[2][N], int ifft)
|
||||||
|
{
|
||||||
|
unsigned long int k, j;
|
||||||
|
float norm, c[2][N];
|
||||||
|
if (ifft != -1)
|
||||||
|
{
|
||||||
|
for (k=0; k<N; k++)
|
||||||
|
{
|
||||||
|
c[0][k]=A[0][k];
|
||||||
|
c[1][k]=B[0][k];
|
||||||
|
}
|
||||||
|
fft1(c, 1);
|
||||||
|
A[0][0] = c[0][0];
|
||||||
|
A[1][0] = 0.0;
|
||||||
|
B[0][0] = c[1][0];
|
||||||
|
B[1][0] = 0.0;
|
||||||
|
for (j=1; j<N; j++)
|
||||||
|
{
|
||||||
|
A[0][j] = 0.5 * (c[0][j] + c[0][N-j]);
|
||||||
|
A[1][j] = 0.5 * (c[1][j] - c[1][N-j]);
|
||||||
|
B[0][j] = 0.5 * (c[1][j] + c[1][N-j]);
|
||||||
|
B[1][j] = -0.5 * (c[0][j] - c[0][N-j]);
|
||||||
|
}
|
||||||
|
} /* end if*/
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (j=0; j<N/2; j++)
|
||||||
|
/* check {xj}, {yj} is to real {Ak}, {Bk} */
|
||||||
|
{
|
||||||
|
if (j == 0) norm = 2.0 * fabs(A[1][0])
|
||||||
|
+ 2.0 * fabs(B[1][0]);
|
||||||
|
else norm = fabs(A[1][j]+A[1][N-j])
|
||||||
|
+ fabs(A[0][j]-A[0][N-j])
|
||||||
|
+ fabs(B[1][j]+B[1][N-j])
|
||||||
|
+ fabs(B[0][j]-B[0][N-j]);
|
||||||
|
if (norm > eps)
|
||||||
|
{
|
||||||
|
printf("The IDFT of the series isn't real!\n");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (j=0; j<N; j++)
|
||||||
|
{
|
||||||
|
c[0][j]=A[0][j] -B[1][j];
|
||||||
|
c[1][j]=A[1][j] +B[0][j];
|
||||||
|
}
|
||||||
|
fft1(c, -1);
|
||||||
|
for (k=0; k<N; k++)
|
||||||
|
{
|
||||||
|
A[0][k] = c[0][k];
|
||||||
|
B[0][k] = c[1][k];
|
||||||
|
A[1][k] = 0.0;
|
||||||
|
B[1][k] = 0.0;
|
||||||
|
}
|
||||||
|
} /* end else */
|
||||||
|
}
|
||||||
|
/* ifft = 1 <--> FFT ** ifft = -1 <--> IFFT */
|
||||||
|
void fft1r1(float A[2][DN], int ifft)
|
||||||
|
{
|
||||||
|
unsigned long int j, k, dk;
|
||||||
|
float norm, c[2][N], d[2][N], tt, hunit, cosj, sinj;
|
||||||
|
hunit = 4.0 * atan(1.0) / (float)N;
|
||||||
|
if ( ifft != -1)
|
||||||
|
{
|
||||||
|
for (k=0; k<N; k++)
|
||||||
|
{
|
||||||
|
dk = k + k;
|
||||||
|
c[0][k] = A[0][dk];
|
||||||
|
d[0][k] = A[0][dk+1];
|
||||||
|
c[1][k] = 0.0;
|
||||||
|
d[1][k] = 0.0;
|
||||||
|
}
|
||||||
|
fft1r2(c, d, 1);
|
||||||
|
for (j = 0; j < N; j++)
|
||||||
|
{
|
||||||
|
tt = (float)j * hunit;
|
||||||
|
cosj = cos(tt);
|
||||||
|
sinj = sign * sin(tt);
|
||||||
|
A[0][j] = c[0][j] + cosj * d[0][j] - sinj * d[1][j];
|
||||||
|
A[1][j] = c[1][j] + cosj * d[1][j] + sinj * d[0][j];
|
||||||
|
}
|
||||||
|
A[0][N] = c[0][0] - d[0][0];
|
||||||
|
A[1][N] = c[1][0] - d[1][0];
|
||||||
|
for(j=1; j<N; j++)
|
||||||
|
{
|
||||||
|
A[0][DN-j] = A[0][j];
|
||||||
|
A[1][DN-j] = -A[1][j];
|
||||||
|
}
|
||||||
|
} // end if
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (j = 0; j <N; j++) // check {xj} is to real {Ak}
|
||||||
|
{
|
||||||
|
if (j ==0) norm = 2.0 * fabs(A[1][0]);
|
||||||
|
else norm = fabs(A[1][j]+A[1][DN-j])+
|
||||||
|
fabs(A[0][j]-A[0][DN-j]);
|
||||||
|
if (norm > eps)
|
||||||
|
{
|
||||||
|
printf("The IDFT of the series {xj} isn't real!\n");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (j = 0; j <N; j++)
|
||||||
|
{
|
||||||
|
c[0][j]= 0.5 * (A[0][j]+A[0][N+j]);
|
||||||
|
c[1][j]= 0.5 * (A[1][j]+A[1][N+j]);
|
||||||
|
tt = (float) j * hunit;
|
||||||
|
cosj = cos(tt);
|
||||||
|
sinj = -sign*sin(tt);
|
||||||
|
d[0][j]= 0.5 * (cosj * (A[0][j]-A[0][N+j])
|
||||||
|
-sinj*(A[1][j] - A[1][N+j]));
|
||||||
|
}
|
||||||
|
fft1r2(c, d, -1);
|
||||||
|
for (k=0; k<N; k++)
|
||||||
|
{
|
||||||
|
dk = k+k;
|
||||||
|
A[0][dk]=c[0][k];
|
||||||
|
A[0][dk+1]=d[0][k];
|
||||||
|
A[1][dk]=0.0;
|
||||||
|
A[1][dk+1]=0.0;
|
||||||
|
}
|
||||||
|
} // end else
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* 一维卡尔曼滤波器
|
||||||
|
* 使用时先定义一个kalman指针,然后调用kalmanCreate()创建一个滤波器
|
||||||
|
* 每次读取到传感器数据后即可调用KalmanFilter()来对数据进行滤波
|
||||||
|
* 使用示例
|
||||||
|
* extKalman_t p; //定义一个卡尔曼滤波器结构体
|
||||||
|
* float SersorData; //需要进行滤波的数据
|
||||||
|
* KalmanCreate(&p,20,200); //初始化该滤波器的Q=20 R=200参数
|
||||||
|
* while(1)
|
||||||
|
* {
|
||||||
|
* SersorData = sersor(); //获取数据
|
||||||
|
* SersorData = KalmanFilter(&p,SersorData); //对数据进行滤波
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "kalman.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name kalmanCreate
|
||||||
|
* @brief 创建一个卡尔曼滤波器
|
||||||
|
* @param p: 滤波器
|
||||||
|
* T_Q:系统噪声协方差
|
||||||
|
* T_R:测量噪声协方差
|
||||||
|
*
|
||||||
|
* @retval none
|
||||||
|
* @attention R固定,Q越大,代表越信任侧量值,Q无穷代表只用测量值
|
||||||
|
* 反之,Q越小代表越信任模型预测值,Q为零则是只用模型预测
|
||||||
|
*/
|
||||||
|
void KalmanCreate(extKalman_t* p, float T_Q, float T_R)
|
||||||
|
{
|
||||||
|
p->X_last = (float)0;
|
||||||
|
p->P_last = 0;
|
||||||
|
p->Q = T_Q;
|
||||||
|
p->R = T_R;
|
||||||
|
p->A = 1;
|
||||||
|
p->B = 0;
|
||||||
|
p->H = 1;
|
||||||
|
p->X_mid = p->X_last;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name KalmanFilter
|
||||||
|
* @brief 卡尔曼滤波器
|
||||||
|
* @param p: 滤波器
|
||||||
|
* dat:待滤波数据
|
||||||
|
* @retval 滤波后的数据
|
||||||
|
* @attention Z(k)是系统输入,即测量值 X(k|k)是卡尔曼滤波后的值,即最终输出
|
||||||
|
* A=1 B=0 H=1 I=1 W(K) V(k)是高斯白噪声,叠加在测量值上了,可以不用管
|
||||||
|
* 以下是卡尔曼的5个核心公式
|
||||||
|
* 一阶H'即为它本身,否则为转置矩阵
|
||||||
|
*/
|
||||||
|
|
||||||
|
float KalmanFilter(extKalman_t* p, float dat)
|
||||||
|
{
|
||||||
|
p->X_mid = p->A * p->X_last; //百度对应公式(1) x(k|k-1) = A*X(k-1|k-1)+B*U(k)+W(K)
|
||||||
|
p->P_mid = p->A * p->P_last + p->Q; //百度对应公式(2) p(k|k-1) = A*p(k-1|k-1)*A'+Q
|
||||||
|
p->kg = p->P_mid / (p->P_mid + p->R); //百度对应公式(4) kg(k) = p(k|k-1)*H'/(H*p(k|k-1)*H'+R)
|
||||||
|
p->X_now = p->X_mid + p->kg * (dat - p->X_mid); //百度对应公式(3) x(k|k) = X(k|k-1)+kg(k)*(Z(k)-H*X(k|k-1))
|
||||||
|
p->P_now = (1 - p->kg) * p->P_mid; //百度对应公式(5) p(k|k) = (I-kg(k)*H)*P(k|k-1)
|
||||||
|
p->P_last = p->P_now; //状态更新
|
||||||
|
p->X_last = p->X_now;
|
||||||
|
return p->X_now; //输出预测结果x(k|k)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
#ifndef _KALMAN_H
|
||||||
|
#define _KALMAN_H
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
float X_last; //上一时刻的最优结果 X(k-|k-1)
|
||||||
|
float X_mid; //当前时刻的预测结果 X(k|k-1)
|
||||||
|
float X_now; //当前时刻的最优结果 X(k|k)
|
||||||
|
float P_mid; //当前时刻预测结果的协方差 P(k|k-1)
|
||||||
|
float P_now; //当前时刻最优结果的协方差 P(k|k)
|
||||||
|
float P_last; //上一时刻最优结果的协方差 P(k-1|k-1)
|
||||||
|
float kg; //kalman增益
|
||||||
|
float A; //系统参数
|
||||||
|
float B;
|
||||||
|
float Q;
|
||||||
|
float R;
|
||||||
|
float H;
|
||||||
|
} extKalman_t;
|
||||||
|
|
||||||
|
void KalmanCreate(extKalman_t* p, float T_Q, float T_R);
|
||||||
|
float KalmanFilter(extKalman_t* p, float dat);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#include <mmsystem.h>
|
||||||
|
#pragma comment(lib, "winmm.lib")
|
||||||
|
#pragma comment(lib, "user32.lib")
|
||||||
|
#define _USE_MATH_DEFINES
|
||||||
|
#include <cmath>
|
||||||
|
#else
|
||||||
|
#include <alsa/asoundlib.h>
|
||||||
|
#include <cmath>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#ifndef M_PI
|
||||||
|
#define M_PI 3.14159265358979323846
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef M_LN2
|
||||||
|
#define M_LN2 0.693147180559945309417
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Windows 回调结构
|
||||||
|
struct PlaybackContext {
|
||||||
|
volatile bool done;
|
||||||
|
WAVEHDR* header;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Windows 播放完成回调函数
|
||||||
|
void CALLBACK waveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance,
|
||||||
|
DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
|
||||||
|
if (uMsg == WOM_DONE) {
|
||||||
|
PlaybackContext* ctx = reinterpret_cast<PlaybackContext*>(dwInstance);
|
||||||
|
ctx->done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#define SND_EXPORT __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
#define SND_EXPORT
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SND_EXPORT bool play_frequency(float frequency, int32_t duration_ms,
|
||||||
|
double volume, int32_t channels,
|
||||||
|
int32_t sample_rate) {
|
||||||
|
// 参数验证
|
||||||
|
if (frequency <= 0.0f || duration_ms <= 0 ||
|
||||||
|
volume <= 0.0 || volume > 1.0 ||
|
||||||
|
channels <= 0 || channels > 8 ||
|
||||||
|
sample_rate <= 0) {
|
||||||
|
std::cerr << "Invalid parameters" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 精确计算帧数:使用 round 而非截断
|
||||||
|
// duration_ms / 1000.0 得到秒数,乘以采样率得到帧数
|
||||||
|
int64_t num_frames = static_cast<int64_t>(
|
||||||
|
std::round(static_cast<double>(sample_rate) * duration_ms / 1000.0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (num_frames <= 0) {
|
||||||
|
std::cerr << "Duration too short to generate any frames" << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算实际播放时长(用于调试)
|
||||||
|
double actual_duration_ms = static_cast<double>(num_frames) / sample_rate * 1000.0;
|
||||||
|
std::cout << "Requested duration: " << duration_ms << " ms, "
|
||||||
|
<< "Actual duration: " << actual_duration_ms << " ms, "
|
||||||
|
<< "Frames: " << num_frames << std::endl;
|
||||||
|
|
||||||
|
// 计算总采样点数
|
||||||
|
int32_t total_samples = static_cast<int32_t>(num_frames * channels);
|
||||||
|
std::vector<int16_t> buffer(total_samples);
|
||||||
|
|
||||||
|
// 计算振幅(应用对数音量缩放以获得更自然的音量控制)
|
||||||
|
int32_t amplitude = static_cast<int32_t>(32767.0 * volume);
|
||||||
|
|
||||||
|
// 生成正弦波数据
|
||||||
|
for (int64_t i = 0; i < num_frames; i++) {
|
||||||
|
// 使用 double 计算以提高精度
|
||||||
|
double t = static_cast<double>(i) / sample_rate;
|
||||||
|
double value = sin(2.0 * M_PI * frequency * t);
|
||||||
|
|
||||||
|
// 应用淡入淡出以避免咔嗒声
|
||||||
|
int64_t fade_samples = static_cast<int64_t>(sample_rate * 0.005); // 5ms 淡入淡出
|
||||||
|
double fade_factor = 1.0;
|
||||||
|
|
||||||
|
if (i < fade_samples) {
|
||||||
|
// 淡入:从 0 到 1
|
||||||
|
fade_factor = static_cast<double>(i) / fade_samples;
|
||||||
|
} else if (i >= num_frames - fade_samples) {
|
||||||
|
// 淡出:从 1 到 0
|
||||||
|
fade_factor = static_cast<double>(num_frames - 1 - i) / fade_samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t sample = static_cast<int16_t>(
|
||||||
|
amplitude * value * fade_factor
|
||||||
|
);
|
||||||
|
|
||||||
|
// 填充所有声道
|
||||||
|
for (int32_t ch = 0; ch < channels; ch++) {
|
||||||
|
buffer[i * channels + ch] = sample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// ===== Windows 实现(带回调,精确计时) =====
|
||||||
|
WAVEFORMATEX wfx;
|
||||||
|
wfx.wFormatTag = WAVE_FORMAT_PCM;
|
||||||
|
wfx.nChannels = channels;
|
||||||
|
wfx.nSamplesPerSec = sample_rate;
|
||||||
|
wfx.wBitsPerSample = 16;
|
||||||
|
wfx.nBlockAlign = channels * 2; // 16-bit = 2 bytes per sample
|
||||||
|
wfx.nAvgBytesPerSec = sample_rate * wfx.nBlockAlign;
|
||||||
|
wfx.cbSize = 0;
|
||||||
|
|
||||||
|
PlaybackContext context;
|
||||||
|
context.done = false;
|
||||||
|
context.header = new WAVEHDR();
|
||||||
|
|
||||||
|
// 打开波形设备,使用回调函数
|
||||||
|
HWAVEOUT hWaveOut;
|
||||||
|
MMRESULT result = waveOutOpen(
|
||||||
|
&hWaveOut,
|
||||||
|
WAVE_MAPPER,
|
||||||
|
&wfx,
|
||||||
|
reinterpret_cast<DWORD_PTR>(waveOutProc), // 回调函数
|
||||||
|
reinterpret_cast<DWORD_PTR>(&context), // 传递给回调的数据
|
||||||
|
CALLBACK_FUNCTION
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != MMSYSERR_NOERROR) {
|
||||||
|
std::cerr << "Cannot open audio device. Error code: " << result << std::endl;
|
||||||
|
delete context.header;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备 WAVEHDR
|
||||||
|
WAVEHDR& header = *context.header;
|
||||||
|
ZeroMemory(&header, sizeof(WAVEHDR));
|
||||||
|
header.lpData = reinterpret_cast<char*>(buffer.data());
|
||||||
|
header.dwBufferLength = static_cast<DWORD>(total_samples * sizeof(int16_t));
|
||||||
|
header.dwFlags = 0;
|
||||||
|
header.dwLoops = 0;
|
||||||
|
|
||||||
|
result = waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
|
||||||
|
if (result != MMSYSERR_NOERROR) {
|
||||||
|
std::cerr << "Failed to prepare audio header. Error code: " << result << std::endl;
|
||||||
|
waveOutClose(hWaveOut);
|
||||||
|
delete context.header;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入音频数据
|
||||||
|
result = waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR));
|
||||||
|
if (result != MMSYSERR_NOERROR) {
|
||||||
|
std::cerr << "Failed to write audio data. Error code: " << result << std::endl;
|
||||||
|
waveOutUnprepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
|
||||||
|
waveOutClose(hWaveOut);
|
||||||
|
delete context.header;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待播放完成(使用事件等待而非轮询)
|
||||||
|
auto start_time = timeGetTime();
|
||||||
|
while (!context.done) {
|
||||||
|
// 使用 MsgWaitForMultipleObjects 等待消息或超时
|
||||||
|
// 这样可以在等待的同时处理 Windows 消息
|
||||||
|
DWORD wait_result = MsgWaitForMultipleObjects(0, NULL, FALSE, 100, QS_ALLINPUT);
|
||||||
|
|
||||||
|
if (wait_result == WAIT_OBJECT_0) {
|
||||||
|
// 处理 Windows 消息
|
||||||
|
MSG msg;
|
||||||
|
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
|
||||||
|
TranslateMessage(&msg);
|
||||||
|
DispatchMessage(&msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超时检查(防止无限等待)
|
||||||
|
DWORD elapsed = timeGetTime() - start_time;
|
||||||
|
if (elapsed > static_cast<DWORD>(duration_ms + 1000)) {
|
||||||
|
std::cerr << "Playback timeout" << std::endl;
|
||||||
|
waveOutReset(hWaveOut);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto end_time = timeGetTime();
|
||||||
|
DWORD actual_play_time = end_time - start_time;
|
||||||
|
std::cout << "Actual playback time: " << actual_play_time << " ms" << std::endl;
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
waveOutUnprepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
|
||||||
|
waveOutClose(hWaveOut);
|
||||||
|
delete context.header;
|
||||||
|
|
||||||
|
#else
|
||||||
|
// ===== Linux ALSA 实现 =====
|
||||||
|
snd_pcm_t *pcm_handle = nullptr;
|
||||||
|
const char* device = "default";
|
||||||
|
|
||||||
|
int rc = snd_pcm_open(&pcm_handle, device, SND_PCM_STREAM_PLAYBACK, 0);
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Cannot open device '" << device << "': "
|
||||||
|
<< snd_strerror(rc) << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置硬件参数
|
||||||
|
snd_pcm_hw_params_t *hw_params;
|
||||||
|
snd_pcm_hw_params_alloca(&hw_params);
|
||||||
|
|
||||||
|
rc = snd_pcm_hw_params_any(pcm_handle, hw_params);
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Cannot get hardware parameters: " << snd_strerror(rc) << std::endl;
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = snd_pcm_hw_params_set_access(pcm_handle, hw_params,
|
||||||
|
SND_PCM_ACCESS_RW_INTERLEAVED);
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Cannot set access type: " << snd_strerror(rc) << std::endl;
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = snd_pcm_hw_params_set_format(pcm_handle, hw_params,
|
||||||
|
SND_PCM_FORMAT_S16_LE);
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Cannot set sample format: " << snd_strerror(rc) << std::endl;
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = snd_pcm_hw_params_set_channels(pcm_handle, hw_params, channels);
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Cannot set channel count: " << snd_strerror(rc) << std::endl;
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int hw_sample_rate = static_cast<unsigned int>(sample_rate);
|
||||||
|
rc = snd_pcm_hw_params_set_rate_near(pcm_handle, hw_params,
|
||||||
|
&hw_sample_rate, 0);
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Cannot set sample rate: " << snd_strerror(rc) << std::endl;
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hw_sample_rate != static_cast<unsigned int>(sample_rate)) {
|
||||||
|
std::cout << "Requested sample rate " << sample_rate
|
||||||
|
<< " Hz not supported, using " << hw_sample_rate << " Hz" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置缓冲区大小和周期
|
||||||
|
snd_pcm_uframes_t buffer_size;
|
||||||
|
rc = snd_pcm_hw_params_get_buffer_size_max(hw_params, &buffer_size);
|
||||||
|
if (buffer_size > static_cast<snd_pcm_uframes_t>(num_frames)) {
|
||||||
|
snd_pcm_hw_params_set_buffer_size_near(pcm_handle, hw_params,
|
||||||
|
&reinterpret_cast<snd_pcm_uframes_t&>(num_frames));
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = snd_pcm_hw_params(pcm_handle, hw_params);
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Cannot set hardware parameters: " << snd_strerror(rc) << std::endl;
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置软件参数
|
||||||
|
snd_pcm_sw_params_t *sw_params;
|
||||||
|
snd_pcm_sw_params_alloca(&sw_params);
|
||||||
|
|
||||||
|
rc = snd_pcm_sw_params_current(pcm_handle, sw_params);
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Cannot get software parameters: " << snd_strerror(rc) << std::endl;
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = snd_pcm_sw_params_set_start_threshold(pcm_handle, sw_params,
|
||||||
|
static_cast<snd_pcm_uframes_t>(num_frames));
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Cannot set start threshold: " << snd_strerror(rc) << std::endl;
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = snd_pcm_sw_params(pcm_handle, sw_params);
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Cannot set software parameters: " << snd_strerror(rc) << std::endl;
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放音频(写入数据)
|
||||||
|
snd_pcm_sframes_t frames_written = 0;
|
||||||
|
snd_pcm_uframes_t frames_to_write = static_cast<snd_pcm_uframes_t>(num_frames);
|
||||||
|
|
||||||
|
// 循环写入直到所有数据都播放完毕
|
||||||
|
while (frames_written < static_cast<snd_pcm_sframes_t>(num_frames)) {
|
||||||
|
snd_pcm_uframes_t remaining = static_cast<snd_pcm_uframes_t>(num_frames - frames_written);
|
||||||
|
|
||||||
|
rc = snd_pcm_writei(pcm_handle,
|
||||||
|
buffer.data() + (frames_written * channels),
|
||||||
|
remaining);
|
||||||
|
|
||||||
|
if (rc == -EPIPE) {
|
||||||
|
// 缓冲区下溢,需要恢复
|
||||||
|
std::cerr << "Buffer underrun occurred" << std::endl;
|
||||||
|
snd_pcm_prepare(pcm_handle);
|
||||||
|
rc = snd_pcm_writei(pcm_handle,
|
||||||
|
buffer.data() + (frames_written * channels),
|
||||||
|
remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Error writing to PCM device: " << snd_strerror(rc) << std::endl;
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rc > 0) {
|
||||||
|
frames_written += rc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待所有数据播放完成
|
||||||
|
rc = snd_pcm_drain(pcm_handle);
|
||||||
|
if (rc < 0) {
|
||||||
|
std::cerr << "Error draining PCM device: " << snd_strerror(rc) << std::endl;
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
snd_pcm_close(pcm_handle);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cuda_runtime.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#define CHECK(call) do {\
|
||||||
|
cudaError_t err = call;\
|
||||||
|
if (err != cudaSuccess) {\
|
||||||
|
fprintf(stderr, "CUDA error at %s:%d\n", __FILE__, __LINE__);\
|
||||||
|
fprintf(stderr, "%s\n", cudaGetErrorString(err));\
|
||||||
|
exit(EXIT_FAILURE);\
|
||||||
|
}\
|
||||||
|
} while (0)
|
||||||
@@ -0,0 +1,872 @@
|
|||||||
|
#include <cstdint>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <random>
|
||||||
|
#include "error.cuh"
|
||||||
|
extern "C" {
|
||||||
|
/* 创建一个设备数组 */
|
||||||
|
static void* _crtDevArr(const size_t m)
|
||||||
|
{
|
||||||
|
void *d;
|
||||||
|
CHECK(cudaMalloc(&d, m));
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 释放一个设备数组 */
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_Free(void* d)
|
||||||
|
#else
|
||||||
|
void Cu4cj_Free(void* d)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
CHECK(cudaFree(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 创建一个单精度浮点数组 输入数组的长度(不初始化) */
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* Cu4cj_Orig(uint64_t n)
|
||||||
|
#else
|
||||||
|
void* Cu4cj_Orig(int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
return _crtDevArr(n * sizeof(float));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 从主机内存拷贝数据到设备内存
|
||||||
|
* FIXME: 是否需要做成输入目的地址
|
||||||
|
*/
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) float* Cu4cj_ToDev(float* h, uint64_t n)
|
||||||
|
#else
|
||||||
|
float* Cu4cj_ToDev(float* h, int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
float* d = (float*)Cu4cj_Orig(n);
|
||||||
|
CHECK(cudaMemcpy(d, h, n * sizeof(float), cudaMemcpyHostToDevice));
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 从设备内存拷贝数据到主机内存 */
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_ToHost(float* d, float* h, const uint64_t n)
|
||||||
|
#else
|
||||||
|
void Cu4cj_ToHost(float* d, float* h, int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
CHECK(cudaMemcpy(h, d, n * sizeof(float), cudaMemcpyDeviceToHost));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_Copy(float* d, float* h, const uint64_t n)
|
||||||
|
#else
|
||||||
|
void Cu4cj_Copy(float* d, float* h, int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
CHECK(cudaMemcpy(h, d, n * sizeof(float), cudaMemcpyDeviceToHost));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 创建一个单精度浮点数组并将其初始化为零
|
||||||
|
* FIXME: 当n == 10000 时进程被杀死 */
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) float* Cu4cj_Zero(const uint64_t n)
|
||||||
|
#else
|
||||||
|
float* Cu4cj_Zero(int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
float* d = (float*)Cu4cj_Orig(n);
|
||||||
|
CHECK(cudaMemset(d, 0, n*sizeof(float)));
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 正态分布初始化(适用于神经网络权重) */
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) float* Cu4cj_Normal(uint64_t n, uint64_t fan_in, uint64_t fan_out)
|
||||||
|
#else
|
||||||
|
float* Cu4cj_Normal(int64_t n, int64_t fan_in, int64_t fan_out)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
// 计算 Xavier/Glorot 初始化的标准差
|
||||||
|
float std = sqrt(2.0f / (fan_in + fan_out));
|
||||||
|
|
||||||
|
// 先在主机上分配内存并生成正态分布随机数
|
||||||
|
float* h = (float*)malloc(n * sizeof(float));
|
||||||
|
|
||||||
|
/* 初始化随机数种子 */
|
||||||
|
srand(time(NULL));
|
||||||
|
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
// 使用 Box-Muller 变换生成正态分布随机数
|
||||||
|
float u1 = (float)rand() / RAND_MAX;
|
||||||
|
float u2 = (float)rand() / RAND_MAX;
|
||||||
|
float z = sqrt(-2.0f * log(u1)) * cos(2.0f * 3.1415926535f * u2);
|
||||||
|
h[i] = z * std;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在设备上分配内存
|
||||||
|
float* d = (float*)Cu4cj_Orig(n);
|
||||||
|
|
||||||
|
// 将主机内存中的数据拷贝到设备内存
|
||||||
|
CHECK(cudaMemcpy(d, h, n * sizeof(float), cudaMemcpyHostToDevice));
|
||||||
|
|
||||||
|
// 释放主机内存
|
||||||
|
free(h);
|
||||||
|
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 向量加法
|
||||||
|
static void __global__ _devAdd(float* d1, float* d2, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] + d2[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量加法
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_Add(float* d1, float* d2, float* d_r, uint64_t n)
|
||||||
|
#else
|
||||||
|
void Cu4cj_Add(float* d1, float* d2, float* d_r, int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
_devAdd<<<(n + 255) / 256, 256>>>(d1, d2, d_r, n);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量减法
|
||||||
|
static void __global__ _devSub(float* d1, float* d2, float* r, int64_t n)
|
||||||
|
{
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] - d2[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量减法
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_Sub(float* d1, float* d2, float* d_r, uint64_t n)
|
||||||
|
#else
|
||||||
|
void Cu4cj_Sub(float* d1, float* d2, float* d_r, int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
_devSub<<<(n + 255) / 256, 256>>>(d1, d2, d_r, n);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量依次相乘
|
||||||
|
static void __global__ _devMul(float* d1, float* d2, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] * d2[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量依次相乘
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_Mul(float* d1, float* d2, float* d_r, uint64_t n)
|
||||||
|
#else
|
||||||
|
void Cu4cj_Mul(float* d1, float* d2, float* d_r, int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
_devMul<<<(n + 255) / 256, 256>>>(d1, d2, d_r, n);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量依次相除
|
||||||
|
static void __global__ _devDiv(float* d1, float* d2, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] / d2[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量依次相除
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_Div(float* d1, float* d2, float* d_r, uint64_t n)
|
||||||
|
#else
|
||||||
|
void Cu4cj_Div(float* d1, float* d2, float* d_r, int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
_devDiv<<<(n + 255) / 256, 256>>>(d1, d2, d_r, n);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向量加法广播
|
||||||
|
static void __global__ _devAddBroadcast(float* d1, float a, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] + a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加法广播
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_AddBroadcast(float* d1, float a, float* d_r, uint64_t n)
|
||||||
|
#else
|
||||||
|
void Cu4cj_AddBroadcast(float* d1, float a, float* d_r, int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
_devAddBroadcast<<<(n + 255) / 256, 255>>>(d1, a, d_r, n);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 减法广播
|
||||||
|
static void __global__ _devSubBroadcast(float* d1, float a, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] - a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 减法广播
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_SubBroadcast(float* d1, float a, float* d_r, uint64_t n)
|
||||||
|
#else
|
||||||
|
void Cu4cj_SubBroadcast(float* d1, float a, float* d_r, int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
_devSubBroadcast<<<(n + 255) / 256, 256>>>(d1, a, d_r, n);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 乘法广播
|
||||||
|
static void __global__ _devMulBroadcast(float* d1, float a, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] * a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 乘法广播
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_MulBroadcast(float* d1, float a, float* d_r, uint64_t n)
|
||||||
|
#else
|
||||||
|
void Cu4cj_MulBroadcast(float* d1, float a, float* d_r, int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
_devMulBroadcast<<<(n + 255) / 256, 256>>>(d1, a, d_r, n);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 除法广播
|
||||||
|
static void __global__ _devDivBroadcast(float* d1, float a, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] / a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 除法广播
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_DivBroadcast(float* d1, float a, float* d_r, uint64_t n)
|
||||||
|
#else
|
||||||
|
void Cu4cj_DivBroadcast(float* d1, float a, float* d_r, int64_t n)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
_devDivBroadcast<<<(n + 255) / 256, 256>>>(d1, a, d_r, n);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 矩阵乘法
|
||||||
|
static void __global__ _devMatDot(float* d1, float* d2, float* r, int64_t m, int64_t n, int64_t k) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
int j = blockIdx.y * blockDim.y + threadIdx.y;
|
||||||
|
if (i < m && j < k) {
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (int p = 0; p < n; p++) {
|
||||||
|
sum += d1[i * n + p] * d2[p * k + j];
|
||||||
|
}
|
||||||
|
r[i * k + j] = sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 矩阵乘法
|
||||||
|
// m、n、k 分别为矩阵 A、B、C 的行数、列数
|
||||||
|
// 其中 n 为矩阵 A 的列数,也为矩阵 B 的行数
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void Cu4cj_MatDot(float* d1, float* d2, float* d_r, uint64_t m, uint64_t n, uint64_t k)
|
||||||
|
#else
|
||||||
|
void Cu4cj_MatDot(float* d1, float* d2, float* d_r, int64_t m, int64_t n, int64_t k)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
dim3 blockDim(16, 16);
|
||||||
|
dim3 gridDim((m + blockDim.x - 1) / blockDim.x, (k + blockDim.y - 1) / blockDim.y);
|
||||||
|
_devMatDot<<<gridDim, blockDim>>>(d1, d2, d_r, m, n, k);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 矩阵转置
|
||||||
|
static void __global__ _devTransport(float* d1, float* d2, int64_t m, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
int j = blockIdx.y * blockDim.y + threadIdx.y;
|
||||||
|
if (i < m && j < n) {
|
||||||
|
d2[j * m + i] = d1[i * n + j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) float* Cu4cj_Transport(float *m, uint64_t r, uint64_t c)
|
||||||
|
#else
|
||||||
|
float* Cu4cj_Transport(float *m, uint64_t r, uint64_t c)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
float *d_r = (float*)Cu4cj_Zero(c * r * sizeof(float));
|
||||||
|
_devTransport<<<1, c * r>>>(m, d_r, c, r);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
return d_r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Sigmoid 激活函数,输入为矩阵,输出为矩阵
|
||||||
|
static void __global__ _devSigmoid(float* d1, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = 1.0f / (1.0f + exp(-d1[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sigmoid 导数
|
||||||
|
static void __global__ _devSigmoidDerivative(float* d1, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] * (1.0f - d1[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReLU
|
||||||
|
static void __global__ _devReLU(float* d1, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] > 0.0f ? d1[i] : 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReLU 导数
|
||||||
|
static void __global__ _devReLUDerivative(float* d1, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] > 0.0f ? 1.0f : 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaky_ReLU
|
||||||
|
static void __global__ _devLeakyReLU(float* d1, float* r, int64_t n, float a) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] > 0.0f ? d1[i] : a * d1[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaky_ReLU 导数
|
||||||
|
static void __global__ _devLeakyReLU_Derivative(float* d1, float* r, int64_t n, float a) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = d1[i] > 0.0f ? 1.0f : a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tanh 激活函数,输入为矩阵,输出为矩阵
|
||||||
|
static void __global__ _devTanh(float* d1, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = (exp(d1[i]) - exp(-d1[i])) / (exp(d1[i]) + exp(-d1[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tanh 导数
|
||||||
|
static void __global__ _devTanhDerivative(float* d1, float* r, int64_t n) {
|
||||||
|
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
if (i < n) {
|
||||||
|
r[i] = 1.0f - d1[i] * d1[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace qing {
|
||||||
|
class Matrix {
|
||||||
|
public:
|
||||||
|
Matrix(uint64_t r, uint64_t c) {
|
||||||
|
this->r = r;
|
||||||
|
this->c = c;
|
||||||
|
this->d = (float*)Cu4cj_Zero(r * c * sizeof(float));
|
||||||
|
}
|
||||||
|
Matrix(uint64_t r, uint64_t c, float* d) {
|
||||||
|
this->r = r;
|
||||||
|
this->c = c;
|
||||||
|
this->d = d;
|
||||||
|
}
|
||||||
|
Matrix(const Matrix& m) {
|
||||||
|
this->r = m.getR();
|
||||||
|
this->c = m.getC();
|
||||||
|
this->d = (float*)Cu4cj_Zero(m.getR() * m.getC() * sizeof(float));
|
||||||
|
Cu4cj_Copy(m.getD(), d, m.getR() * m.getC() * sizeof(float));
|
||||||
|
}
|
||||||
|
~Matrix() {
|
||||||
|
Cu4cj_Free(d);
|
||||||
|
}
|
||||||
|
uint64_t getR() const {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
uint64_t getC() const {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
float* getD() const {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
float get(uint64_t r, uint64_t c) const {
|
||||||
|
float h;
|
||||||
|
CHECK(cudaMemcpy(&h, &d[c * this->r + r], sizeof(float), cudaMemcpyDeviceToHost));
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
void set(uint64_t r, uint64_t c, float v) {
|
||||||
|
CHECK(cudaMemcpy(&d[c * this->r + r], &v, sizeof(float), cudaMemcpyHostToDevice));
|
||||||
|
}
|
||||||
|
bool hasSameShape(const Matrix& m) {
|
||||||
|
return r == m.getR() && c == m.getC();
|
||||||
|
}
|
||||||
|
bool hasDotShape(const Matrix& m) {
|
||||||
|
return c == m.getR();
|
||||||
|
}
|
||||||
|
Matrix operator+(const Matrix& m) {
|
||||||
|
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
|
||||||
|
Cu4cj_Add(d, m.getD(), d_r, r * c);
|
||||||
|
return Matrix(r, c, d_r);
|
||||||
|
}
|
||||||
|
Matrix operator+(float a) {
|
||||||
|
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
|
||||||
|
Cu4cj_AddBroadcast(d, a, d_r, r * c);
|
||||||
|
return Matrix(r, c, d_r);
|
||||||
|
}
|
||||||
|
Matrix operator-(const Matrix& m) {
|
||||||
|
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
|
||||||
|
Cu4cj_Sub(d, m.getD(), d_r, r * c);
|
||||||
|
return Matrix(r, c, d_r);
|
||||||
|
}
|
||||||
|
Matrix operator-(float a) {
|
||||||
|
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
|
||||||
|
Cu4cj_SubBroadcast(d, a, d_r, r * c);
|
||||||
|
return Matrix(r, c, d_r);
|
||||||
|
}
|
||||||
|
Matrix operator*(const Matrix& m) {
|
||||||
|
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
|
||||||
|
Cu4cj_Mul(d, m.getD(), d_r, r * c);
|
||||||
|
return Matrix(r, c, d_r);
|
||||||
|
}
|
||||||
|
Matrix operator*(float a) {
|
||||||
|
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
|
||||||
|
Cu4cj_MulBroadcast(d, a, d_r, r * c);
|
||||||
|
return Matrix(r, c, d_r);
|
||||||
|
}
|
||||||
|
Matrix operator/(const Matrix& m) {
|
||||||
|
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
|
||||||
|
Cu4cj_Div(d, m.getD(), d_r, r * c);
|
||||||
|
return Matrix(r, c, d_r);
|
||||||
|
}
|
||||||
|
Matrix operator/(float a) {
|
||||||
|
float *d_r = (float*)Cu4cj_Zero(r * c * sizeof(float));
|
||||||
|
Cu4cj_DivBroadcast(d, a, d_r, r * c);
|
||||||
|
return Matrix(r, c, d_r);
|
||||||
|
}
|
||||||
|
Matrix dot(const Matrix& m) {
|
||||||
|
//if (!hasDotShape(m)) {
|
||||||
|
// throw std::invalid_argument("Dot product: matrices must have compatible shapes");
|
||||||
|
//}
|
||||||
|
float *d_r = (float*)Cu4cj_Zero(r * m.getC() * sizeof(float));
|
||||||
|
Cu4cj_MatDot(d, m.getD(), d_r, r, c, m.getC());
|
||||||
|
return Matrix(r, m.getC(), d_r);
|
||||||
|
}
|
||||||
|
Matrix T() {
|
||||||
|
float *d_r = (float*)Cu4cj_Transport(d, r, c);
|
||||||
|
return Matrix(c, r, d_r);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint64_t r, c;
|
||||||
|
float* d;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace qing;
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* createMatrix(const uint64_t r, const uint64_t c)
|
||||||
|
#else
|
||||||
|
void* createMatrix(int64_t r, int64_t c)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
return(void*) new Matrix(r, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void freeMatrix(void* m)
|
||||||
|
#else
|
||||||
|
void freeMatrix(void* m)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
delete (Matrix*)m;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* xavierNormalInit(uint64_t fan_in, uint64_t fan_out)
|
||||||
|
#else
|
||||||
|
void* xavierNormalInit(uint64_t fan_in, uint64_t fan_out)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
float* d = Cu4cj_Normal(fan_in * fan_out, fan_in, fan_out);
|
||||||
|
return (void *)new Matrix(fan_in, fan_out, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* xavierUniformInit(uint64_t fan_in, uint64_t fan_out)
|
||||||
|
#else
|
||||||
|
void* xavierUniformInit(uint64_t fan_in, uint64_t fan_out)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
uint64_t n = fan_in * fan_out;
|
||||||
|
|
||||||
|
// 先在主机上分配内存并生成正态分布随机数
|
||||||
|
float* h = (float*)malloc(n * sizeof(float));
|
||||||
|
|
||||||
|
/* 使用高质量的随机数生成器 */
|
||||||
|
static std::mt19937 generator(std::random_device{}());
|
||||||
|
|
||||||
|
/* 计算区间范围:sqrt(2 / (fan_in + fan_out)) */
|
||||||
|
float scale = std::sqrt(2.0f / (fan_in + fan_out));
|
||||||
|
|
||||||
|
/* 创建均匀分布对象,范围为[-scale, scale] */
|
||||||
|
std::uniform_real_distribution<float> distribution(-scale, scale);
|
||||||
|
|
||||||
|
/* 遍历权重矩阵,进行赋值 */
|
||||||
|
uint64_t k=0;
|
||||||
|
for (uint64_t i=0; i<fan_in; ++i)
|
||||||
|
for (uint64_t j=0; j<fan_out; ++j)
|
||||||
|
h[k++] = distribution(generator);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 在设备上分配内存
|
||||||
|
float* d = (float*)Cu4cj_Orig(n*sizeof(float));
|
||||||
|
|
||||||
|
// 将主机内存中的数据拷贝到设备内存
|
||||||
|
CHECK(cudaMemcpy(d, h, n * sizeof(float), cudaMemcpyHostToDevice));
|
||||||
|
|
||||||
|
// 释放主机内存
|
||||||
|
free(h);
|
||||||
|
|
||||||
|
return (void *)new Matrix(fan_in, fan_out, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) uint64_t getrowMatrix(void *m)
|
||||||
|
#else
|
||||||
|
uint64_t getrowMatrix(void *m)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
return m1->getR();
|
||||||
|
}
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) uint64_t getcolMatrix(void *m)
|
||||||
|
#else
|
||||||
|
uint64_t getcolMatrix(void *m)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
return m1->getC();
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) float getMatrix(void *m, uint64_t r, uint64_t c)
|
||||||
|
#else
|
||||||
|
float getMatrix(void *m, uint64_t r, uint64_t c)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
return m1->get(r, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void setMatrix(void *m, uint64_t r, uint64_t c, float v)
|
||||||
|
#else
|
||||||
|
void setMatrix(void *m, uint64_t r, uint64_t c, float v)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
m1->set(r, c, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) bool sameShapeMatrix(void *m, void *s)
|
||||||
|
#else
|
||||||
|
bool sameShapeMatrix(void *m, void *s)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix* s1 = (Matrix*)s;
|
||||||
|
return (*m1).hasSameShape(*s1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) bool dotShapeMatrix(void *m, void *s)
|
||||||
|
#else
|
||||||
|
bool dotShapeMatrix(void *m, void *s)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix* s1 = (Matrix*)s;
|
||||||
|
return (*m1).hasDotShape(*s1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* plusMatrix(void *m, void *s)
|
||||||
|
#endif
|
||||||
|
#ifdef __linux__
|
||||||
|
void* plusMatrix(void *m, void *s)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix* s1 = (Matrix*)s;
|
||||||
|
Matrix r = *m1 + *s1;
|
||||||
|
return (void *)new Matrix(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* plusBroadcastMatrix(void *m, float a)
|
||||||
|
#else
|
||||||
|
void* plusBroadcastMatrix(void *m, float a)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix r = *m1 + a;
|
||||||
|
return (void *)new Matrix(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* subMatrix(void *m, void *s)
|
||||||
|
#else
|
||||||
|
void* subMatrix(void *m, void *s)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix* s1 = (Matrix*)s;
|
||||||
|
Matrix r = *m1 - *s1;
|
||||||
|
return (void *)new Matrix(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* subBroadcastMatrix(void *m, float a)
|
||||||
|
#else
|
||||||
|
void* subBroadcastMatrix(void *m, float a)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix r = *m1 - a;
|
||||||
|
return (void *)new Matrix(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* multiMatrix(void *m, void *s)
|
||||||
|
#else
|
||||||
|
void* multiMatrix(void *m, void *s)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix* s1 = (Matrix*)s;
|
||||||
|
Matrix r = (*m1) * (*s1);
|
||||||
|
return (void *)new Matrix(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* multiBroadcastMatrix(void *m, float a)
|
||||||
|
#else
|
||||||
|
void* multiBroadcastMatrix(void *m, float a)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix r = (*m1) * a;
|
||||||
|
return (void *)new Matrix(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* divMatrix(void *m, void *s)
|
||||||
|
#else
|
||||||
|
void* divMatrix(void *m, void *s)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix* s1 = (Matrix*)s;
|
||||||
|
Matrix r = (*m1) / (*s1);
|
||||||
|
return (void *)new Matrix(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* divBroadcastMatrix(void *m, float a)
|
||||||
|
#else
|
||||||
|
void* divBroadcastMatrix(void *m, float a)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix r = (*m1) / a;
|
||||||
|
return (void *)new Matrix(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* dotMatrix(void *m, void *s)
|
||||||
|
#else
|
||||||
|
void* dotMatrix(void *m, void *s)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix* s1 = (Matrix*)s;
|
||||||
|
Matrix r = (*m1).dot(*s1);
|
||||||
|
return (void *)new Matrix(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* transposeMatrix(void *m)
|
||||||
|
#endif
|
||||||
|
#ifdef __linux__
|
||||||
|
void* transposeMatrix(void *m)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix r = m1->T();
|
||||||
|
return (void *)new Matrix(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* Sigmoid_do(void *m)
|
||||||
|
#else
|
||||||
|
void* Sigmoid_do(void *m)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix s = Matrix(*m1);
|
||||||
|
_devSigmoid<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
return (void *)new Matrix(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* Sigmoid_dot(void *m)
|
||||||
|
#else
|
||||||
|
void* Sigmoid_dot(void *m)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix s = Matrix(*m1);
|
||||||
|
_devSigmoidDerivative<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
return (void *)new Matrix(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* ReLU_do(void *m)
|
||||||
|
#else
|
||||||
|
void* ReLU_do(void *m)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix s = Matrix(*m1);
|
||||||
|
_devReLU<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
return (void *)new Matrix(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* ReLU_dot(void *m)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix s = Matrix(*m1);
|
||||||
|
_devReLUDerivative<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
return (void *)new Matrix(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* Leaky_ReLU_do(void *m, float a)
|
||||||
|
#else
|
||||||
|
void* LeakyReLU_do(void *m, float a)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix s = Matrix(*m1);
|
||||||
|
_devLeakyReLU<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC(), a);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
return (void *)new Matrix(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* Leaky_ReLU_dot(void *m, float a)
|
||||||
|
#else
|
||||||
|
void* LeakyReLU_dot(void *m, float a)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix s = Matrix(*m1);
|
||||||
|
_devLeakyReLU_Derivative<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC(), a);
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
return (void *)new Matrix(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tanh
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* Tanh_do(void *m)
|
||||||
|
#else
|
||||||
|
void* Tanh_do(void *m)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix s = Matrix(*m1);
|
||||||
|
_devTanh<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
return (void *)new Matrix(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN64
|
||||||
|
__declspec(dllexport) void* Tanh_dot(void *m)
|
||||||
|
#else
|
||||||
|
void* Tanh_dot(void *m)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Matrix* m1 = (Matrix*)m;
|
||||||
|
Matrix s = Matrix(*m1);
|
||||||
|
_devTanhDerivative<<<1, s.getR()* s.getC()>>>(s.getD(), s.getD(), s.getR()* s.getC());
|
||||||
|
CHECK(cudaGetLastError());
|
||||||
|
CHECK(cudaDeviceSynchronize());
|
||||||
|
return (void *)new Matrix(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
|
||||||
|
/* TODO(20260518-qing): 稍后我们专门做一个i2c代码仓库,这样就能够区分不同架构上面的int的大小
|
||||||
|
* TODO(20260615-qing): 有没有可能只做一个简单的封装不引入异常机制 */
|
||||||
|
|
||||||
|
package bot4cj.hd
|
||||||
|
import pthread4cj.*
|
||||||
|
import ctype4cj.*
|
||||||
|
|
||||||
|
/* -------- 我们将直接调用glibc中的一些函数 -------- */
|
||||||
|
foreign func ioctl(fd: C_Int, addr: C_ULong, ...): C_Int /* -------- 成功返回非负整数,失败返回-1 -------- */
|
||||||
|
|
||||||
|
const I2C_FAILED = -1i32 /* -------- I2C关闭文件失败 -------- */
|
||||||
|
const I2C_SUCCESS = 0i32 /* -------- I2C关闭文件成功 -------- */
|
||||||
|
const I2C_SLAVE = 0x0703u64 /* -------- at least on arm64 -------- */
|
||||||
|
|
||||||
|
/* -------- OPEN I2C DEVICE 打开I2C设备 -------- */
|
||||||
|
@C
|
||||||
|
public func openI2cDev(path: CString): Int32 {
|
||||||
|
unsafe {
|
||||||
|
let fd = libc_open(path, O_RDWR)
|
||||||
|
if (fd > 0) {
|
||||||
|
I2cLock_create(fd) /* -------- 为I2C设备创建锁 -------- */
|
||||||
|
return fd
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw I2cOpenedFailed("?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 简化版,判断在外部实现 ---- */
|
||||||
|
@C
|
||||||
|
public func openI2cDev1(path: CString): C_Int {
|
||||||
|
unsafe {
|
||||||
|
let fd = libc_open(path, O_RDWR)
|
||||||
|
I2cLock_create(fd)
|
||||||
|
return fd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- CLOSE I2C DEVICE 关闭I2C设备 -------- */
|
||||||
|
@C
|
||||||
|
public func closeI2cDev(fd: Int32): Unit {
|
||||||
|
unsafe {
|
||||||
|
I2cLock_destroy(fd) /* -------- 销毁I2C设备的锁 -------- */
|
||||||
|
let ret = libc_close(fd)
|
||||||
|
if (ret == I2C_FAILED) {
|
||||||
|
throw I2cClosedFailed("?") }
|
||||||
|
else if (ret == I2C_SUCCESS) { ; }
|
||||||
|
else {
|
||||||
|
throw I2cUnknowExp(
|
||||||
|
"?", "$function returned: {ret.toString()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@C
|
||||||
|
public func closeI2cDev1(fd: C_Int): Unit {
|
||||||
|
unsafe {
|
||||||
|
I2cLock_destroy(fd)
|
||||||
|
libc_close(fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- SET I2C ADDR 设置I2C地址 -------- */
|
||||||
|
@C
|
||||||
|
public func setI2cAddr(fd: Int32, addr: UInt8): Unit {
|
||||||
|
unsafe {
|
||||||
|
let ret = ioctl(fd, I2C_SLAVE, addr)
|
||||||
|
if (ret < 0) {
|
||||||
|
throw I2cSetAddressFailed("?") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@C
|
||||||
|
public func setI2cAddr1(fd: C_Int, addr: UInt8): C_Int {
|
||||||
|
unsafe {
|
||||||
|
return ioctl(fd, I2C_SLAVE, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- WRITE I2C REG 写I2C寄存器 -------- */
|
||||||
|
@C
|
||||||
|
public func writeI2cReg(fd: Int32, reg: UInt8, val:UInt8): Unit {
|
||||||
|
var a: VArray<UInt8, $2> = [reg, val]
|
||||||
|
var p: CPointer<Unit> = unsafe { CPointer<Unit>(inout a) }
|
||||||
|
if (2 != unsafe { libc_write(fd, p, 2) }) {
|
||||||
|
throw I2cWriteRegFailed("?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- READ I2C REG 读I2C寄存器 --------
|
||||||
|
* INFO(20260522-qing): 读取寄存器之前要先写寄存器地址
|
||||||
|
* INFO(20260616-qing): 这个函数只读取一个字节的数据
|
||||||
|
* -------- */
|
||||||
|
@C
|
||||||
|
public func readI2cReg(fd: Int32, reg: UInt8): Int8 {
|
||||||
|
var ptr = reg
|
||||||
|
if (1 != unsafe { libc_write(fd, CPointer<Unit>(inout ptr), 1) }) {
|
||||||
|
throw I2cReadRegFailed("?")
|
||||||
|
}
|
||||||
|
var value: Int8 = 0
|
||||||
|
var p = unsafe { CPointer<Unit>(inout value) }
|
||||||
|
let ret = unsafe { libc_read(fd, p, 1) }
|
||||||
|
if (ret != 1) { throw I2cReadRegFailed("?") }
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- I2C锁具 --------
|
||||||
|
* INFO(20260521-qing): 因为每个I2C接口在通信时具有原子化的需求
|
||||||
|
* - 需要一个互斥锁集合,在打开第一个I2C设备时被初始化,
|
||||||
|
* - 释放最后一个I2C设备时,将锁具释放
|
||||||
|
* - **每次进行I2C通信时,进行上锁操作** */
|
||||||
|
@C
|
||||||
|
struct I2C_Lock {
|
||||||
|
var i2c_fd: Int32 = 0 /* -------- 这个锁绑定的I2C设备文件句柄 -------- */
|
||||||
|
var lock = CPointer<UInt8>() /* -------- phread_mutex_t 锁对象 -------- */
|
||||||
|
var is_active = false /* -------- 该锁具是否被激活 --------*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var MAIN_LOCK = CPointer<UInt8>() /* -------- 主锁,操作锁具时使用 -------- */
|
||||||
|
var LOCKS = CPointer<I2C_Lock>() /* -------- 指向锁具内存的指针 -------- */
|
||||||
|
const LOCKS_SIZE: Int64 = 20 /* -------- 锁具的容量 --------*/
|
||||||
|
|
||||||
|
/* -------- 为一个I2C设备注册锁具 -------- */
|
||||||
|
@C
|
||||||
|
func I2cLock_create(fd: Int32): Unit {
|
||||||
|
unsafe {
|
||||||
|
/* -------- 初始化主要的锁 -------- */
|
||||||
|
if (MAIN_LOCK.isNull()) {
|
||||||
|
MAIN_LOCK = _mutex_create()
|
||||||
|
LOCKS = LibC.malloc<I2C_Lock>(count: LOCKS_SIZE)
|
||||||
|
for (i in 0..20) {
|
||||||
|
let tmp = I2C_Lock()
|
||||||
|
(LOCKS+i).write(tmp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 获取主锁 --------*/
|
||||||
|
_mutex_lock(MAIN_LOCK)
|
||||||
|
var flag = false
|
||||||
|
|
||||||
|
for (idx in 0..LOCKS_SIZE) {
|
||||||
|
var lock = (LOCKS+idx).read()
|
||||||
|
if (lock.is_active == false) {
|
||||||
|
lock.is_active = true
|
||||||
|
lock.i2c_fd = fd
|
||||||
|
lock.lock = _mutex_create()
|
||||||
|
_mutex_init(lock.lock)
|
||||||
|
(LOCKS+idx).write(lock)
|
||||||
|
flag = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mutex_unlock(MAIN_LOCK)
|
||||||
|
if (flag ) { return } /* -------- 出口 -------- */
|
||||||
|
throw OverflowException("LOCKS is over.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* -------- 为一个I2C设备注销锁具 -------- */
|
||||||
|
@C
|
||||||
|
func I2cLock_destroy(fd: Int32): Unit {
|
||||||
|
unsafe {
|
||||||
|
/* -------- 检查锁具有没有被初始化 --------*/
|
||||||
|
if (MAIN_LOCK.isNull()) {
|
||||||
|
throw NoneValueException("no i2c lock")
|
||||||
|
}
|
||||||
|
_mutex_lock(MAIN_LOCK)
|
||||||
|
|
||||||
|
var flag_any = false
|
||||||
|
var flag_target = false
|
||||||
|
for (idx in 0..LOCKS_SIZE) {
|
||||||
|
var lock = (LOCKS+idx).read()
|
||||||
|
if (lock.is_active == true && lock.i2c_fd == fd) {
|
||||||
|
lock.is_active = false
|
||||||
|
_mutex_destroy(lock.lock)
|
||||||
|
_mutex_free(lock.lock)
|
||||||
|
(LOCKS+idx).write(lock)
|
||||||
|
flag_target = true
|
||||||
|
/* -------- NOTE: 扫描整个锁具不终止 -------- */
|
||||||
|
}
|
||||||
|
else if (lock.is_active == true && lock.i2c_fd != fd) {
|
||||||
|
flag_any = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_mutex_unlock(MAIN_LOCK)
|
||||||
|
|
||||||
|
/* -------- 如果锁具里已经没有I2C锁, 就释放整个锁具 -------- */
|
||||||
|
if (!flag_any) {
|
||||||
|
LibC.free(LOCKS)
|
||||||
|
_mutex_free(MAIN_LOCK)
|
||||||
|
MAIN_LOCK = CPointer<UInt8>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 出口 -------- */
|
||||||
|
if (flag_target) {
|
||||||
|
return }
|
||||||
|
|
||||||
|
|
||||||
|
throw NoneValueException("i2c lock nor found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 为一个I2C锁具加锁 -------- */
|
||||||
|
@C
|
||||||
|
func I2cLock_lock(fd: Int32): Unit {
|
||||||
|
unsafe {
|
||||||
|
if (MAIN_LOCK.isNull()) {
|
||||||
|
throw NoneValueException("no i2c lock")
|
||||||
|
}
|
||||||
|
|
||||||
|
_mutex_lock(MAIN_LOCK)
|
||||||
|
|
||||||
|
var flag = false
|
||||||
|
for (idx in 0..LOCKS_SIZE) {
|
||||||
|
var lock = (LOCKS+idx).read()
|
||||||
|
if (lock.is_active == true && lock.i2c_fd == fd) {
|
||||||
|
_mutex_lock(lock.lock)
|
||||||
|
(LOCKS+idx).write(lock)
|
||||||
|
flag = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mutex_unlock(MAIN_LOCK)
|
||||||
|
if (flag) { return }
|
||||||
|
throw NoneValueException("i2c lock nor found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 为一个I2C锁具解锁 -------- */
|
||||||
|
@C
|
||||||
|
func I2cLock_unlock(fd: Int32): Unit {
|
||||||
|
unsafe {
|
||||||
|
if (MAIN_LOCK.isNull()) {
|
||||||
|
throw NoneValueException("no i2c lock")
|
||||||
|
}
|
||||||
|
_mutex_lock(MAIN_LOCK)
|
||||||
|
|
||||||
|
var flag = false
|
||||||
|
for (idx in 0..LOCKS_SIZE) {
|
||||||
|
var lock = (LOCKS+idx).read()
|
||||||
|
if (lock.is_active == true && lock.i2c_fd == fd) {
|
||||||
|
_mutex_unlock(lock.lock)
|
||||||
|
(LOCKS+idx).write(lock)
|
||||||
|
flag = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_mutex_unlock(MAIN_LOCK)
|
||||||
|
if (flag) {return}
|
||||||
|
throw NoneValueException("i2c lock nor found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- 原生I2C通信类--------
|
||||||
|
* INFO(20260521-qing): 直接调用glibc的接口 */
|
||||||
|
public class I2C_Orig {
|
||||||
|
private var fd: Int32 = -1
|
||||||
|
private var path: String
|
||||||
|
public init (path: String) {
|
||||||
|
this.path = path
|
||||||
|
let s1 = unsafe {
|
||||||
|
LibC.mallocCString(path) }
|
||||||
|
fd = unsafe { openI2cDev(s1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func _close(): Unit {
|
||||||
|
unsafe { closeI2cDev(fd) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setAddr(addr: UInt8) {
|
||||||
|
unsafe { setI2cAddr(fd, addr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func writeReg(reg: UInt8, val:UInt8): Unit {
|
||||||
|
unsafe { writeI2cReg(fd, reg, val) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func readReg(reg: UInt8): Int8 {
|
||||||
|
var value = unsafe { readI2cReg(fd, reg) }
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/*--------获取文件句柄--------*/
|
||||||
|
public func getFd(): Int32 {
|
||||||
|
return fd
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- I2C设备异常 -------- */
|
||||||
|
open private class I2cExcept <: Exception {
|
||||||
|
public I2cExcept(dev: String, msg: String) {
|
||||||
|
super('${dev}: ${msg}') }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 打开I2C设备失败 -------- */
|
||||||
|
private class I2cOpenedFailed <: I2cExcept {
|
||||||
|
public I2cOpenedFailed(dev: String) {
|
||||||
|
super(dev, "failed to open i2c device") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 关闭I2C设备失败 -------- */
|
||||||
|
private class I2cClosedFailed <: I2cExcept {
|
||||||
|
public I2cClosedFailed(dev: String) {
|
||||||
|
super(dev, "failed to close i2c device") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- I2C设置地址失败 -------- */
|
||||||
|
private class I2cSetAddressFailed <: I2cExcept {
|
||||||
|
public I2cSetAddressFailed(dev: String) {
|
||||||
|
super(dev, "failed to open i2c device") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- I2C写从机I2C失败 -------- */
|
||||||
|
private class I2cWriteRegFailed <: I2cExcept {
|
||||||
|
public I2cWriteRegFailed(dev: String) {
|
||||||
|
super(dev, "failed to write i2c reg") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- I2C读从机I2C失败 -------- */
|
||||||
|
private class I2cReadRegFailed <: I2cExcept {
|
||||||
|
public I2cReadRegFailed(dev: String) {
|
||||||
|
super(dev, "failed to read i2c reg") }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 未知I2C异常 -------- */
|
||||||
|
private class I2cUnknowExp <: I2cExcept {
|
||||||
|
public I2cUnknowExp(dev: String, msg: String) {
|
||||||
|
super(dev, msg) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
/* ----------------
|
||||||
|
* MPU6050陀螺仪与加速度计模块
|
||||||
|
* -
|
||||||
|
* TODO(20260516-qing): 陀螺仪需要卡尔曼滤波、通过四元数等方式获取姿态角
|
||||||
|
* - 所以应该使用一个C/C++线程来持续地采样,并且留一个接口进行获取
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot4cj.hd
|
||||||
|
import std.time.*
|
||||||
|
import std.math.*
|
||||||
|
import pthread4cj.*
|
||||||
|
import bot4cj.math.*
|
||||||
|
|
||||||
|
const ADDR_0: UInt8 = 0x68 /* 默认地址 */
|
||||||
|
const ADDR_1: UInt8 = 0x68 /* 地址2 */
|
||||||
|
const PWR_MGMT_1: UInt8 = 0x6B /* 电源管理寄存器1 */
|
||||||
|
const PWR_NOSLEEP: UInt8 = 0x00 /* 电源管理 - 非睡眠模式 */
|
||||||
|
const GYRO_CONF_REG: UInt8 = 0x1B /* 陀螺仪配置寄存器 */
|
||||||
|
const GYRO_FS_500DPS: UInt8 = 0x08 /* 陀螺仪量程 - 正负500度/秒 */
|
||||||
|
const GYRO_REG_X_HIGH: UInt8 = 0x43 /* x - 陀螺仪横轴 高字节 */
|
||||||
|
const GYRO_REG_X_LOW: UInt8 = 0x44
|
||||||
|
const GYRO_REG_Y_HIGH: UInt8 = 0x45 /* y */
|
||||||
|
const GYRO_REG_Y_LOW: UInt8 = 0x46
|
||||||
|
const GYRO_REG_Z_HIGH: UInt8 = 0x47 /* z */
|
||||||
|
const GYRO_REG_Z_LOW: UInt8 = 0x48
|
||||||
|
const ACCEL_CONF_REG: UInt8 = 0x1C /* 加速度计配置寄存器 */
|
||||||
|
const ACCEL_FS_8g: UInt8 = 0x10 /* 加速度计量程 正负8g */
|
||||||
|
const ACCEL_REG_X_HIGH: UInt8 = 0x3B
|
||||||
|
const ACCEL_REG_X_LOW: UInt8 = 0x3C
|
||||||
|
const ACCEL_REG_Y_HIGH: UInt8 = 0x3D
|
||||||
|
const ACCEL_REG_Y_LOW: UInt8 = 0x3E
|
||||||
|
const ACCEL_REG_Z_HIGH: UInt8 = 0x3F
|
||||||
|
const ACCEL_REG_Z_LOW: UInt8 = 0x40
|
||||||
|
|
||||||
|
/* -------- MPU6050传感器数据 -------- */
|
||||||
|
@C
|
||||||
|
public struct MPU6050_sensorData {
|
||||||
|
public var accel_x: Int16 = 0 /* 加速度计 */
|
||||||
|
public var accel_y: Int16 = 0
|
||||||
|
public var accel_z: Int16 = 0
|
||||||
|
public var gyro_x: Int16 = 0 /* 陀螺仪 */
|
||||||
|
public var gyro_y: Int16 = 0
|
||||||
|
public var gyro_z: Int16 = 0
|
||||||
|
public func print() { /* 格式化打印 */
|
||||||
|
println("[accel x, y, z] ${accel_x}, ${accel_y}, ${accel_z}")
|
||||||
|
println("[gyro x, y, z] ${gyro_x}, ${gyro_y}, ${gyro_z}")
|
||||||
|
println("--------------------")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- 设置MPU6050 -------- */
|
||||||
|
@C
|
||||||
|
func mpu6050_setup(i2c_fd: Int32, addr: UInt8) {
|
||||||
|
unsafe {
|
||||||
|
I2cLock_lock(i2c_fd)
|
||||||
|
setI2cAddr(i2c_fd, addr);
|
||||||
|
sleep(Duration.millisecond * 1)
|
||||||
|
writeI2cReg(i2c_fd, PWR_MGMT_1, PWR_NOSLEEP); /* 退出睡眠 */
|
||||||
|
writeI2cReg(i2c_fd, GYRO_CONF_REG, GYRO_FS_500DPS); /* ±500°/s */
|
||||||
|
writeI2cReg(i2c_fd, ACCEL_CONF_REG, ACCEL_FS_8g); /* ±8g */
|
||||||
|
I2cLock_unlock(i2c_fd)
|
||||||
|
sleep(Duration.millisecond * 100); /* 等待稳定 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@C
|
||||||
|
/* -------- 读取 16 位传感器数据(带符号)--------
|
||||||
|
* FIXME: 是否能够正确处理符号位 */
|
||||||
|
func read16BitReg(i2c_fd: Int32, high: UInt8, low: UInt8): Int16 {
|
||||||
|
unsafe {
|
||||||
|
let hh: Int16 = Int16(readI2cReg(i2c_fd, high))
|
||||||
|
let ll: Int16 = Int16(readI2cReg(i2c_fd, low))
|
||||||
|
return (hh << 8) | ll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 读取传感器数据 -------- */
|
||||||
|
@C
|
||||||
|
func mpu6050_readSensor(i2c_fd: Int32, addr: UInt8) {
|
||||||
|
var data = MPU6050_sensorData()
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
I2cLock_lock(i2c_fd)
|
||||||
|
setI2cAddr(i2c_fd, addr)
|
||||||
|
data.accel_x = read16BitReg(i2c_fd, ACCEL_REG_X_HIGH, ACCEL_REG_X_LOW);
|
||||||
|
data.accel_y = read16BitReg(i2c_fd, ACCEL_REG_Y_HIGH, ACCEL_REG_Y_LOW);
|
||||||
|
data.accel_z = read16BitReg(i2c_fd, ACCEL_REG_Z_HIGH, ACCEL_REG_Z_LOW);
|
||||||
|
data.gyro_x = read16BitReg(i2c_fd, GYRO_REG_X_HIGH, GYRO_REG_X_LOW);
|
||||||
|
data.gyro_y = read16BitReg(i2c_fd, GYRO_REG_Y_HIGH, GYRO_REG_Y_LOW);
|
||||||
|
data.gyro_z = read16BitReg(i2c_fd, GYRO_REG_Z_HIGH, GYRO_REG_Z_LOW);
|
||||||
|
I2cLock_unlock(i2c_fd)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- 计算PITCH角 -------- */
|
||||||
|
//@C
|
||||||
|
//func mpu6050_calcPitch()
|
||||||
|
|
||||||
|
/* -------- mpu6050主线程 --------
|
||||||
|
* TODO(20260525-qing): 使用互补滤波就够用了 */
|
||||||
|
const RAD_TO_DEG = 57.2956f32 /* ---- 弧度转换为角度的系数(180/π ≈ 57.2956) ---- */
|
||||||
|
//foreign func pthread_testcancel(): Unit
|
||||||
|
@C
|
||||||
|
func mpu6050_main(args: CPointer<Unit>): CPointer<Unit> {
|
||||||
|
|
||||||
|
/* -------- 获取传入线程的参数 -------- */
|
||||||
|
let param = unsafe{ CPointer<mpu6050_th_param>(args).read() }
|
||||||
|
|
||||||
|
/* -------- 为加速度计准备滤波器组 -------- */
|
||||||
|
var kalman_accel_x = extKalman(20.0f32, 200.0f32)
|
||||||
|
var kalman_accel_y = extKalman(20.0f32, 200.0f32)
|
||||||
|
var kalman_accel_z = extKalman(20.0f32, 200.0f32)
|
||||||
|
|
||||||
|
/* -------- 为陀螺仪准备滤波器组 -------- */
|
||||||
|
var kalman_gyro_x = extKalman(20.0f32, 200.0f32)
|
||||||
|
var kalman_gyro_y = extKalman(20.0f32, 200.0f32)
|
||||||
|
var kalman_gyro_z = extKalman(20.0f32, 200.0f32)
|
||||||
|
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
|
||||||
|
println("I am running. i2c_fd: ${param.i2c_fd}, addr: ${param.addr}")
|
||||||
|
//unsafe { _mutex_lock(mutex) }
|
||||||
|
|
||||||
|
/* -------- 获取传感器数据 -------- */
|
||||||
|
var data = unsafe { mpu6050_readSensor(param.i2c_fd, param.addr) }
|
||||||
|
|
||||||
|
/* -------- 为加速度计执行滤波操作 -------- */
|
||||||
|
let filtered_accel_x = kalman_accel_x.filter(Float32(data.accel_x))
|
||||||
|
let filtered_accel_y = kalman_accel_y.filter(Float32(data.accel_y))
|
||||||
|
let filtered_accel_z = kalman_accel_z.filter(Float32(data.accel_z))
|
||||||
|
|
||||||
|
/* -------- 为陀螺仪执行滤波操作 -------- */
|
||||||
|
let filtered_gyro_x = kalman_gyro_x.filter(Float32(data.gyro_x))
|
||||||
|
let filtered_gyro_y = kalman_gyro_y.filter(Float32(data.gyro_y))
|
||||||
|
let filtered_gyro_z = kalman_gyro_z.filter(Float32(data.gyro_z))
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- 计算Pitch(俯仰角)和Roll(翻滚角) -------- */
|
||||||
|
let roll = atan2(filtered_accel_y, filtered_accel_z) * RAD_TO_DEG
|
||||||
|
let pitch = atan(-filtered_accel_x / sqrt(filtered_accel_y * filtered_accel_y + filtered_accel_z * filtered_accel_z)) * RAD_TO_DEG
|
||||||
|
|
||||||
|
let roll1 = atan(filtered_accel_y / sqrt(filtered_accel_x * filtered_accel_x + filtered_accel_z * filtered_accel_z)) * RAD_TO_DEG
|
||||||
|
let pitch1 = atan2(-filtered_accel_x, filtered_accel_z) * RAD_TO_DEG
|
||||||
|
|
||||||
|
print("roll: ${roll}\t")
|
||||||
|
println("pitch: ${pitch}")
|
||||||
|
print("roll1: ${roll1}\t")
|
||||||
|
println("pitch1: ${pitch1}")
|
||||||
|
|
||||||
|
//unsafe { _mutex_unlock(mutex) }
|
||||||
|
sleep(Duration.millisecond * 10)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 返回NULL指针以占位 -------- */
|
||||||
|
return CPointer<Unit>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- mpu6050线程参数 --------
|
||||||
|
* INFO(20260522-qing): mutex字段是一个锁的指针,需要在MPU6050的构造函数中获取资源
|
||||||
|
* - 用于辅助别的线程获取值。 */
|
||||||
|
@C
|
||||||
|
struct mpu6050_th_param {
|
||||||
|
public var i2c_fd: Int32 = 0 /* i2c文件句柄 */
|
||||||
|
public var addr: UInt8 = 0 /* i2c设备地址 */
|
||||||
|
public var mutex = CPointer<UInt8>() /* 用于辅助别的线程获取值的锁 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- MPU6050陀螺仪、加速度计 --------
|
||||||
|
* - 输入并持有一个I2C文件句柄 */
|
||||||
|
public class MPU6050 {
|
||||||
|
|
||||||
|
/* -------- 主初始化器 --------
|
||||||
|
* FIXME(20260517-qing): 由于gc的原因,我们可能会手动构造RAII,
|
||||||
|
* - 将一部分初始化移动到别的函数 */
|
||||||
|
public init(i2c_fd: Int32, addr: UInt8) {
|
||||||
|
this.i2c_fd = i2c_fd
|
||||||
|
this.addr = addr
|
||||||
|
unsafe {
|
||||||
|
param = LibC.malloc<mpu6050_th_param>()
|
||||||
|
var tmp = mpu6050_th_param()
|
||||||
|
tmp.i2c_fd = i2c_fd
|
||||||
|
tmp.addr = addr
|
||||||
|
tmp.mutex = LibC.malloc<UInt8>()
|
||||||
|
param.write(tmp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 终结器 -------- */
|
||||||
|
~init() {
|
||||||
|
/* -------- 清除构造时申请的线程参数和锁 -------- */
|
||||||
|
unsafe {
|
||||||
|
var tmp = param.read()
|
||||||
|
LibC.free(tmp.mutex)
|
||||||
|
LibC.free(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 初始化该模块 -------- */
|
||||||
|
public func setup() {
|
||||||
|
unsafe { mpu6050_setup(i2c_fd, addr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/*----------------*/
|
||||||
|
private var i2c_fd: Int32 /* -------- I2C设备文件句柄 -------- */
|
||||||
|
private var addr: UInt8 /* -------- I2C设备地址 -------- */
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- 获取原始MPU6050传感器数据 -------- */
|
||||||
|
public func readSensor(): MPU6050_sensorData {
|
||||||
|
return unsafe { mpu6050_readSensor(i2c_fd, addr) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- mpu6050启动守护 --------
|
||||||
|
* TODO(20260520-qing): 放进类中
|
||||||
|
* --------------------------------- */
|
||||||
|
var param: CPointer<mpu6050_th_param> /* -------- 线程参数必须能够持久保存 -------- */
|
||||||
|
var pth: Option<Pth> = None /* -------- 线程对象 -------- */
|
||||||
|
public func startDaemon() { /* -------- 启动线程 -------- */
|
||||||
|
pth = Pth(mpu6050_main, CPointer<Unit>(param))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 停止线程 -------- */
|
||||||
|
public func stopDaemon() {
|
||||||
|
if (let Some(P) <- pth) {
|
||||||
|
P.close() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 等待线程终止 -------- */
|
||||||
|
public func waitDaemon() {
|
||||||
|
if (let Some(P) <- pth) {
|
||||||
|
var ptr: CPointer<Unit> = CPointer<Unit>()
|
||||||
|
P.join(CPointer<CPointer<Unit>>(CPointer<CPointer<Unit>>(ptr))) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package bot4cj.hd
|
||||||
|
import pthread4cj.*
|
||||||
|
|
||||||
|
/* pca9685舵机控制版 - bot4cj
|
||||||
|
* TODO(20260515-qing): 由于对统一接口的需求,设计基于回调的虚拟化硬件
|
||||||
|
* TODO(20260516-qing): 获取pca9685的中文或者英文参考资料
|
||||||
|
* NOTE(20260618-qing): 该代码已经被Lua脚本所取代,留着备用说不定以后会删除。 */
|
||||||
|
public class PCA9685 {
|
||||||
|
private static const ADDR_0: UInt8 = 0x40 /* PCA9685的i2c地址 */
|
||||||
|
private let i2c_fd: Int32
|
||||||
|
private let addr: UInt8
|
||||||
|
public init( i2c_fd: Int32,
|
||||||
|
addr!: UInt8 = ADDR_0) { /* PCA9685硬件地址 */
|
||||||
|
this.i2c_fd = i2c_fd
|
||||||
|
this.addr = addr
|
||||||
|
/* -------- 初始化PCA9685 --------
|
||||||
|
* FIXME(20260517-qing): 在旧版机器人代码移除以后,移除这一段 */
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 设置PWM频率为50Hz(舵机标准周期20ms0) --------
|
||||||
|
* TODO(20260516-qing): 不将数值写死 */
|
||||||
|
public func setup() {
|
||||||
|
unsafe {
|
||||||
|
I2cLock_lock(i2c_fd)
|
||||||
|
setI2cAddr(i2c_fd, addr);
|
||||||
|
writeI2cReg(i2c_fd, 0x00, 0x20); /* 开启AI(地址自增)和关闭SLEEP */
|
||||||
|
sleep(5000 * Duration.nanosecond); /* 等待振荡器启动 */
|
||||||
|
/* FIXME: 这一条是不是重复了 */
|
||||||
|
var freq: Float32 = 50.0;
|
||||||
|
var prescale_val: Float32 = 25000000.0 / (4096.0 * freq) - 1.0; /* 25MHz主频 */
|
||||||
|
var prescale: UInt8 = UInt8(prescale_val + 0.5);
|
||||||
|
|
||||||
|
writeI2cReg(i2c_fd, 0x00, 0x10); /* 进入SLEEP模式(允许改频率) */
|
||||||
|
writeI2cReg(i2c_fd, 0xFE, prescale); /* 写入预分频值 */
|
||||||
|
writeI2cReg(i2c_fd, 0x00, 0x20); /* 退出SLEEP */
|
||||||
|
|
||||||
|
I2cLock_unlock(i2c_fd)
|
||||||
|
}
|
||||||
|
sleep(5000 * Duration.nanosecond); /* 等待振荡器启动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- 设置指定通道的PWM信号(0-4095)-------- */
|
||||||
|
public func setPWM(channel: UInt8, on: UInt16, off: UInt16) {
|
||||||
|
var reg: UInt8 = 0x06 + 4 * channel; /* LED0 寄存器起始地址 */
|
||||||
|
unsafe {
|
||||||
|
writeI2cReg(i2c_fd, reg, UInt8(on & 0xFF));
|
||||||
|
writeI2cReg(i2c_fd, reg + 1, UInt8(on >> 8));
|
||||||
|
writeI2cReg(i2c_fd, reg + 2, UInt8(off & 0xFF));
|
||||||
|
writeI2cReg(i2c_fd, reg + 3, UInt8(off >> 8));
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 角度转PWM值(0°~180° → 0.5ms~2.5ms脉宽) -------- */
|
||||||
|
public func setAngle(channel: UInt8, angle: Float32) {
|
||||||
|
const min: Float32 = 0.5; /* 0°对应0.5ms */
|
||||||
|
const max: Float32 = 2.5; /* 180°对应2.5ms */
|
||||||
|
let width: Float32 = min + (max - min) * (angle / 180.0);
|
||||||
|
let off_val: UInt16 = UInt16(width * 4096.0 / 20.0); /* 20ms 周期 */
|
||||||
|
unsafe {
|
||||||
|
I2cLock_lock(i2c_fd)
|
||||||
|
setI2cAddr(i2c_fd, this.addr);
|
||||||
|
this.setPWM(channel, 0, off_val); /* ON=0, OFF=计算值 */
|
||||||
|
I2cLock_unlock(i2c_fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package bot4cj.hd
|
||||||
|
import std.math.*
|
||||||
|
/* -------- 舵机类 --------
|
||||||
|
* 含最大值、最小值、漂移值三个调整项
|
||||||
|
* 以及一个设置角度回调函数
|
||||||
|
*
|
||||||
|
* FIXME(20260618-qing): 在构造函数中设置角度最大值 */
|
||||||
|
public class Servo {
|
||||||
|
private var max: Float32
|
||||||
|
private var min: Float32
|
||||||
|
private var offset: Float32
|
||||||
|
private var callback_setAngle: (Float32) -> Unit
|
||||||
|
private static let MAX_ANGLE: Float32 = 180.0f32
|
||||||
|
public init(max!: Float32, min!: Float32, offset!: Float32,
|
||||||
|
callback_setAngle!: (Float32) -> Unit) {
|
||||||
|
this.max =max
|
||||||
|
this.min = min
|
||||||
|
this.offset = offset
|
||||||
|
this.callback_setAngle = callback_setAngle
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setAngle(angle: Float32) {
|
||||||
|
var a = angle
|
||||||
|
a += offset /* ---- 加上漂移值 ---- */
|
||||||
|
if (a > abs(max)) { a = abs(max)}
|
||||||
|
if (a < abs(min)) { a = abs(min)}
|
||||||
|
if (max < 0.0f32) { a = MAX_ANGLE - a }
|
||||||
|
callback_setAngle(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
macro package bot4cj.hd.extern
|
||||||
|
|
||||||
|
import std.fs.*
|
||||||
|
import std.io.*
|
||||||
|
import std.ast.*
|
||||||
|
|
||||||
|
public macro ExternHardware(attrTokens: Tokens, inputTokens: Tokens): Tokens {
|
||||||
|
let expr = LitConstExpr(attrTokens)
|
||||||
|
let file = File(expr.literal.value, OpenMode.Read)
|
||||||
|
let bytes = readToEnd(file)
|
||||||
|
file.close()
|
||||||
|
let sb = StringBuilder()
|
||||||
|
sb.appendFromUtf8(bytes)
|
||||||
|
let code = sb.toString()
|
||||||
|
return cangjieLex(code)
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package bot4cj.hd
|
||||||
|
import ctype4cj.*
|
||||||
|
|
||||||
|
const FBIOGET_VSCREENINFO = 0x4602u64
|
||||||
|
const FBIOGET_FSCREENINFO = 0x4600u64
|
||||||
|
const PROT_WRITE: C_Int = 0x2
|
||||||
|
const MAP_SHARED: C_Int = 0x01
|
||||||
|
|
||||||
|
/* --- 定义在I2C.cj里了 ---*/
|
||||||
|
//foreign func ioctl(fd: C_Int, request: C_ULong, ...): C_Int
|
||||||
|
/* ---- 内存映射 ---- */
|
||||||
|
public type off_t = C_LongLong
|
||||||
|
foreign func mmap(addr: CPointer<Unit>, length: UIntNative, prot: C_Int, flags: C_Int, fd: C_Int, offset: off_t): CPointer<Unit>
|
||||||
|
|
||||||
|
/* -------- 打开framebuffer帧缓冲设备 -------- */
|
||||||
|
@C
|
||||||
|
public func fb_open(fb_dev: CString): C_Int {
|
||||||
|
return unsafe { libc_open(fb_dev, O_RDWR) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 帧缓冲区固定参数 -------- */
|
||||||
|
@C
|
||||||
|
public struct fb_fix_screeninfo {
|
||||||
|
var id: VArray<C_Char, $16> /* 设备驱动名称字符串,比如"TT Builtin" */
|
||||||
|
var smem_start: C_ULong = 0 /* 帧缓冲内存(显存)的物理起始地址 */
|
||||||
|
/* (physical address) */
|
||||||
|
var smem_len: UInt32 = 0 /* 帧缓冲内存的总长度,单位字节 */
|
||||||
|
var type_: UInt32 = 0 /* 显存类型(fb存储布局)see FB_TYPE_* */
|
||||||
|
var type_aux: UInt32 = 0 /* 显存附加类型 */
|
||||||
|
var visual: UInt32 = 0 /* 色彩显示模式 see FB_VISUAL_* */
|
||||||
|
var xpanstep: UInt16 = 0 /* zero if no hardware panning */
|
||||||
|
var ypanstep: UInt16 = 0 /* zero if no hardware panning */
|
||||||
|
var ywrapstep: UInt16 = 0 /* zero if no hardware ywrap */
|
||||||
|
var line_length: UInt32 = 0 /* 显示屏上一行像素数据所占用的字节数 */
|
||||||
|
var mmio_start: C_ULong = 0 /* 内存映射I/O的物理起始地址 */
|
||||||
|
/* (physical address) */
|
||||||
|
var mmio_len: UInt32 = 0 /* 内存映射的区域长度,单位字节 */
|
||||||
|
var accel: UInt32 = 0 /* 硬件加速类型 */
|
||||||
|
/* specific chip/card we have */
|
||||||
|
var capabilities: UInt16 = 0 /* see FB_CAP_* */
|
||||||
|
var reserved: VArray<UInt16, $2> /* 保留字段,用于未来扩展,确保兼容 */
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
id = VArray<C_Char, $16> (repeat: 0)
|
||||||
|
reserved = VArray<UInt16, $2> (repeat: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 颜色字段偏移量的解释:所有偏移量均从右侧开始计算,
|
||||||
|
* 位于一个“像素”值内部,该像素值的宽度正好等于 'bits_per_pixel'
|
||||||
|
* (也就是说:你可以将偏移量作为 << 的右操作数使用)。
|
||||||
|
* 之后的像素就是一个比特流,会原封不动地写入显存。
|
||||||
|
*
|
||||||
|
* 对于伪彩色:所有颜色分量的偏移量和长度应该相同。
|
||||||
|
* 偏移量指定了调色板索引的最低有效位在像素值中的位置。
|
||||||
|
* 长度表示可用的调色板条目数量(即条目数 = 1 << 长度)。
|
||||||
|
*/
|
||||||
|
@C
|
||||||
|
public struct fb_bitfield {
|
||||||
|
var offset: UInt32 = 0 /* 位域的起始位置(从右侧算起)*/
|
||||||
|
var length: UInt32 = 0 /* 位域的长度(比特数) */
|
||||||
|
var msb_right: UInt32 = 0 /* != 0 : 最高有效位在右侧 */
|
||||||
|
/* (即低位在右) */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 帧缓冲区可变参数 -------- */
|
||||||
|
@C
|
||||||
|
public struct fb_var_screeninfo {
|
||||||
|
var xres: UInt32 = 0 /* visible resolution */
|
||||||
|
var yres: UInt32 = 0
|
||||||
|
var xres_virtual: UInt32 = 0 /* virtual resolution */
|
||||||
|
var yres_virtual: UInt32 = 0
|
||||||
|
var xoffset: UInt32 = 0 /* offset from virtual to visible */
|
||||||
|
var yoffset: UInt32 = 0 /* resolution */
|
||||||
|
|
||||||
|
var bits_per_pixel: UInt32 = 0 /* guess what */
|
||||||
|
var grayscale: UInt32 = 0 /* 0 = color, 1 = grayscale, */
|
||||||
|
/* >1 = FOURCC */
|
||||||
|
var red: fb_bitfield /* bitfield in fb mem if true color, */
|
||||||
|
var green: fb_bitfield /* else only length is significant */
|
||||||
|
var blue: fb_bitfield
|
||||||
|
var transp: fb_bitfield /* transparency */
|
||||||
|
|
||||||
|
var nonstd: UInt32 = 0 /* != 0 Non standard pixel format */
|
||||||
|
|
||||||
|
var activate: UInt32 = 0 /* see FB_ACTIVATE_* */
|
||||||
|
|
||||||
|
var height: UInt32 = 0 /* height of picture in mm */
|
||||||
|
var width: UInt32 = 0 /* width of picture in mm */
|
||||||
|
|
||||||
|
var accel_flags: UInt32 = 0 /* (OBSOLETE) see fb_info.flags */
|
||||||
|
|
||||||
|
/* Timing: All values in pixclocks, except pixclock (of course) */
|
||||||
|
var pixclock: UInt32 = 0 /* pixel clock in ps (pico seconds) */
|
||||||
|
var left_margin: UInt32 = 0 /* time from sync to picture */
|
||||||
|
var right_margin: UInt32 = 0 /* time from picture to sync */
|
||||||
|
var upper_margin: UInt32 = 0 /* time from sync to picture */
|
||||||
|
var lower_margin: UInt32 = 0
|
||||||
|
var hsync_len: UInt32 = 0 /* length of horizontal sync */
|
||||||
|
var vsync_len: UInt32 = 0 /* length of vertical sync */
|
||||||
|
var sync: UInt32 =0 /* see FB_SYNC_* */
|
||||||
|
var vmode: UInt32 = 0 /* see FB_VMODE_* */
|
||||||
|
var rotate: UInt32 = 0 /* angle we rotate counter clockwise */
|
||||||
|
var colorspace: UInt32 = 0 /* colorspace for FOURCC-based modes */
|
||||||
|
var reserved: VArray<UInt32, $4>/* Reserved for future compatibility */
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
red = fb_bitfield()
|
||||||
|
green = fb_bitfield()
|
||||||
|
blue = fb_bitfield()
|
||||||
|
transp = fb_bitfield()
|
||||||
|
reserved = VArray<UInt32, $4> (repeat: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 获取帧缓冲区可变参数 ---- */
|
||||||
|
@C
|
||||||
|
public func fb_get_var_info(fd: C_Int) {
|
||||||
|
var info = fb_var_screeninfo()
|
||||||
|
unsafe { ioctl(fd, FBIOGET_VSCREENINFO, inout info) }
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 获取帧缓冲区固定参数 ---- */
|
||||||
|
@C
|
||||||
|
public func fb_get_fix_info(fd: C_Int) {
|
||||||
|
var info = fb_fix_screeninfo()
|
||||||
|
unsafe { ioctl(fd, FBIOGET_FSCREENINFO, inout info) }
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 内存映射 ---- */
|
||||||
|
@C
|
||||||
|
public func fb_mmap(addr: CPointer<Unit>, length: UIntNative, fd: C_Int, offset: off_t): CPointer<Unit>
|
||||||
|
{
|
||||||
|
let buf = unsafe { mmap(addr, length, PROT_WRITE, MAP_SHARED, fd, offset) }
|
||||||
|
return buf
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package bot4cj.hd
|
||||||
|
import std.time.*
|
||||||
|
//foreign func play_frequency(frequency: Int32, duration_ms: Int32, sample_rate: Int32): Bool
|
||||||
|
//foreign func play_frequency(frequency: Float32, duration_ms: Int32, volume: Float64, channels: Int32, sample_rate: Int32): Bool
|
||||||
|
|
||||||
|
/* -------- 扬声器类 --------
|
||||||
|
* TODO: 一次打开设备,一段时间后再关闭 */
|
||||||
|
public class Spk {
|
||||||
|
private var channels: Int32
|
||||||
|
private var sample_rate: Int32
|
||||||
|
public init(channels!: Int32, sample_rate!: Int32) {
|
||||||
|
this.channels = channels
|
||||||
|
this.sample_rate = sample_rate
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 控制扬声器发声 -------- */
|
||||||
|
public func play(frequency: Float32, volume: Float64, durations_ms: Int32): Bool {
|
||||||
|
//unsafe { play_frequency(frequency, durations_ms, volume, channels, sample_rate) }
|
||||||
|
sleep(Duration.millisecond * Int64(durations_ms))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package bot4cj
|
||||||
|
import std.io.*
|
||||||
|
import std.fs.*
|
||||||
|
import std.env.*
|
||||||
|
import std.time.*
|
||||||
|
import std.argopt.*
|
||||||
|
import std.convert.*
|
||||||
|
import std.collection.*
|
||||||
|
import stdx.encoding.json.*
|
||||||
|
import bot4cj.hd.*
|
||||||
|
import bot4cj.math.*
|
||||||
|
import ctype4cj.*
|
||||||
|
|
||||||
|
/* ---- 神经网络输出路径 ---- */
|
||||||
|
var NNS_OUT: Option<String> = None
|
||||||
|
main(args: Array<String>): Int64 {
|
||||||
|
|
||||||
|
/* ---- 解析命令行参数 ---- */
|
||||||
|
let specs = [
|
||||||
|
ArgumentSpec.Long("help", ArgumentMode.NoValue), /* ---- 拉起帮助菜单 ---- */
|
||||||
|
ArgumentSpec.Short(r'i', ArgumentMode.RequiredValue), /* ---- 神经网络模型 - 输入 ---- */
|
||||||
|
ArgumentSpec.Short(r'o', ArgumentMode.RequiredValue), /* ---- 神经网络模型 - 输出 ---- */
|
||||||
|
ArgumentSpec.Long("test", ArgumentMode.RequiredValue) /* ---- 运行一个测试分支 - 需要输入测试类型 ---- */
|
||||||
|
]
|
||||||
|
|
||||||
|
/* ---- 执行解析 ---- */
|
||||||
|
let parsed = parseArguments(args, specs)
|
||||||
|
let options = parsed.options
|
||||||
|
|
||||||
|
//---------------------------
|
||||||
|
/* 读取设置项 */
|
||||||
|
|
||||||
|
/*---------------------------*/
|
||||||
|
/*---------------------------*/
|
||||||
|
/* 初始化神经网络 (init nn)
|
||||||
|
* TODO: 可以去掉内部初始化参数,使用矩阵式模板进行初始化 */
|
||||||
|
|
||||||
|
|
||||||
|
//var nns: ArrayList<Nn> =
|
||||||
|
//if (options.keys().contains("i")) {
|
||||||
|
// let filename = options['i']
|
||||||
|
// load_nns(filename) }
|
||||||
|
//else {
|
||||||
|
// let nn_in = ArrayList<Nn>()
|
||||||
|
// nn_in.add(Nn(41, 80, ActivationFuncs.Leaky_ReLU, 0.2f32))
|
||||||
|
// nn_in.add(Nn(80, 80, ActivationFuncs.Leaky_ReLU, 0.2f32))
|
||||||
|
// nn_in.add(Nn(80, 40, ActivationFuncs.Leaky_ReLU, 0.2f32))
|
||||||
|
// nn_in.add(Nn(40, 17, ActivationFuncs.Sigmoid, 0.2f32)) // 16个舵机加1个时延
|
||||||
|
// nn_in }
|
||||||
|
|
||||||
|
/* ---- 使用-o选项进行输出路径的选择 ---- */
|
||||||
|
if (options.contains("o")) {
|
||||||
|
NNS_OUT = Option<String>.Some(options['o'])
|
||||||
|
}
|
||||||
|
|
||||||
|
/*---------------------------*/
|
||||||
|
/*---------------------------*/
|
||||||
|
/* 分支选择 */
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- 拉起帮助手册
|
||||||
|
* -------- */
|
||||||
|
if (options.contains("help")) {
|
||||||
|
print_help()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 启动一个测试分支
|
||||||
|
* -------- */
|
||||||
|
else if (options.contains("test")) {
|
||||||
|
run_test(options["test"])
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 缺省行为:从标准输入获取动作脚本
|
||||||
|
* --------
|
||||||
|
* -------- TODO(20260519-qing): 进行脚本引入(相对路径也可,从**环境变量**的路径中搜寻也可) */
|
||||||
|
else {
|
||||||
|
let consoleReader = getStdIn()
|
||||||
|
let robot = RobotStruct(consoleReader)
|
||||||
|
try {
|
||||||
|
robot.active()
|
||||||
|
robot.run()
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
robot.deactive()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- NOTE(20260523-qing): 储存微调网络模型
|
||||||
|
* ---- */
|
||||||
|
// let consoleReader = getStdIn()
|
||||||
|
// run_action_drl(consoleReader, nns)
|
||||||
|
// if (options.contains("o")) {
|
||||||
|
// save_nns(nns, options['o']) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 打印帮助手册
|
||||||
|
* -------- */
|
||||||
|
func print_help()
|
||||||
|
{
|
||||||
|
println("这是一个机器人动作脚本解析器。")
|
||||||
|
println("使用管道输入动作脚本,示例:")
|
||||||
|
println("cat std.mt | ./bot")
|
||||||
|
println()
|
||||||
|
println("--i2c=/path/to/device\t选择i2c设备")
|
||||||
|
println()
|
||||||
|
println("-i 选择输入的模型的路径")
|
||||||
|
println("-o 设置输出的模型的路径")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 集成多种常规测试、临时测试
|
||||||
|
* -------- */
|
||||||
|
func run_test(key: String) {
|
||||||
|
|
||||||
|
/* ----------- 神经网络与门测试
|
||||||
|
* -----------
|
||||||
|
* ----------- INFO: 我们使用随机生成的动态数据集,这样会导致一些后果 */
|
||||||
|
if (key == "nn") {
|
||||||
|
let nn_t = ArrayList<Nn1>()
|
||||||
|
nn_t.add(Nn1(2, 4, 0.05f32, Af.Leaky_ReLU(FP32(0.01))))
|
||||||
|
nn_t.add(Nn1(4, 1, 0.05f32, Af.Sigmoid))
|
||||||
|
let batch = 1
|
||||||
|
let num =2000
|
||||||
|
let prt_num = 100
|
||||||
|
for (i in 0..(num*batch)) {
|
||||||
|
let m = Matrix1(1, 2)
|
||||||
|
let t = Matrix1(1, 1)
|
||||||
|
prepare_andDoor(m, t)
|
||||||
|
let l = train1(nn_t, m, t, 1.0f32) // 这个1.0是学习率的折扣,而非学习率本身
|
||||||
|
if (i % prt_num == prt_num-1) {
|
||||||
|
print("${i}\t:")
|
||||||
|
print("${l}")
|
||||||
|
println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
else if (key == "cnn") {
|
||||||
|
testCNN()
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (key == "cnn1") {
|
||||||
|
testGeometryCNN()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------- 测试解析矩阵,并且输出
|
||||||
|
* -------- */
|
||||||
|
else if (key == "matrix_o") {
|
||||||
|
let file = File("ss/test.mt", OpenMode.Read)
|
||||||
|
let robot = RobotStruct(file)
|
||||||
|
file.close()
|
||||||
|
let outf = File("ss/output.mt", OpenMode.Write)
|
||||||
|
robot.save(outf)
|
||||||
|
outf.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 解析测试输出的矩阵
|
||||||
|
* -------- */
|
||||||
|
else if (key == "matrix_2") {
|
||||||
|
let file = File("ss/output.mt", OpenMode.Read)
|
||||||
|
let robot = RobotStruct(file)
|
||||||
|
file.close()
|
||||||
|
robot.active()
|
||||||
|
robot.run()
|
||||||
|
robot.deactive()
|
||||||
|
let outf = File("ss/output_1.mt", OpenMode.Write)
|
||||||
|
robot.save(outf)
|
||||||
|
outf.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
// Adam.cj - Adam 优化器实现(添加全连接层支持)
|
||||||
|
package bot4cj.math
|
||||||
|
|
||||||
|
import std.math.*
|
||||||
|
import std.collection.*
|
||||||
|
|
||||||
|
public class Adam {
|
||||||
|
let learningRate: FP32
|
||||||
|
let beta1: FP32
|
||||||
|
let beta2: FP32
|
||||||
|
let epsilon: FP32
|
||||||
|
|
||||||
|
var m: HashMap<String, Matrix1>
|
||||||
|
var v: HashMap<String, Matrix1>
|
||||||
|
var t: Int64
|
||||||
|
|
||||||
|
// 完整参数构造函数
|
||||||
|
public init(
|
||||||
|
lr: FP32,
|
||||||
|
beta1: FP32,
|
||||||
|
beta2: FP32,
|
||||||
|
epsilon: FP32
|
||||||
|
) {
|
||||||
|
learningRate = lr
|
||||||
|
this.beta1 = beta1
|
||||||
|
this.beta2 = beta2
|
||||||
|
this.epsilon = epsilon
|
||||||
|
m = HashMap<String, Matrix1>()
|
||||||
|
v = HashMap<String, Matrix1>()
|
||||||
|
t = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认参数构造函数
|
||||||
|
public init() {
|
||||||
|
learningRate = FP32(0.001)
|
||||||
|
beta1 = FP32(0.9)
|
||||||
|
beta2 = FP32(0.999)
|
||||||
|
epsilon = FP32(0.00000001)
|
||||||
|
m = HashMap<String, Matrix1>()
|
||||||
|
v = HashMap<String, Matrix1>()
|
||||||
|
t = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅自定义学习率的构造函数
|
||||||
|
public init(lr: FP32) {
|
||||||
|
learningRate = lr
|
||||||
|
beta1 = FP32(0.9)
|
||||||
|
beta2 = FP32(0.999)
|
||||||
|
epsilon = FP32(0.00000001)
|
||||||
|
m = HashMap<String, Matrix1>()
|
||||||
|
v = HashMap<String, Matrix1>()
|
||||||
|
t = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算幂运算的辅助函数
|
||||||
|
*/
|
||||||
|
private func power(base: FP32, exp: Int64): FP32 {
|
||||||
|
var result = FP32(1.0)
|
||||||
|
for (i in 0..exp) {
|
||||||
|
result = result * base
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算平方根的辅助函数 - 使用牛顿迭代法
|
||||||
|
*/
|
||||||
|
private func sqrt(val: FP32): FP32 {
|
||||||
|
let x = val.getValue()
|
||||||
|
if (x <= 0.0) {
|
||||||
|
return FP32(0.0)
|
||||||
|
}
|
||||||
|
var guess = x
|
||||||
|
for (i in 0..10) {
|
||||||
|
guess = (guess + x / guess) / 2.0
|
||||||
|
}
|
||||||
|
return FP32(guess)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新矩阵参数(通用方法)
|
||||||
|
* @param param 要更新的参数矩阵
|
||||||
|
* @param grad 梯度矩阵
|
||||||
|
* @param key 参数在HashMap中的键名
|
||||||
|
*/
|
||||||
|
private func updateMatrix(
|
||||||
|
param: Matrix1,
|
||||||
|
grad: Matrix1,
|
||||||
|
key: String
|
||||||
|
): Unit {
|
||||||
|
// 初始化动量
|
||||||
|
let hasState = m.contains(key)
|
||||||
|
if (!hasState) {
|
||||||
|
let rows = param.get_row()
|
||||||
|
let cols = param.get_col()
|
||||||
|
m.add(key, Matrix1(rows, cols))
|
||||||
|
v.add(key, Matrix1(rows, cols))
|
||||||
|
}
|
||||||
|
|
||||||
|
let mMat = m.get(key).getOrThrow()
|
||||||
|
let vMat = v.get(key).getOrThrow()
|
||||||
|
|
||||||
|
let rows = param.get_row()
|
||||||
|
let cols = param.get_col()
|
||||||
|
|
||||||
|
for (r in 0..rows) {
|
||||||
|
for (c in 0..cols) {
|
||||||
|
let g = grad.get(r, c).getOrThrow()
|
||||||
|
|
||||||
|
// 更新一阶矩
|
||||||
|
let oldM = mMat.get(r, c).getOrThrow()
|
||||||
|
let newM = beta1 * oldM + (FP32(1.0) - beta1) * g
|
||||||
|
mMat.set(r, c, newM)
|
||||||
|
|
||||||
|
// 更新二阶矩
|
||||||
|
let oldV = vMat.get(r, c).getOrThrow()
|
||||||
|
let newV = beta2 * oldV + (FP32(1.0) - beta2) * g * g
|
||||||
|
vMat.set(r, c, newV)
|
||||||
|
|
||||||
|
// 偏差修正
|
||||||
|
let one = FP32(1.0)
|
||||||
|
let beta1Pow = power(beta1, t)
|
||||||
|
let beta2Pow = power(beta2, t)
|
||||||
|
|
||||||
|
let mHat = newM / (one - beta1Pow)
|
||||||
|
let vHat = newV / (one - beta2Pow)
|
||||||
|
|
||||||
|
// 更新参数
|
||||||
|
let oldParam = param.get(r, c).getOrThrow()
|
||||||
|
let update = learningRate * mHat / (sqrt(vHat) + epsilon)
|
||||||
|
param.set(r, c, oldParam - update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新卷积层参数
|
||||||
|
*/
|
||||||
|
public func updateConvLayer(layer: ConvLayer): Unit {
|
||||||
|
t += 1
|
||||||
|
|
||||||
|
// 更新卷积核
|
||||||
|
var kGrad = match (layer.kernelsGrad) {
|
||||||
|
case Some(val) => val
|
||||||
|
case _ => throw Exception("No kernel gradients")
|
||||||
|
}
|
||||||
|
|
||||||
|
for (oc in 0..layer.nOutputChannels) {
|
||||||
|
let key = "conv_kernel_" + oc.toString()
|
||||||
|
updateMatrix(layer.kernels[oc], kGrad[oc], key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新偏置
|
||||||
|
var bGrad = match (layer.biasGrad) {
|
||||||
|
case Some(val) => val
|
||||||
|
case _ => throw Exception("No bias gradient")
|
||||||
|
}
|
||||||
|
|
||||||
|
let biasKey = "conv_bias"
|
||||||
|
updateMatrix(layer.biases, bGrad, biasKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新全连接层参数(Nn1)
|
||||||
|
*/
|
||||||
|
public func updateFCLayer(layer: Nn1): Unit {
|
||||||
|
t += 1
|
||||||
|
|
||||||
|
// 更新权重
|
||||||
|
var wGrad = match (layer.weights_grad) {
|
||||||
|
case Some(val) => val
|
||||||
|
case _ => throw Exception("No weights gradient")
|
||||||
|
}
|
||||||
|
|
||||||
|
let weightKey = "fc_weights"
|
||||||
|
updateMatrix(layer.weights, wGrad, weightKey)
|
||||||
|
|
||||||
|
// 更新偏置
|
||||||
|
var bGrad = match (layer.bias_grad) {
|
||||||
|
case Some(val) => val
|
||||||
|
case _ => throw Exception("No bias gradient")
|
||||||
|
}
|
||||||
|
|
||||||
|
let biasKey = "fc_bias"
|
||||||
|
updateMatrix(layer.bias, bGrad, biasKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置优化器状态
|
||||||
|
*/
|
||||||
|
public func reset(): Unit {
|
||||||
|
m.clear()
|
||||||
|
v.clear()
|
||||||
|
t = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前迭代次数
|
||||||
|
*/
|
||||||
|
public func getStep(): Int64 {
|
||||||
|
t
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
package bot4cj.math
|
||||||
|
import std.math.*
|
||||||
|
|
||||||
|
/* -------- Activation Functions 激活函数族 --------
|
||||||
|
* TODO(20260516-qing): 增加序列化、反序列化工厂函数 */
|
||||||
|
public enum Af {
|
||||||
|
|
||||||
|
None | Sigmoid | ReLU | Leaky_ReLU(FP32) | Tanh | Softmax
|
||||||
|
|
||||||
|
/* -------- 执行激活函数 -------- */
|
||||||
|
public func forward(x: Matrix1) {
|
||||||
|
match(this) {
|
||||||
|
case None => none(x)
|
||||||
|
case Sigmoid => sigmoid(x)
|
||||||
|
case ReLU => relu(x)
|
||||||
|
case Leaky_ReLU(alpha) => leaky_relu(x, alpha)
|
||||||
|
case Tanh => tanh(x)
|
||||||
|
case Softmax => softmax(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 激活函数的导数 -------- */
|
||||||
|
public func backward(y: Matrix1) {
|
||||||
|
match(this) {
|
||||||
|
case None => none_dot(y)
|
||||||
|
case Sigmoid => sigmoid_dot(y)
|
||||||
|
case ReLU => relu_dot(y)
|
||||||
|
case Leaky_ReLU(alpha) => leaky_relu_dot(y, alpha)
|
||||||
|
case Tanh => tanh_dot(y)
|
||||||
|
case Softmax => softmax_jacobian(y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 序列化与反序列化 -------- */
|
||||||
|
public func get_type(): String {
|
||||||
|
return match(this) {
|
||||||
|
case None => "None"
|
||||||
|
case Sigmoid => "Sigmoid"
|
||||||
|
case ReLU => "ReLU"
|
||||||
|
case Leaky_ReLU(alpha) => "Leaky_ReLU"
|
||||||
|
case Tanh => "Tanh"
|
||||||
|
case Softmax => "Softmax"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* */
|
||||||
|
public func get_value(): FP32 {
|
||||||
|
return match(this) {
|
||||||
|
case None => FP32(0.0f32)
|
||||||
|
case Sigmoid => FP32(0.0f32)
|
||||||
|
case ReLU => FP32(0.0f32)
|
||||||
|
case Leaky_ReLU(alpha) => alpha
|
||||||
|
case Tanh => FP32(0.0f32)
|
||||||
|
case Softmax => FP32(0.0f32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 空的激活函数 -------- */
|
||||||
|
private static func none(M: Matrix1) {
|
||||||
|
let row = M.get_row()
|
||||||
|
let col = M.get_col()
|
||||||
|
let N = Matrix1(row, col)
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let x = M.get(r, c).getOrThrow()
|
||||||
|
let x1= x.getValue()
|
||||||
|
N.set(r, c, FP32(x1)) }
|
||||||
|
}
|
||||||
|
return N
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 空激活函数的导数 -------- */
|
||||||
|
private static func none_dot(M: Matrix1) {
|
||||||
|
let row = M.get_row()
|
||||||
|
let col = M.get_col()
|
||||||
|
let N = Matrix1(row, col)
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
N.set(r, c, FP32(1.0f32)) }
|
||||||
|
}
|
||||||
|
return N
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- S型生长曲线 -------- */
|
||||||
|
private static func sigmoid(M: Matrix1) {
|
||||||
|
let row = M.get_row()
|
||||||
|
let col = M.get_col()
|
||||||
|
let N = Matrix1(row, col)
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let x = M.get(r, c).getOrThrow()
|
||||||
|
let x1= x.getValue()
|
||||||
|
let x2 = 1.0f32 / (1.0f32 + exp(-x1))
|
||||||
|
N.set(r, c, FP32(x2)) }
|
||||||
|
}
|
||||||
|
return N
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- S型生长曲线的导数 -------- */
|
||||||
|
private static func sigmoid_dot(M: Matrix1) {
|
||||||
|
let row = M.get_row()
|
||||||
|
let col = M.get_col()
|
||||||
|
let N = Matrix1(row, col)
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let x = M.get(r, c).getOrThrow()
|
||||||
|
let x1= x.getValue()
|
||||||
|
let x2 = 1.0f32 / (1.0f32 + exp(-x1))
|
||||||
|
let x3 = x2 * (1.0f32 - x2)
|
||||||
|
N.set(r, c, FP32(x3)) }
|
||||||
|
}
|
||||||
|
return N
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 线性整流单元 -------- */
|
||||||
|
private static func relu(M: Matrix1) {
|
||||||
|
let row = M.get_row()
|
||||||
|
let col = M.get_col()
|
||||||
|
let N = Matrix1(row, col)
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let x = M.get(r, c).getOrThrow()
|
||||||
|
let x1= x.getValue()
|
||||||
|
let x2 =if (x1 > 0.0f32) {x1} else{0.0f32}
|
||||||
|
N.set(r, c, FP32(x2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return N
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 线性整流单元的导数 -------- */
|
||||||
|
private static func relu_dot(M: Matrix1) {
|
||||||
|
let row = M.get_row()
|
||||||
|
let col = M.get_col()
|
||||||
|
let N = Matrix1(row, col)
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let x = M.get(r, c).getOrThrow()
|
||||||
|
let x1= x.getValue()
|
||||||
|
let x2 =if (x1 > 0.0f32) {1.0f32} else{0.0f32}
|
||||||
|
N.set(r, c, FP32(x2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return N
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 带泄露的线性整流单元 -------- */
|
||||||
|
private static func leaky_relu(M: Matrix1, alpha: FP32) {
|
||||||
|
let row = M.get_row()
|
||||||
|
let col = M.get_col()
|
||||||
|
let N = Matrix1(row, col)
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let x = M.get(r, c).getOrThrow()
|
||||||
|
let x1= x.getValue()
|
||||||
|
let x2 =if (x1 > 0.0f32) {x1} else{x1 * alpha.getValue()}
|
||||||
|
N.set(r, c, FP32(x2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return N
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 带泄露的整流单元导数 -------- */
|
||||||
|
private static func leaky_relu_dot(M: Matrix1, alpha: FP32) {
|
||||||
|
let row = M.get_row()
|
||||||
|
let col = M.get_col()
|
||||||
|
let N = Matrix1(row, col)
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let x = M.get(r, c).getOrThrow()
|
||||||
|
let x1= x.getValue()
|
||||||
|
let x2 =if (x1 > 0.0f32) { 1.0f32 } else{alpha.getValue()}
|
||||||
|
N.set(r, c, FP32(x2)) }
|
||||||
|
}
|
||||||
|
return N
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 双曲正切函数 --------*/
|
||||||
|
private static func tanh(M: Matrix1) {
|
||||||
|
let row = M.get_row()
|
||||||
|
let col = M.get_col()
|
||||||
|
let N = Matrix1(row, col)
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let x = M.get(r, c).getOrThrow()
|
||||||
|
let x1= x.getValue()
|
||||||
|
let x2 = tanh(x1)
|
||||||
|
N.set(r, c, FP32(x2)) }
|
||||||
|
}
|
||||||
|
return N
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 双曲正切函数的导数 -------- */
|
||||||
|
private static func tanh_dot(M: Matrix1) {
|
||||||
|
let row = M.get_row()
|
||||||
|
let col = M.get_col()
|
||||||
|
let N = Matrix1(row, col)
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let y = M.get(r, c).getOrThrow()
|
||||||
|
let y_val= y.getValue() /*---- NOTE(20260525-qing): 修复命名(x => y) ----*/
|
||||||
|
let rea = 1.0f32 - y_val * y_val
|
||||||
|
N.set(r, c, FP32(rea)) }
|
||||||
|
}
|
||||||
|
return N
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 归一化指数函数 -------- */
|
||||||
|
private static func softmax(M: Matrix1) {
|
||||||
|
let row = M.get_row()
|
||||||
|
let col = M.get_col()
|
||||||
|
let N = Matrix1(row, col)
|
||||||
|
var max_val = 0.0f32
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let x = M.get(r, c).getOrThrow()
|
||||||
|
let x1= x.getValue()
|
||||||
|
if (x1 > max_val) {
|
||||||
|
max_val = x1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var sum = 0.0f32
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let x = M.get(r, c).getOrThrow()
|
||||||
|
let x1= x.getValue()
|
||||||
|
let x2 = exp(x1 - max_val)
|
||||||
|
N.set(r, c, FP32(x2))
|
||||||
|
sum += x2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (r in 0..row) {
|
||||||
|
for (c in 0..col) {
|
||||||
|
let x = N.get(r, c).getOrThrow()
|
||||||
|
let x1= x.getValue()
|
||||||
|
let x2 = x1 / sum
|
||||||
|
N.set(r, c, FP32(x2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return N
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- 计算Softmax的雅可比矩阵 --------
|
||||||
|
* 输入:已计算好的softmax输出(概率分布)
|
||||||
|
* 输出:雅可比矩阵 J[i][j] = ∂y_i/∂x_j */
|
||||||
|
private func softmax_jacobian(softmax_output: Matrix1) {
|
||||||
|
let row = softmax_output.get_row()
|
||||||
|
let col = softmax_output.get_col()
|
||||||
|
let n = row * col
|
||||||
|
var jacobian = Matrix1(n, n)
|
||||||
|
|
||||||
|
for (i in 0..n) {
|
||||||
|
for (j in 0..n) {
|
||||||
|
|
||||||
|
let row_i = i / col /* 需要将一维索引转换为二维索引来访问Matrix1 */
|
||||||
|
let col_i = i % col
|
||||||
|
let row_j = j / col
|
||||||
|
let col_j = j % col
|
||||||
|
|
||||||
|
let val_i = softmax_output.get(row_i, col_i).getOrThrow().getValue()
|
||||||
|
let val_j = softmax_output.get(row_j, col_j).getOrThrow().getValue()
|
||||||
|
|
||||||
|
if (i == j) { /* 对角线元素: y_i * (1 - y_i) */
|
||||||
|
jacobian.set(i, j, FP32(val_i * (1.0f32 - val_i))) }
|
||||||
|
else { /* 非对角线元素: -y_i * y_j */
|
||||||
|
jacobian.set(i, j, FP32(-val_i * val_j)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jacobian
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package bot4cj.math
|
||||||
|
import std.random.*
|
||||||
|
import std.time.*
|
||||||
|
import std.math.*
|
||||||
|
|
||||||
|
/* -------- 神经网络使用的权重矩阵初始化类 --------
|
||||||
|
* TODO(20260517-qing): 将该函数改为枚举类,统一一个接口,根据模式匹配执行初始化
|
||||||
|
* - 这样在矩阵式中可以定义初始化方式 */
|
||||||
|
public class Ini {
|
||||||
|
|
||||||
|
/* -------- xavier正态初始化 -------- */
|
||||||
|
public static func xavierNormalInit(target: Matrix1, fan_in: Int64, fan_out: Int64) {
|
||||||
|
let seed = DateTime.now().nanosecond
|
||||||
|
let random = Random(UInt64(seed))
|
||||||
|
let sigma = sqrt(2.0f32 / Float32(fan_in + fan_out)) /* 计算标准差 */
|
||||||
|
for (r in 0..fan_in) {
|
||||||
|
for (c in 0..fan_out) {
|
||||||
|
let v = random.nextGaussianFloat32(mean: 0.0f32, sigma:sigma)
|
||||||
|
target.set(r, c, FP32(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------- Glorot均匀初始化 -------- */
|
||||||
|
public static func xavierUniformInit(target: Matrix1, fan_in: Int64, fan_out: Int64) {
|
||||||
|
let limit = sqrt(6.0f32 / Float32(fan_in + fan_out))
|
||||||
|
let seed = DateTime.now().nanosecond
|
||||||
|
let random = Random(UInt64(seed))
|
||||||
|
|
||||||
|
for (r in 0..fan_in) {
|
||||||
|
for (c in 0..fan_out) {
|
||||||
|
let v = (random.nextFloat32() * 2.0f32 - 1.0f32) * limit /* 均匀分布 [-limit, limit] */
|
||||||
|
target.set(r, c, FP32(v)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 559 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user