285 Commits

Author SHA1 Message Date
SikongJueluo
28ba709adf fix: 修复数码管时常无法使用的问题 2025-08-22 05:03:01 +08:00
SikongJueluo
6302489f3a feat: 增加示波器探测参数显示,增加旋转编码器按下的功能 2025-08-22 04:05:00 +08:00
SikongJueluo
7d3ef598de fix: 修复示波器修改后无法配置的问题;修复无法生成api的问题;feat: 新增全局控制七段数码管 2025-08-22 02:17:30 +08:00
SikongJueluo
8fbd30e69f fix: 修复一系列bug,包括热启动,前端模板,jtag; feat:修改矩阵键盘样式 2025-08-21 22:59:29 +08:00
SikongJueluo
78dcc5a629 fix: 修改commandID,并修复七段数码管的配置问题 2025-08-21 20:58:23 +08:00
SikongJueluo
e5b492247c fix: 修复Switch会在每次刷新后发送请求 2025-08-21 19:40:19 +08:00
SikongJueluo
e3b7cc4f63 fix: 修复由于基本的通信协议更改不完全导致的无法控制电源与jtag的问题 2025-08-21 19:19:56 +08:00
8ab55f411d fix: 修复编码器需要重新开关才能使用的问题 2025-08-20 21:04:54 +08:00
02af59c37e fix: 修复示波器与数码管无法关闭的问题 2025-08-20 17:20:52 +08:00
0932c8ba75 fix: 尝试修复hub的scantask的key的问题 2025-08-20 16:55:12 +08:00
4c9b9cd3d6 fix: 尝试修复示波器与旋转编码器无法工作的问题 2025-08-20 16:40:38 +08:00
62c16c016d fix: 尝试修复示波器无法关闭的问题 2025-08-20 16:02:58 +08:00
f23a8a9712 fix: 修复示波器WebSocket的问题 2025-08-20 15:28:58 +08:00
ec84eeeaa4 feat: 新增重置控制端的功能;前端可以显示提交记录 fix: 修复资源数据库sha256计算问题;修复资源数据库无法上传的问题 2025-08-20 10:20:30 +08:00
alivender
c8444d1d4e fix: 修改部分外设BASE偏移量;增加WS2812后端监控器;DSO寄 2025-08-20 01:37:27 +08:00
ca0322137b feat: 前端增加提交功能 2025-08-19 21:02:49 +08:00
2aef180ddb feat: 调整路由,实现页面跳转 2025-08-19 17:28:20 +08:00
228e87868d feat: 实现lazy load,加快加载速度;美化界面 2025-08-19 16:15:12 +08:00
3c73aa344a feat: 使用DDR读取Hdmi视频流 2025-08-19 15:20:17 +08:00
7e53b805ae feat: 使用SignalR实时发送示波器数据,并美化示波器界面 2025-08-19 12:55:18 +08:00
1b5b0e28e3 fix: 修复摄像头无法正常启动,以及关闭摄像头会导致后端崩溃的问题 2025-08-18 19:14:05 +08:00
alivender
7265b10870 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-08-18 16:15:10 +08:00
alivender
f548462472 fix: 修改示波器后端 2025-08-18 16:15:08 +08:00
283bf2a956 fix: 适配usb摄像头,当似乎没有正常工作 2025-08-18 15:15:41 +08:00
3c52110a2f feat: 迁移信号发生器前端至底栏 2025-08-17 20:30:35 +08:00
cbb83d3dcd fix: 修复旋转编码器地址与控制问题 2025-08-17 17:09:42 +08:00
4a55143b8e feat: 完成前后端旋转编码器的数字孪生 2025-08-17 17:01:42 +08:00
alivender
cbf85165b7 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-08-17 14:55:41 +08:00
alivender
fdfc5729ec feat&fix: 完善JPEGClient逻辑,前端新增编码器组件 2025-08-17 14:55:38 +08:00
8e69c96891 fix: 调整进度条的步幅 2025-08-17 14:23:35 +08:00
caa26c729e fix: 前端修复拨码开关第一个无法开关的问题;后端修复进度条停止在3%的问题 2025-08-17 13:46:14 +08:00
55edfd771e fix: 修复进度条的问题 2025-08-17 13:33:11 +08:00
97b86acfa8 fix: 修复拨码开关数字孪生无法正常工作的问题 2025-08-17 12:29:46 +08:00
b6720d867d feat: 实现拨动开关的数字孪生 2025-08-16 16:01:10 +08:00
a2ac1bcb3b fix: 修复比特流下载失败的问题 2025-08-16 15:55:14 +08:00
e61cf96c07 refactor: 使用更简洁的方式进行认证 2025-08-16 14:53:28 +08:00
c974de593a fix: 尝试去修复后端无法停止扫描数码管的问题 2025-08-16 14:21:26 +08:00
9bd3fb29e3 fix: 前后端修复七段数码管无法正常工作的问题 2025-08-16 13:05:01 +08:00
0a1e0982c2 feat: 前端七段数码管添加数字孪生功能 2025-08-16 11:56:27 +08:00
3644c75304 feat: 完成jpeg读取后端 2025-08-15 21:04:50 +08:00
774c9575d4 fix: 修复后端数码管无法正常读取/关闭的问题 2025-08-15 15:25:37 +08:00
a00cc84e48 fix: 修复数据库与SignalR无法连接的问题 2025-08-15 13:02:56 +08:00
6fa7fffa7f fix: 修复网络配置失败的问题 2025-08-14 20:31:43 +08:00
56eeb5dce3 feat: 完成数码管websocket通信 2025-08-14 20:25:32 +08:00
7bfc362b1f feat: 完成七段数码管后端 2025-08-14 15:21:18 +08:00
alivender
0e07a5996a feat: 合并冲突 2025-08-14 15:08:41 +08:00
alivender
4b2afe13db Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-08-14 15:04:57 +08:00
9af4546a11 fix: 修复调整resource manager接口导致无法过编译的问题 2025-08-14 14:22:06 +08:00
66bc5882af feat: 完成jpeg后端 2025-08-14 14:19:46 +08:00
e5dac3e731 feat: 完成提交作业的后端 2025-08-14 11:38:09 +08:00
24622d30cf feat: 使首页的教程placehold支持中文,同时使markdown编辑器同app主题变化 2025-08-14 11:37:30 +08:00
c4b3a09198 feat: 添加Markdown编辑器 2025-08-13 19:27:09 +08:00
7a59c29e06 feat: 实现可编辑已有的实验 2025-08-13 16:11:06 +08:00
76342553ad feat: 认证管理实时获取token 2025-08-13 14:36:01 +08:00
efcdee2109 chore: 移除无用的库 2025-08-13 14:34:50 +08:00
37156c937a fix: 修复signalR无法认证的问题 2025-08-13 14:32:41 +08:00
6e84953740 refactor: 重新调整exam页面 2025-08-11 17:01:24 +08:00
b09961473e fix: 修复主题无法保存的问题 2025-08-11 16:21:25 +08:00
ed9eacf33f fix: 修复数据库无法正常获取信息的问题 2025-08-11 13:34:21 +08:00
c1d641c20c refactor: 视频流前后端适配 2025-08-11 13:09:30 +08:00
b95a61c532 refactor: 重构数据库相关操作 2025-08-10 20:13:44 +08:00
079004c17d fix: 修复生成api时,缺失main.ts的问题 2025-08-10 20:13:12 +08:00
11ef4dfba6 refactor: 重构videostream; fix: 修复进度条guid无法生成的问题 2025-08-10 20:07:37 +08:00
bbde060d11 feat: 完成基本的Jpeg控制 2025-08-10 20:06:05 +08:00
0547bb5a02 refactor: video stream service; fix: progress tracker guid 2025-08-09 14:09:03 +08:00
771f5e8e9f feat: 完成基本的Jpeg控制 2025-08-08 18:38:16 +08:00
58378851bb fix: 修复progresstracker堆栈溢出的问题 2025-08-08 16:34:31 +08:00
ae50ba3b9f feat: 为Number添加更多处理方式 2025-08-08 16:33:48 +08:00
d2508f6484 feat: 修改视频流后端服务,使其适配jpeg格式 2025-08-07 15:16:18 +08:00
aff9da2a60 feat: 添加下载进度条 2025-08-04 20:00:02 +08:00
alivender
e0ac21d141 feat: 部分修复Hdmi再次启动启动不了的bug 2025-08-04 17:13:50 +08:00
8396b7aaea feat: 支持HDMI关闭传输 2025-08-04 17:00:31 +08:00
alivender
a331494fde add: 完善HDMI输入前后端,现在无法关闭 2025-08-04 16:35:42 +08:00
alivender
e86cd5464e add: 逻辑分析仪可设置采样频率 2025-08-04 14:31:58 +08:00
alivender
04b136117d Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-08-04 13:27:37 +08:00
alivender
5c87204ef6 feat: 逻辑分析仪深度可用户输入自定义数字 2025-08-04 13:27:35 +08:00
35647d21bb feat: 添加Hdmi视频串流后端 2025-08-04 13:26:20 +08:00
alivender
51b39cee07 add: 添加了HDMI视频流Client 2025-08-04 11:54:58 +08:00
alivender
0bd1ad8a0e add: 添加了960*540分辨率 2025-08-02 21:07:08 +08:00
alivender
f2c7c78b64 feat: JtaggetDR可以一次全部获取到 2025-08-02 16:01:07 +08:00
alivender
2f23ffe482 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-08-02 13:15:07 +08:00
alivender
9904fecbee feat: 统一资源管理 2025-08-02 13:14:01 +08:00
cb229c2a30 fix: 修复jtag边界扫描前后端的bug:无法开始停止,无法通过认证,后端崩溃 2025-08-02 13:10:44 +08:00
alivender
e5f2be616c feat:删除刷新保存功能,大幅提升性能 2025-08-01 20:51:50 +08:00
alivender
2e9e378457 feat: 完善部分jtag边界扫描websocket代码 2025-08-01 20:21:32 +08:00
alivender
9fe0ee959f Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-08-01 20:00:00 +08:00
9adc5295f8 feat: 使用SignalR来控制jtag边界扫描 2025-08-01 19:55:55 +08:00
alivender
8047987935 Index界面可以隐藏NavBar 2025-08-01 13:40:21 +08:00
alivender
2d77706013 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-08-01 12:57:33 +08:00
alivender
c564844673 add: 添加实验列表界面,实验增删完全依赖数据库实现 2025-08-01 12:57:30 +08:00
2adeca3b99 feat: 配置板子网络时,更新动态mac 2025-07-31 16:33:19 +08:00
bafd06162c feat: 优化Common函数以提高性能 2025-07-31 15:43:16 +08:00
8c404d4072 fix: 修复Debugger处理数据时,最终转化为字节时出现的转化问题 2025-07-31 14:31:39 +08:00
alivender
d27b5d7737 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-07-31 14:03:05 +08:00
alivender
4df583e74b add: 为前后端添加exam数据库管理 2025-07-31 14:03:00 +08:00
1ca9999f15 fix: 尝试去修复Debugger处理数据时出现错乱的问题 2025-07-31 13:56:48 +08:00
alivender
0cc35ce541 feat: 移除电源控制按钮,在jtag操作时自动开启 2025-07-31 13:20:45 +08:00
alivender
d7c02ee6c9 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-07-31 13:14:26 +08:00
alivender
6b701658d1 add: 为逻辑分析仪添加了深度、预存储深度、通道组设置 2025-07-31 13:14:23 +08:00
2f1be8b0b7 fix: 修复前端显示问题与后端无法读取Debugger数据的问题 2025-07-30 20:26:14 +08:00
82bc03b9fb fix: 修复由于Debugger ReadData是get请求无法获取body导致返回B0adRequest的问题 2025-07-30 19:33:18 +08:00
3257a68407 feat: 修改后端apiclient生成逻辑
fix: 修复debugger获取flag失败的问题
refactor: 重新编写debugger前后端逻辑
2025-07-30 15:31:14 +08:00
alivender
6dfd275091 add: 逻辑分析仪后端适配DDR存储功能 2025-07-29 20:38:31 +08:00
6c5250f9c2 fix: 修复示波器停止捕获导致无法配置的问题,并取消示波器动画 2025-07-29 19:13:15 +08:00
912eb625f5 fix: 修改ui,并修复bug 2025-07-29 19:13:15 +08:00
10e4a82e5b feat: 修改示波器外观 2025-07-29 19:13:15 +08:00
ef267721fd feat: 实现自动刷新示波器 2025-07-29 19:13:15 +08:00
DLUTdky
1d35c36da6 fix: web camera 2025-07-29 19:13:15 +08:00
3da0f284f3 feat: 完成debugger前后端交互 2025-07-29 19:10:21 +08:00
23d4459406 feat: 添加debugger后端api,并修改waveformdisplay使其更加通用 2025-07-29 15:45:45 +08:00
a4192659d1 feat: 优化debugger波形显示 2025-07-29 15:06:36 +08:00
f200d90fc0 feat: 添加数值类型的波形显示 2025-07-29 14:57:07 +08:00
6c1bda50ce fix: 服务端使用本地ip 2025-07-29 12:30:56 +08:00
3535b94123 fix: debugger的波形显示修复 2025-07-21 21:36:18 +08:00
5da9d9f4e2 fix: 服务端使用本地IP 2025-07-21 19:19:29 +08:00
e7c8d3fb9e feat: 修改debugger的波形显示 2025-07-21 18:11:43 +08:00
e872f24936 feat: 前端添加切换摄像头功能 2025-07-21 17:24:22 +08:00
d1c9710afe fea: 后端添加usb摄像头功能 2025-07-21 16:33:27 +08:00
alivender
422aaa89d5 fix:修复示波器获取失败的问题 2025-07-20 13:17:36 +08:00
alivender
5103145d01 feat:为边界扫描添加自动重复扫描 2025-07-20 10:34:42 +08:00
alivender
27c8ceb1db fix:修复七段数码管的显示问题 2025-07-20 10:33:57 +08:00
d30712d0f6 fix:尝试修复余晖效果 2025-07-20 09:31:19 +08:00
a56a65cc0d feat: 为获取idcode添加动画 2025-07-20 09:13:33 +08:00
9c7bde206b fix: 修复jtag未认证的问题 2025-07-20 09:09:50 +08:00
alivender
1fa944f3c7 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-07-19 11:36:10 +08:00
alivender
1492f16fdd feat: readwithwait函数增加了新参数;camera有更多分辨率选择。 2025-07-19 11:36:07 +08:00
042ca40998 feat: 完善示波器前端 2025-07-18 21:56:55 +08:00
e4a1c34a6c feat: 添加示波器前后端 2025-07-18 21:49:37 +08:00
alivender
ba79a2093b feat: 使用netsh指令设置arp 2025-07-18 14:37:25 +08:00
35bad4027d fix: 修复arp在windows下无法正常配置的问题 2025-07-18 13:51:23 +08:00
9af2d3e87e feat: 使用静态arp处理通信 2025-07-18 13:16:07 +08:00
e9ad1f0256 fix: 修复获取主机ip错误的问题 2025-07-18 12:28:32 +08:00
12cd35edff feat: 添加arp支持,仅支持管理员模式 2025-07-18 12:28:17 +08:00
69c7cbf4d8 feat: 简单实现静态arp设置 2025-07-17 21:56:22 +08:00
80b6dfb38d fea: 前端完成动态ip与动态mac的适配 2025-07-17 21:18:59 +08:00
0f4386457d feat: 更新网络配置后端及其api 2025-07-17 20:46:37 +08:00
08a9be543e refactor: 修改webapi,修改实验板网络设置时不再需要实验板ip与端口 2025-07-17 19:05:13 +08:00
688fe05b1b feat: 修改脚本使其自动执行后处理功能,处理生成api的错误 2025-07-17 19:04:23 +08:00
2ff735e06a refactor:修改api获取ip的方法 2025-07-17 16:58:09 +08:00
53eaac43e3 feat: 添加修改ip与数据库的联动,并重新生成了webapi
fix: 修复构建错误
2025-07-17 16:13:42 +08:00
f5dd474ba0 feat: 完成实验板动态ip与动态mac 2025-07-17 15:49:35 +08:00
alivender
e4ead72d53 fix: 修复了break问题 2025-07-17 14:49:20 +08:00
fb13a5c484 fix: 修复删除过多东西导致无法读取数据的问题 2025-07-17 14:26:14 +08:00
1053d71d29 fix: 由于解析错误导致的无法通信的问题 2025-07-17 14:11:24 +08:00
56dcbf5caa fix: udpserver解析数据错误 2025-07-17 11:47:40 +08:00
dfe279bf37 feat: 添加了端口占用检测,若被占用则不会启动 2025-07-17 11:23:45 +08:00
e3b769b24e feat: 添加嵌入式逻辑分析仪 2025-07-16 21:54:40 +08:00
alivender
8e19587a16 feat: 提交前端逻辑分析仪后台捕获;Camera现在可以以更高帧率运行 2025-07-16 21:14:23 +08:00
d551cbe793 feat: 更新通信协议 2025-07-16 20:25:43 +08:00
822091243e fix: 修复前端捕获按钮的问题 2025-07-16 17:46:21 +08:00
bcee42d8c1 fix: 修复强制捕获的bug 2025-07-16 16:58:15 +08:00
9165c2e5f4 fix: 数字孪生的实验板不再需要设置开发板ip与端口 2025-07-16 16:47:52 +08:00
8070e03496 feat: 添加强制捕获按钮 2025-07-16 16:23:40 +08:00
43e3cce048 fix: 更正逻辑分析仪频率 2025-07-16 15:27:53 +08:00
bcdefb2779 feat: 简单实现debugger的通信 2025-07-16 15:23:54 +08:00
519094b3a0 fix: 修复前端逻辑分析仪数据配置错误导致无法应用配置的问题 2025-07-16 14:31:17 +08:00
57cf82b48f fix: 修改异步接受为同步接受 2025-07-16 14:06:29 +08:00
b08b86dbbe fix: 使用C#自带的ping来刷新arp 2025-07-16 13:26:10 +08:00
0cfbebf804 fix:重新使用sortedlist来保证udp接受数据的顺序 2025-07-16 12:23:13 +08:00
446da52515 feat: 实现逻辑分析仪的捕获功能 2025-07-15 20:37:25 +08:00
alivender
c70cc46aa9 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-07-15 20:05:34 +08:00
alivender
0f850c3ae7 add: 添加前端对焦交互逻辑 2025-07-15 20:04:16 +08:00
99dc7b52cc feat: 新增ARP刷新函数,并且在每次clearData后执行一次刷新 2025-07-15 20:03:23 +08:00
0410d14d3a feat: 完成逻辑分析仪前后端交互 2025-07-15 19:50:33 +08:00
alivender
474151d412 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-07-15 19:10:49 +08:00
alivender
4562be2d01 feat: 完善Camera对焦代码 2025-07-15 19:10:12 +08:00
ef76f3e9c7 refactor: 修改逻辑分析仪,使其直接使用manager进行管理 2025-07-15 18:49:37 +08:00
alivender
a28ae9be97 feat: 现在停止视频流会使摄像头休眠,配置过程会唤醒,配置完后再休眠 2025-07-15 18:35:14 +08:00
9f25391540 feat: 完成逻辑分析仪前端设计 2025-07-15 18:30:18 +08:00
alivender
938ee80979 fix: 删除Camera无用读函数 2025-07-15 17:59:30 +08:00
alivender
4af7da6344 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-07-15 17:57:48 +08:00
alivender
4b140ef683 feat: 完善I2C读的逻辑 2025-07-15 17:57:15 +08:00
b139542c4c fix: 添加互斥锁,并增加更多log输出 2025-07-15 16:36:16 +08:00
be8fed995c feat: 使用排序来解决时间冲突 2025-07-15 14:26:24 +08:00
49cbdc51d9 fix: 修复多个外设无法认证的问题
refactor: 同时使用更加优雅的方式处理injection
2025-07-15 11:30:13 +08:00
alivender
705e322e41 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-07-14 17:28:01 +08:00
alivender
0f3754ce99 fix: UDPPools限制最大队列256 2025-07-14 17:27:59 +08:00
89cc2291c0 fix: 管理员无法通过认证固化比特流文件的问题 2025-07-14 17:27:09 +08:00
alivender
683d918d30 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-07-14 17:15:37 +08:00
alivender
d901a440a7 feat: 完善camera复位逻辑 2025-07-14 17:15:35 +08:00
6500c1ce2d fix: 使用互斥锁确保数据写入的顺序性 2025-07-14 17:13:53 +08:00
c9fc6961fa feat: 持续完善逻辑分析仪的界面 2025-07-14 16:42:30 +08:00
4d6c06a0e0 feat: 添加逻辑分析仪 2025-07-14 16:07:37 +08:00
alivender
533e2561ab fix: 完善I2c ReadData函数 2025-07-14 14:08:11 +08:00
e8a16fd446 fix: udp实现11端口 2025-07-14 14:02:23 +08:00
ca906489c2 feat: 实现udp多端口 2025-07-14 14:01:08 +08:00
alivender
2b1ee90af7 fix: 修改I2C读逻辑 2025-07-14 13:37:48 +08:00
2894ee24be feat: 实现udp并发接受数据?? 2025-07-14 13:30:16 +08:00
6068a10d67 feat: not finish logic analyzer ui 2025-07-14 12:15:53 +08:00
alivender
c5f0e706a4 feat: 部分修复摄像头批量读逻辑 2025-07-14 12:14:40 +08:00
alivender
9b580be5e9 add: 添加自动对焦逻辑(还没写完) 2025-07-14 10:48:13 +08:00
1273be7dee feat: 完成logicanalyzer的api 2025-07-13 19:42:05 +08:00
78737f6839 feat: 删除无用数据与冗余逻辑以提升性能 2025-07-13 16:55:39 +08:00
e38770a496 fix: 删除无用函数与信号,修复全屏bug 2025-07-13 16:00:49 +08:00
a76ee74656 feat:减少冗余代码??? 2025-07-13 14:48:18 +08:00
alivender
f710a66c69 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-07-13 13:56:39 +08:00
alivender
4e5dc91f10 feat: 增加了摄像头硬件复位和唤醒逻辑 2025-07-13 13:55:17 +08:00
8221f8e133 feat: 使用发送多个地址包来改善大数据读取的速度 2025-07-13 13:53:29 +08:00
bad64bdfbd fix: 优化界面,解耦组件抽屉 2025-07-13 13:28:45 +08:00
alivender
c29c3652bc Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-07-13 11:43:10 +08:00
alivender
352ee1f4f2 add: 添加分辨率设置逻辑 2025-07-13 11:42:26 +08:00
32b126b93f feat: 添加大数据接收方法,以提高接受速度 2025-07-13 11:40:41 +08:00
b913f58f13 feat: udpServer使用异步处理数据包 2025-07-13 10:53:59 +08:00
alivender
0350ce8829 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-07-13 10:51:40 +08:00
alivender
229e6e70ed feat: 细化OV配置 2025-07-13 10:51:24 +08:00
eebc5105a0 feat: 画布解耦合 2025-07-12 21:55:58 +08:00
15c6eefe30 fix: 修复普通用户无法正常读取用户信息的问题 2025-07-12 18:54:10 +08:00
28af2df093 feat: 优化工程页面的用户体验,包括删除一些不必要的元素,同时使用storage保存一些界面参数方便用户体验 2025-07-12 18:24:45 +08:00
alivender
e25f08739a fix: 跑通摄像头640x480配置 2025-07-12 18:24:25 +08:00
f253a33c83 feat: 完善用户界面,添加绑定与解除绑定的功能 2025-07-12 17:46:23 +08:00
0fb0c4e395 feat: 后端添加获取空闲实验板,继续修改前端界面使其更加合理 2025-07-12 14:59:28 +08:00
44e357b887 feat: 为表格优化界面,添加icon与动画 2025-07-12 13:44:48 +08:00
50ffd491fe feat: 更加完善实验板管理面板,前后端分离 2025-07-12 13:37:02 +08:00
e0619eb9a3 feat: 添加完整实验板信息 2025-07-11 21:50:35 +08:00
da6386c6f0 feat: 前端完成适配后端api 2025-07-11 21:44:23 +08:00
8789d6f9ee feat: 添加管理员实验板管理界面 2025-07-11 21:09:10 +08:00
546b9250fa feat: 后端添加管理员认证 2025-07-11 20:07:39 +08:00
3f2c772eeb feat: 添加注册界面 2025-07-11 19:26:27 +08:00
fae07d9eae fix: postcss build failed 2025-07-11 17:48:33 +08:00
eedec80927 style: 重新调整结构 2025-07-11 17:31:49 +08:00
b4bb563782 feat: 增加了登录选项 2025-07-11 16:36:28 +08:00
d88c710606 feat: 改进api生成方式 2025-07-11 14:32:26 +08:00
bdffba7576 fix: 修改camera寄存器地址,同时修改前端逻辑 2025-07-11 13:22:46 +08:00
alivender
d83bc250bd fix: 注释前端重复配置摄像头寄存器问题 2025-07-11 13:09:51 +08:00
285d3e8585 fix: 修改Camera的初始化命令,同时修改摄像头启动逻辑 2025-07-11 12:42:24 +08:00
alivender
8a1d6e52cb fix: 修复了i2c地址设置问题 2025-07-11 12:06:44 +08:00
33a2dbf437 fix: 修复udp突发长度错误的问题,以及camera的i2c地址错误的问题 2025-07-11 11:54:21 +08:00
4a5709a783 fix: 修复了之前如修复的i2c问题 2025-07-10 21:29:37 +08:00
d6167ac286 feat: backend add auth method 2025-07-10 19:39:00 +08:00
c6c3f1cc41 fix: 修复i2c发送数据包错误的问题 2025-07-10 18:42:33 +08:00
540f5c788d fix: 配置摄像头必须初始化 2025-07-10 16:29:10 +08:00
558a139593 fix: 修改摄像头读取地址 2025-07-10 16:24:23 +08:00
fad37ba922 feat: 添加摄像头初始化的axi寄存器配置 2025-07-10 16:16:17 +08:00
c7c8cbaeb8 feat: finish camera init cmd 2025-07-10 15:57:07 +08:00
15f9b68e7d feat: 使用全局ip与port配置摄像头 2025-07-09 21:54:34 +08:00
48501d79e2 feat: add storage for eqp 2025-07-09 21:21:07 +08:00
bbad7388d8 feat: 添加功能底栏 2025-07-09 20:48:11 +08:00
cbb3543c4a feat: 添加全局alert,并替换原先是toast 2025-07-09 19:06:41 +08:00
53027470fe fix:无法找到相关库的问题 2025-07-09 17:13:36 +08:00
2a766c3f6b refactor: merge 2025-07-09 17:08:12 +08:00
de28471f87 rm:去除konva 2025-07-09 17:05:29 +08:00
3a292c0a98 refactor:持续解耦合 2025-07-09 16:32:17 +08:00
91b00a977c refactor: 给Canvas解耦合 2025-07-09 15:55:49 +08:00
c5ce246caf refactor: 重构projectview页面结构 2025-07-09 14:42:29 +08:00
497fa731ca fix: 修复关闭串流时,服务器仍然读取camera数据的问题 2025-07-09 14:07:47 +08:00
443aea5e3e feat: 更新api,并更新了串流页面 2025-07-09 13:39:03 +08:00
67bdec8570 feat: 添加信号量池 2025-07-08 21:26:45 +08:00
1af3fa3a8f feat: 添加I2C与Camera初始化的支持 2025-07-08 21:21:31 +08:00
dd7efe3c84 refactor: 重新调整后端工程结构 2025-07-08 21:21:07 +08:00
alivender
23236b22bd Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-07-07 21:53:58 +08:00
alivender
ef1a6c8208 fix: 限制UDP最大包字节数,修复了字节计算问题 2025-07-07 21:53:55 +08:00
ff7f7b5a76 refactor: 将IP与端口输入框单独抽象成独立文件方便调用 2025-07-07 20:24:34 +08:00
da7b3f4a4b feat: 支持修改单位 2025-07-07 19:53:43 +08:00
a9ab5926ed feat: 实现简易示波器功能 2025-07-07 19:38:12 +08:00
2e084bfb58 refactor:使用lucide替代navbar的icon,并删去video的标题栏 2025-07-07 15:18:29 +08:00
221d598a6e env: update 2025-07-07 13:33:18 +08:00
c3bd61ed51 refactor: 重构canvas 2025-07-03 21:56:58 +08:00
287c416247 fix: 调整Camera的taskID,使其能够正常接受数据 2025-07-03 19:30:17 +08:00
e84a784517 feat: 支持实际摄像头视频流 2025-07-03 17:51:12 +08:00
178ac0de67 feat: 将摄像头数据从生成的数据改为读取实际数据 2025-07-03 15:47:00 +08:00
bed0158a5f style: 继续调整后端 2025-07-03 14:52:00 +08:00
7ffb15c722 style: 重新调整一下后端的结构,并通过csharpier格式化 2025-07-03 14:14:45 +08:00
alivender
5ba71d220f 加入了TODO 2025-07-03 13:49:57 +08:00
14d8499f77 refactor: try to rewrite component manager 2025-07-02 21:16:18 +08:00
d18cf82813 feat: right mouse down to drag canvas 2025-07-02 20:27:35 +08:00
f1e2dbd9d8 feat: add components drawer 2025-07-01 21:08:58 +08:00
262c5e4003 fix: select not work 2025-07-01 20:12:39 +08:00
fbd13f8f2f fix: select rect wrong after zoom in / out 2025-06-30 21:48:41 +08:00
6cf7ef02ac feat: add resizer and add zoom in / out for canvas 2025-06-30 21:39:08 +08:00
alivender
4c14ada97b feat: add http vedio test 2025-06-18 18:39:58 +08:00
8207c37e12 fix: box select failed 2025-06-14 10:58:00 +08:00
db71681bdf try to fix box select 2025-06-13 22:05:09 +08:00
2270022bbe feat: auto hide rect bounding when not hover 2025-06-13 21:05:44 +08:00
dcadb97a7f fix: correct calculate rect bounding 2025-06-13 20:08:46 +08:00
1538bb9d07 add boarder 2025-06-12 21:49:23 +08:00
f340c86a41 feat: add rect select but have some problems 2025-06-11 21:25:15 +08:00
b6fb7e05fa refactor: rewrite basic canvas using konva 2025-06-06 21:05:46 +08:00
e0db12e0eb update flake and add konva 2025-06-06 20:28:17 +08:00
81f91b2b71 fix: boundary scan could not close and jtag scan failed 2025-05-20 20:31:52 +08:00
alivender
bbfe06822d Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-05-20 20:19:09 +08:00
alivender
d73166187a Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-05-20 20:18:58 +08:00
alivender
2eabb79d0f Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-05-20 20:09:42 +08:00
alivender
a865cfc950 fix: 注释掉调试日志以清理控制台输出 2025-05-20 20:09:39 +08:00
fa7c947351 feat: backend add task id to reduce conflict 2025-05-20 20:08:20 +08:00
dc64a65702 feat: add power control 2025-05-20 19:11:29 +08:00
179 changed files with 53047 additions and 6473 deletions

5
.gitignore vendored
View File

@@ -18,6 +18,7 @@ coverage
/cypress/videos/
/cypress/screenshots/
DebuggerCmd.md
# Editor directories and files
.vscode/*
@@ -27,10 +28,10 @@ coverage
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
prompt.md
*.tsbuildinfo
# Generated Files
*.sqlite
components.d.ts

View File

@@ -15,11 +15,15 @@ clean:
rm -rf "dist"
rm -rf "wwwroot"
update:
npm install
dotnet restore ./server/server.csproj
update: update-node update-dotnet
git submodule update --init --remote --recursive
update-node:
npm install
update-dotnet:
dotnet restore ./server/server.csproj
# 生成Restful API到网页客户端
gen-api:
npm run gen-api

48
FPGAWebLabServer.sln Normal file
View File

@@ -0,0 +1,48 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "server", "server\server.csproj", "{F31D6A0D-0407-41CE-A67E-01B847488EFB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "server.test", "server.test\server.test.csproj", "{CC274582-AC3C-4FD1-977C-96F1BC2760BD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|x64.ActiveCfg = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|x64.Build.0 = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|x86.ActiveCfg = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Debug|x86.Build.0 = Debug|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|Any CPU.Build.0 = Release|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|x64.ActiveCfg = Release|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|x64.Build.0 = Release|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|x86.ActiveCfg = Release|Any CPU
{F31D6A0D-0407-41CE-A67E-01B847488EFB}.Release|x86.Build.0 = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|x64.ActiveCfg = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|x64.Build.0 = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|x86.ActiveCfg = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Debug|x86.Build.0 = Debug|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|Any CPU.Build.0 = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|x64.ActiveCfg = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|x64.Build.0 = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|x86.ActiveCfg = Release|Any CPU
{CC274582-AC3C-4FD1-977C-96F1BC2760BD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

10
flake.lock generated
View File

@@ -2,12 +2,12 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1741246872,
"narHash": "sha256-Q6pMP4a9ed636qilcYX8XUguvKl/0/LGXhHcRI91p0U=",
"rev": "10069ef4cf863633f57238f179a0297de84bd8d3",
"revCount": 763342,
"lastModified": 1748929857,
"narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=",
"rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4",
"revCount": 810143,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.763342%2Brev-10069ef4cf863633f57238f179a0297de84bd8d3/01956ed4-f66c-7a87-98e4-b7e58f4aa591/source.tar.gz"
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.810143%2Brev-c2a03962b8e24e669fb37b7df10e7c79531ff1a4/01973914-8b42-7168-9ee2-4d6ea6946695/source.tar.gz"
},
"original": {
"type": "tarball",

View File

@@ -7,22 +7,26 @@
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
pkgs = import nixpkgs {
pkgs = import nixpkgs {
inherit system;
config.permittedInsecurePackages = ["dotnet-sdk-6.0.428"];
config.permittedInsecurePackages = [
"dotnet-sdk-6.0.428"
"beekeeper-studio-5.2.9"
];
};
});
in
{
devShells = forEachSupportedSystem ({ pkgs }: {
default = pkgs.mkShell {
packages = with pkgs; [
packages = with pkgs; [
# Frontend
nodejs
sqlite
sqls
sql-studio
beekeeper-studio
zlib
bash
# Backend
(dotnetCorePackages.combinePackages [
dotnetCorePackages.sdk_9_0
@@ -30,6 +34,8 @@
dotnetCorePackages.sdk_8_0
])
nuget
mono
vlc
# msbuild
omnisharp-roslyn
csharpier
@@ -38,10 +44,10 @@
typescript-language-server
];
shellHook = ''
export PATH=$PATH:
export PATH=$PATH:/home/sikongjueluo/.dotnet/tools
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${pkgs.zlib}/lib
export DOTNET_ROOT=${pkgs.dotnetCorePackages.sdk_9_0}/share/dotnet
'';
};
});
};

4034
nohup.out Normal file

File diff suppressed because it is too large Load Diff

2412
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,24 +9,32 @@
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"pregen-api": "cd server && dotnet run --property:Configuration=Release &",
"gen-api": "npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts",
"postgen-api": "pkill server"
"gen-api": "npx tsx scripts/GenerateWebAPI.ts"
},
"dependencies": {
"@microsoft/signalr": "^9.0.6",
"@svgdotjs/svg.js": "^3.2.4",
"@tanstack/vue-table": "^8.21.3",
"@types/lodash": "^4.17.16",
"@types/signalr": "^2.4.3",
"@vueuse/core": "^13.5.0",
"async-mutex": "^0.5.0",
"axios": "^1.11.0",
"echarts": "^5.6.0",
"highlight.js": "^11.11.1",
"lodash": "^4.17.21",
"log-symbols": "^7.0.0",
"lucide-vue-next": "^0.525.0",
"marked": "^12.0.0",
"mathjs": "^14.4.0",
"md-editor-v3": "^5.8.4",
"pinia": "^3.0.1",
"tinypool": "^1.0.2",
"reka-ui": "^2.3.1",
"ts-log": "^2.2.7",
"ts-results-es": "^5.0.1",
"vue": "^3.5.13",
"vue-echarts": "^7.0.3",
"vue-konva": "^3.2.1",
"vue-router": "4",
"yocto-queue": "^1.2.1",
"zod": "^3.24.2"
@@ -40,11 +48,15 @@
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.20",
"daisyui": "^5.0.0",
"node-fetch": "^3.3.2",
"npm-run-all2": "^7.0.2",
"nswag": "^14.3.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.12",
"ts-node": "^10.9.2",
"tsx": "^4.20.3",
"typescript": "~5.7.3",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.1.0",
"vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.2"

File diff suppressed because it is too large Load Diff

429
scripts/GenerateWebAPI.ts Normal file
View File

@@ -0,0 +1,429 @@
import { spawn, exec, ChildProcess } from "child_process";
import { promisify } from "util";
import fetch from "node-fetch";
import * as fs from "fs";
const execAsync = promisify(exec);
// Windows 支持函数
function getCommand(command: string): string {
// dotnet 在 Windows 上不需要 .cmd 后缀
if (command === "dotnet") {
return "dotnet";
}
return process.platform === "win32" ? `${command}.cmd` : command;
}
function getSpawnOptions() {
return process.platform === "win32"
? { stdio: "pipe", shell: true }
: { stdio: "pipe" };
}
async function waitForServer(
url: string,
maxRetries: number = 30,
interval: number = 1000,
): Promise<boolean> {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (response.ok) {
console.log("✓ Server is ready");
return true;
}
} catch (error) {
// Server not ready yet
}
console.log(`Waiting for server... (${i + 1}/${maxRetries})`);
await new Promise((resolve) => setTimeout(resolve, interval));
}
return false;
}
// 改进全局变量类型
let serverProcess: ChildProcess | null = null;
let webProcess: ChildProcess | null = null;
async function startWeb(): Promise<ChildProcess> {
console.log("Starting Vite frontend...");
return new Promise((resolve, reject) => {
const process = spawn(
getCommand("npm"),
["run", "dev"],
getSpawnOptions() as any,
);
let webStarted = false;
process.stdout?.on("data", (data) => {
const output = data.toString();
console.log(`Web: ${output}`);
// 检查 Vite 是否已启动
if (
(output.includes("Local:") || output.includes("ready in")) &&
!webStarted
) {
webStarted = true;
resolve(process);
}
});
process.stderr?.on("data", (data) => {
console.error(`Web Error: ${data}`);
});
process.on("error", (error) => {
reject(error);
});
process.on("exit", (code, signal) => {
console.log(`Web process exited with code ${code} and signal ${signal}`);
if (!webStarted) {
reject(new Error(`Web process exited unexpectedly with code ${code}`));
}
});
// 存储进程引用
webProcess = process;
// 超时处理
setTimeout(() => {
if (!webStarted) {
reject(new Error("Web server failed to start within timeout"));
}
}, 10000); // 10秒超时
});
}
async function startServer(): Promise<ChildProcess> {
console.log("Starting .NET server...");
return new Promise((resolve, reject) => {
const process = spawn(
getCommand("dotnet"),
["run", "--property:Configuration=Release"],
{
cwd: "server",
...getSpawnOptions(),
} as any,
);
let serverStarted = false;
process.stdout?.on("data", (data) => {
const output = data.toString();
console.log(`Server: ${output}`);
// 检查服务器是否已启动
if (output.includes("Now listening on:") && !serverStarted) {
serverStarted = true;
resolve(process);
}
});
process.stderr?.on("data", (data) => {
console.error(`Server Error: ${data}`);
});
process.on("error", (error) => {
reject(error);
});
process.on("exit", (code, signal) => {
console.log(
`Server process exited with code ${code} and signal ${signal}`,
);
if (!serverStarted) {
reject(
new Error(`Server process exited unexpectedly with code ${code}`),
);
}
});
// 存储进程引用
serverProcess = process;
// 超时处理
setTimeout(() => {
if (!serverStarted) {
reject(new Error("Server failed to start within timeout"));
}
}, 10000); // 10秒超时
});
}
async function stopServer(): Promise<void> {
console.log("Stopping server...");
if (!serverProcess) {
console.log("No server process to stop");
return;
}
try {
// 检查进程是否还存在
if (serverProcess.killed || serverProcess.exitCode !== null) {
console.log("✓ Server process already terminated");
serverProcess = null;
return;
}
// 发送 SIGTERM 信号
const killed = serverProcess.kill("SIGTERM");
if (!killed) {
console.warn("Failed to send SIGTERM to server process");
return;
}
// 设置超时,如果 3 秒内没有退出则强制终止
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
if (
serverProcess &&
!serverProcess.killed &&
serverProcess.exitCode === null
) {
console.log("Force killing server process...");
serverProcess.kill("SIGKILL");
}
resolve();
}, 3000); // 减少超时时间到3秒
});
await Promise.race([timeoutPromise]);
} catch (error) {
console.warn("Warning: Could not stop server process:", error);
} finally {
serverProcess = null;
// 只有在进程可能没有正常退出时才执行清理
// 移除自动清理逻辑,因为正常退出时不需要
}
}
async function stopWeb(): Promise<void> {
console.log("Stopping web server...");
if (!webProcess) {
console.log("No web process to stop");
return;
}
try {
// 检查进程是否还存在
if (webProcess.killed || webProcess.exitCode !== null) {
console.log("✓ Web process already terminated");
webProcess = null;
return;
}
// 发送 SIGTERM 信号
const killed = webProcess.kill("SIGTERM");
if (!killed) {
console.warn("Failed to send SIGTERM to web process");
return;
}
// 设置超时,如果 3 秒内没有退出则强制终止
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
if (webProcess && !webProcess.killed && webProcess.exitCode === null) {
console.log("Force killing web process...");
webProcess.kill("SIGKILL");
}
resolve();
}, 3000);
});
await Promise.race([timeoutPromise]);
} catch (error) {
console.warn("Warning: Could not stop web process:", error);
} finally {
webProcess = null;
}
}
async function postProcessApiClient(): Promise<void> {
console.log("Post-processing API client...");
try {
const filePath = "src/APIClient.ts";
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
throw new Error(`API client file not found: ${filePath}`);
}
// 读取文件内容
let content = fs.readFileSync(filePath, "utf8");
// 替换 ArgumentException 中的 message 属性声明
content = content.replace(
/(\s+)message!:\s*string;/g,
"$1declare message: string;",
);
content = content.replace(
"{ AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelToken }",
"{ AxiosError, type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type CancelToken }",
);
// 写回文件
fs.writeFileSync(filePath, content, "utf8");
console.log("✓ API client post-processing completed");
} catch (error) {
throw new Error(`Failed to post-process API client: ${error}`);
}
}
async function generateApiClient(): Promise<void> {
console.log("Generating API client...");
try {
const url = "http://127.0.0.1:5000/GetAPIClientCode";
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to fetch API client code: ${response.status} ${response.statusText}`,
);
}
const code = await response.text();
// 写入 APIClient.ts
const filePath = "src/APIClient.ts";
fs.writeFileSync(filePath, code, "utf8");
console.log("✓ API client code fetched and written successfully");
// 添加后处理步骤
await postProcessApiClient();
} catch (error) {
throw new Error(`Failed to generate API client: ${error}`);
}
}
async function generateSignalRClient(): Promise<void> {
console.log("Generating SignalR TypeScript client...");
try {
const { stdout, stderr } = await execAsync(
"dotnet tsrts --project ./server/server.csproj --output ./src/utils/signalR",
);
if (stdout) console.log(stdout);
if (stderr) console.error(stderr);
console.log("✓ SignalR TypeScript client generated successfully");
} catch (error) {
throw new Error(`Failed to generate SignalR client: ${error}`);
}
}
async function main(): Promise<void> {
try {
// Generate SignalR client
await generateSignalRClient();
console.log("✓ SignalR TypeScript client generated successfully");
// Start web frontend first
await startWeb();
console.log("✓ Frontend started");
// Wait a bit for frontend to fully initialize
await new Promise((resolve) => setTimeout(resolve, 3000));
// Start server
await startServer();
console.log("✓ Backend started");
// Wait for server to be ready (给服务器额外时间完全启动)
await new Promise((resolve) => setTimeout(resolve, 2000));
// Check if swagger endpoint is available
const serverReady = await waitForServer(
"http://localhost:5000/swagger/v1/swagger.json",
);
if (!serverReady) {
throw new Error("Server failed to start within the expected time");
}
// Generate API client
await generateApiClient();
console.log("✓ API generation completed successfully");
} catch (error) {
console.error("❌ Error:", error);
process.exit(1);
} finally {
// Always try to stop processes in order: server first, then web
await stopServer();
await stopWeb();
}
}
// 改进的进程终止处理 - 添加防重复执行
let isCleaningUp = false;
const cleanup = async (signal: string) => {
if (isCleaningUp) {
console.log("Cleanup already in progress, ignoring signal");
return;
}
isCleaningUp = true;
console.log(`\nReceived ${signal}, cleaning up...`);
try {
await Promise.all([stopServer(), stopWeb()]);
} catch (error) {
console.error("Error during cleanup:", error);
}
// 立即退出,不等待
process.exit(0);
};
process.on("SIGINT", () => cleanup("SIGINT"));
process.on("SIGTERM", () => cleanup("SIGTERM"));
// 处理未捕获的异常
process.on("uncaughtException", async (error) => {
if (isCleaningUp) return;
console.error("❌ Uncaught exception:", error);
isCleaningUp = true;
try {
await Promise.all([stopServer(), stopWeb()]);
} catch (cleanupError) {
console.error("Error during cleanup:", cleanupError);
}
process.exit(1);
});
process.on("unhandledRejection", async (reason, promise) => {
if (isCleaningUp) return;
console.error("❌ Unhandled rejection at:", promise, "reason:", reason);
isCleaningUp = true;
try {
await Promise.all([stopServer(), stopWeb()]);
} catch (cleanupError) {
console.error("Error during cleanup:", cleanupError);
}
process.exit(1);
});
main().catch(async (error) => {
if (isCleaningUp) return;
console.error("❌ Unhandled error:", error);
isCleaningUp = true;
try {
await Promise.all([stopServer(), stopWeb()]);
} catch (cleanupError) {
console.error("Error during cleanup:", cleanupError);
}
process.exit(1);
});

View File

@@ -1,61 +0,0 @@
using System.Buffers.Binary;
using Common;
namespace server.test;
public class CommonTest
{
[Fact]
public void ReverseBytesTest()
{
var rnd = new Random();
var bytesLen = 8;
var bytesArray = new byte[bytesLen];
rnd.NextBytes(bytesArray);
var rev2Bytes = new byte[] {
bytesArray[1],
bytesArray[0],
bytesArray[3],
bytesArray[2],
bytesArray[5],
bytesArray[4],
bytesArray[7],
bytesArray[6],
};
Assert.Equal(Number.ReverseBytes(bytesArray, 2).Value, rev2Bytes);
var rev4Bytes = new byte[] {
bytesArray[3],
bytesArray[2],
bytesArray[1],
bytesArray[0],
bytesArray[7],
bytesArray[6],
bytesArray[5],
bytesArray[4],
};
Assert.Equal(Number.ReverseBytes(bytesArray, 4).Value, rev4Bytes);
}
[Fact]
public void ToBitTest()
{
Assert.Equal(true, Number.ToBit(0xFF, 3).Value);
Assert.Equal(false, Number.ToBit(0x00, 3).Value);
Assert.Equal(true, Number.ToBit(0xAA, 3).Value);
Assert.Equal(false, Number.ToBit(0xAA, 2).Value);
}
[Fact]
public void ReverseBits()
{
Assert.Equal((byte)0x05, Common.Number.ReverseBits((byte)0xA0));
var bytesSrc = new byte[] { 0xAB, 0x00, 0x00, 0x01 };
var bytes = new byte[4];
for (int i = 0; i < 4; i++)
bytes[i] = Common.Number.ReverseBits(bytesSrc[i]);
Assert.Equal(new byte[] { 0xD5, 0x00, 0x00, 0x80 }, bytes);
}
}

386
server.test/NumberTest.cs Normal file
View File

@@ -0,0 +1,386 @@
using System.Collections;
using Common;
namespace CommonTest;
/// <summary>
/// 针对 Common.Number 的单元测试,覆盖所有公开方法
/// </summary>
public class NumberTest
{
/// <summary>
/// 测试 NumberToBytes 的正常与异常情况
/// </summary>
[Fact]
public void Test_NumberToBytes()
{
// 测试大端isLowNumHigh=false
var result1 = Number.NumberToBytes(0x12345678ABCDEF01, 8, false);
Assert.True(result1.IsSuccessful);
Assert.Equal(new byte[] { 0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x01 }, result1.Value);
// 测试小端isLowNumHigh=true
var result2 = Number.NumberToBytes(0x12345678ABCDEF01, 8, true);
Assert.True(result2.IsSuccessful);
Assert.Equal(new byte[] { 0x01, 0xEF, 0xCD, 0xAB, 0x78, 0x56, 0x34, 0x12 }, result2.Value);
// 测试长度不足4字节
var result3 = Number.NumberToBytes(0x12345678, 4, false);
Assert.True(result3.IsSuccessful);
Assert.Equal(new byte[] { 0x12, 0x34, 0x56, 0x78 }, result3.Value);
// 测试超长
var result4 = Number.NumberToBytes(0x1, 9, false);
Assert.False(result4.IsSuccessful);
}
/// <summary>
/// 测试 BytesToUInt64 的正常与异常情况,覆盖不同参数组合
/// </summary>
[Fact]
public void Test_BytesToUInt64()
{
// 正常大端isLowNumHigh=false
var bytes = new byte[] { 0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x01 };
var result = Number.BytesToUInt64((byte[])bytes.Clone());
Assert.True(result.IsSuccessful);
Assert.Equal(0x12345678ABCDEF01UL, result.Value);
// 正常小端isLowNumHigh=true
var bytes2 = new byte[] { 0x01, 0xEF, 0xCD, 0xAB, 0x78, 0x56, 0x34, 0x12 };
var result2 = Number.BytesToUInt64((byte[])bytes2.Clone(), true);
Assert.True(result2.IsSuccessful);
Assert.Equal(0x12345678ABCDEF01UL, result2.Value);
// 长度不足8字节numLength=4大端
var bytes3 = new byte[] { 0x12, 0x34, 0x56, 0x78 };
var result3 = Number.BytesToUInt64((byte[])bytes3.Clone(), 0, 4, false);
Assert.True(result3.IsSuccessful);
Assert.Equal(0x1234567800000000UL, result3.Value);
// 长度不足8字节numLength=4小端
var bytes4 = new byte[] { 0x78, 0x56, 0x34, 0x12 };
var result4 = Number.BytesToUInt64((byte[])bytes4.Clone(), 0, 4, true);
Assert.True(result4.IsSuccessful);
Assert.Equal(0x12345678UL, result4.Value);
// numLength=0
var bytes5 = new byte[] { 0x12, 0x34, 0x56, 0x78 };
var result5 = Number.BytesToUInt64((byte[])bytes5.Clone(), 0, 0, false);
Assert.True(result5.IsSuccessful);
Assert.Equal(0UL, result5.Value);
// offset测试
var bytes6 = new byte[] { 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x01 };
var result6 = Number.BytesToUInt64(bytes6, 2, 8, false);
Assert.True(result6.IsSuccessful);
Assert.Equal(0x12345678ABCDEF01UL, result6.Value);
// numLength超限>8应返回异常
var bytes7 = new byte[9];
var result7 = Number.BytesToUInt64(bytes7, 0, 9, false);
Assert.False(result7.IsSuccessful);
// offset+numLength超限
var bytes8 = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var result8 = Number.BytesToUInt64(bytes8, 2, 4, false);
Assert.True(result8.IsSuccessful);
Assert.Equal(0x0304000000000000UL, result8.Value);
// bytes长度不足offset+numLength
var bytes9 = new byte[] { 0x01, 0x02 };
var result9 = Number.BytesToUInt64(bytes9, 1, 2, true);
Assert.True(result9.IsSuccessful);
Assert.Equal(0x02UL, result9.Value);
// 空数组
var result10 = Number.BytesToUInt64(new byte[0], 0, 0, false);
Assert.True(result10.IsSuccessful);
Assert.Equal(0UL, result10.Value);
}
/// <summary>
/// 测试 BytesToUInt32 的正常与异常情况,覆盖不同参数组合
/// </summary>
[Fact]
public void Test_BytesToUInt32()
{
// 正常大端isLowNumHigh=false
var bytes = new byte[] { 0x12, 0x34, 0x56, 0x78 };
var result = Number.BytesToUInt32((byte[])bytes.Clone());
Assert.True(result.IsSuccessful);
Assert.Equal(0x12345678U, result.Value);
// 正常小端isLowNumHigh=true
var bytes2 = new byte[] { 0x78, 0x56, 0x34, 0x12 };
var result2 = Number.BytesToUInt32((byte[])bytes2.Clone(), true);
Assert.True(result2.IsSuccessful);
Assert.Equal(0x12345678U, result2.Value);
// 长度不足4字节numLength=2大端
var bytes3 = new byte[] { 0x12, 0x34 };
var result3 = Number.BytesToUInt32((byte[])bytes3.Clone(), 0, 2, false);
Assert.True(result3.IsSuccessful);
Assert.Equal(0x12340000U, result3.Value);
// 长度不足4字节numLength=2小端
var bytes4 = new byte[] { 0x34, 0x12 };
var result4 = Number.BytesToUInt32((byte[])bytes4.Clone(), 0, 2, true);
Assert.True(result4.IsSuccessful);
Assert.Equal(0x1234U, result4.Value);
// numLength=0
var bytes5 = new byte[] { 0x12, 0x34, 0x56, 0x78 };
var result5 = Number.BytesToUInt32((byte[])bytes5.Clone(), 0, 0, false);
Assert.True(result5.IsSuccessful);
Assert.Equal(0U, result5.Value);
// offset测试
var bytes6 = new byte[] { 0x00, 0x00, 0x12, 0x34, 0x56, 0x78 };
var result6 = Number.BytesToUInt32(bytes6, 2, 4, false);
Assert.True(result6.IsSuccessful);
Assert.Equal(0x12345678U, result6.Value);
// numLength超限>4应返回异常
var bytes7 = new byte[5];
var result7 = Number.BytesToUInt32(bytes7, 0, 5, false);
Assert.False(result7.IsSuccessful);
// offset+numLength超限
var bytes8 = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var result8 = Number.BytesToUInt32(bytes8, 2, 2, false);
Assert.True(result8.IsSuccessful);
Assert.Equal(0x03040000U, result8.Value);
// bytes长度不足offset+numLength
var bytes9 = new byte[] { 0x01, 0x02 };
var result9 = Number.BytesToUInt32(bytes9, 1, 1, true);
Assert.True(result9.IsSuccessful);
Assert.Equal(0x02U, result9.Value);
// 空数组
var result10 = Number.BytesToUInt32(new byte[0], 0, 0, false);
Assert.True(result10.IsSuccessful);
Assert.Equal(0U, result10.Value);
}
/// <summary>
/// 测试 UInt32ArrayToBytes 的正常与异常情况
/// </summary>
[Fact]
public void Test_UInt32ArrayToBytes()
{
// 正常情况
var arr = new UInt32[] { 0x12345678, 0xABCDEF01 };
var result = Number.UInt32ArrayToBytes(arr);
Assert.True(result.IsSuccessful);
// BlockCopy 按小端序
Assert.Equal(new byte[] { 0x78, 0x56, 0x34, 0x12, 0x01, 0xEF, 0xCD, 0xAB }, result.Value);
// 空数组
var result2 = Number.UInt32ArrayToBytes(new UInt32[0]);
Assert.True(result2.IsSuccessful);
Assert.Empty(result2.Value);
}
/// <summary>
/// 测试 MultiBitsToBytes 和 MultiBitsToNumber (ulong)
/// </summary>
[Fact]
public void Test_MultiBitsToBytesAndNumber_Ulong()
{
// 合并两个比特段
var result = Number.MultiBitsToNumber(0b101UL, 3, 0b11UL, 2);
Assert.True(result.IsSuccessful);
Assert.Equal((ulong)0b10111, result.Value);
// 合并为字节数组
var bytesResult = Number.MultiBitsToBytes(0b101UL, 3, 0b11UL, 2);
Assert.True(bytesResult.IsSuccessful);
Assert.Equal(new byte[] { 0b10111 }, bytesResult.Value);
// 超过64位
var failResult = Number.MultiBitsToNumber(0xFFFFFFFFFFFFFFFF, 64, 1, 1);
Assert.False(failResult.IsSuccessful);
}
/// <summary>
/// 测试 MultiBitsToNumber (uint)
/// </summary>
[Fact]
public void Test_MultiBitsToNumber_Uint()
{
var result = Number.MultiBitsToNumber(0b101U, 3, 0b11U, 2);
Assert.True(result.IsSuccessful);
Assert.Equal((uint)0b10111, result.Value);
// 超过64位
var failResult = Number.MultiBitsToNumber(uint.MaxValue, 64, 1, 1);
Assert.False(failResult.IsSuccessful);
}
/// <summary>
/// 测试 BitsCheck (ulong)
/// </summary>
[Fact]
public void Test_BitsCheck_Ulong()
{
// 完全匹配
Assert.True(Number.BitsCheck(0b1101UL, 0b1101UL));
// 不匹配
Assert.False(Number.BitsCheck(0b1101UL, 0b1001UL));
// 掩码
Assert.True(Number.BitsCheck(0b1101UL, 0b1001UL, 0b1001UL));
}
/// <summary>
/// 测试 BitsCheck (uint)
/// </summary>
[Fact]
public void Test_BitsCheck_Uint()
{
Assert.True(Number.BitsCheck(0b1011U, 0b1011U));
Assert.False(Number.BitsCheck(0b1011U, 0b1001U));
Assert.True(Number.BitsCheck(0b1011U, 0b1001U, 0b1001U));
}
/// <summary>
/// 测试 ToBit
/// </summary>
[Fact]
public void Test_ToBit()
{
// 取第0位
var result = Number.ToBit(0b1010U, 0);
Assert.True(result.IsSuccessful);
Assert.False(result.Value);
// 取第1位
var result2 = Number.ToBit(0b1010U, 1);
Assert.True(result2.IsSuccessful);
Assert.True(result2.Value);
// 负数位置
var result3 = Number.ToBit(0b1010U, -1);
Assert.False(result3.IsSuccessful);
}
/// <summary>
/// 测试 BitsToNumber
/// </summary>
[Fact]
public void Test_BitsToNumber()
{
// 5位BitArray
var bits = new BitArray(new bool[] { true, true, false, true, false }); // 0b01011
var result = Number.BitsToNumber(bits);
Assert.True(result.IsSuccessful);
Assert.Equal((uint)0b01011, result.Value);
// 超过32位
var bits2 = new BitArray(33);
Assert.Throws<ArgumentException>(() => Number.BitsToNumber(bits2));
}
/// <summary>
/// 测试 StringToBytes
/// </summary>
[Fact]
public void Test_StringToBytes()
{
// 16进制字符串
var bytes = Number.StringToBytes("1234ABCD");
Assert.Equal(new byte[] { 0x12, 0x34, 0xAB, 0xCD }, bytes);
// 8位字符串
var bytes2 = Number.StringToBytes("01020304");
Assert.Equal(new byte[] { 0x01, 0x02, 0x03, 0x04 }, bytes2);
}
/// <summary>
/// 测试 ReverseBytes
/// </summary>
[Fact]
public void Test_ReverseBytes()
{
// 步长为2
var src = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var result = Number.ReverseBytes(src, 2);
Assert.True(result.IsSuccessful);
Assert.Equal(new byte[] { 0x02, 0x01, 0x04, 0x03 }, result.Value);
// 步长为4
var src2 = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var result2 = Number.ReverseBytes(src2, 4);
Assert.True(result2.IsSuccessful);
Assert.Equal(new byte[] { 0x04, 0x03, 0x02, 0x01 }, result2.Value);
// 步长为1无变化
var src3 = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var result3 = Number.ReverseBytes(src3, 1);
Assert.True(result3.IsSuccessful);
Assert.Equal(src3, result3.Value);
// 步长为0异常
var result4 = Number.ReverseBytes(src3, 0);
Assert.False(result4.IsSuccessful);
// 步长不能整除
var result5 = Number.ReverseBytes(src3, 3);
Assert.False(result5.IsSuccessful);
}
/// <summary>
/// 测试 ReverseBits (byte)
/// </summary>
[Fact]
public void Test_ReverseBits_Byte()
{
// 0b00010010 -> 0b01001000
byte src = 0b00010010;
byte reversed = Number.ReverseBits(src);
Assert.Equal(0b01001000, reversed);
// 0b11110000 -> 0b00001111
Assert.Equal(0b00001111, Number.ReverseBits(0b11110000));
}
/// <summary>
/// 测试 ReverseBits (byte[])
/// </summary>
[Fact]
public void Test_ReverseBits_ByteArray()
{
var src = new byte[] { 0b00010010, 0b11110000 };
var reversed = Number.ReverseBits(src);
Assert.Equal(new byte[] { 0b01001000, 0b00001111 }, reversed);
// 空数组
var reversed2 = Number.ReverseBits(new byte[0]);
Assert.Empty(reversed2);
}
/// <summary>
/// 测试 GetLength
/// </summary>
[Fact]
public void Test_GetLength()
{
Assert.Equal(5, Number.GetLength(12345));
Assert.Equal(4, Number.GetLength(-123));
Assert.Equal(1, Number.GetLength(0));
}
/// <summary>
/// 测试 IntPow
/// </summary>
[Fact]
public void Test_IntPow()
{
Assert.Equal(8, Number.IntPow(2, 3));
Assert.Equal(1, Number.IntPow(5, 0));
Assert.Equal(0, Number.IntPow(0, 5));
Assert.Equal(7, Number.IntPow(7, 1));
Assert.Equal(81, Number.IntPow(3, 4));
}
}

View File

@@ -0,0 +1,99 @@
using Microsoft.AspNetCore.SignalR;
using Moq;
using server.Hubs;
using server.Services;
public class ProgressTrackerTest
{
[Fact]
public void Test_ProgressReporter_Basic()
{
int reportedValue = -1;
var reporter = new ProgressReporter(async v => { reportedValue = v; await Task.CompletedTask; }, 0, 100, 10);
// Report
reporter.Report(50);
Assert.Equal(50, reporter.Progress);
Assert.Equal(ProgressStatus.InProgress, reporter.Status);
Assert.Equal(50, reportedValue);
// Increase by step
reporter.Increase();
Assert.Equal(60, reporter.Progress);
// Increase by value
reporter.Increase(20);
Assert.Equal(80, reporter.Progress);
// Finish
reporter.Finish();
Assert.Equal(ProgressStatus.Completed, reporter.Status);
Assert.Equal(100, reporter.Progress);
// Cancel
reporter = new ProgressReporter(async v => { reportedValue = v; await Task.CompletedTask; }, 0, 100, 10);
reporter.Cancel();
Assert.Equal(ProgressStatus.Canceled, reporter.Status);
Assert.Equal("User Cancelled", reporter.ErrorMessage);
// Error
reporter = new ProgressReporter(async v => { reportedValue = v; await Task.CompletedTask; }, 0, 100, 10);
reporter.Error("Test Error");
Assert.Equal(ProgressStatus.Failed, reporter.Status);
Assert.Equal("Test Error", reporter.ErrorMessage);
// CreateChild
var parent = new ProgressReporter(async v => { await Task.CompletedTask; }, 10, 100, 5);
var child = parent.CreateChild(50, 5);
Assert.Equal(ProgressStatus.Pending, child.Status);
Assert.NotNull(child);
// Child Increase
child.Increase();
Assert.Equal(ProgressStatus.InProgress, child.Status);
Assert.Equal(20, child.ProgressPercent);
Assert.Equal(20, parent.Progress);
// Child Complete
child.Finish();
Assert.Equal(ProgressStatus.Completed, child.Status);
Assert.Equal(100, child.ProgressPercent);
Assert.Equal(60, parent.Progress);
}
[Fact]
public void Test_ProgressTrackerService_Basic()
{
// Mock SignalR HubContext
var mockHubContext = new Mock<IHubContext<ProgressHub, IProgressReceiver>>();
var service = new ProgressTrackerService(mockHubContext.Object);
// CreateTask
var (taskId, reporter) = service.CreateTask();
Assert.NotNull(taskId);
Assert.NotNull(reporter);
// GetReporter
var optReporter = service.GetReporter(taskId);
Assert.True(optReporter.HasValue);
Assert.Equal(reporter, optReporter.Value);
// GetProgressStatus
var optStatus = service.GetProgressStatus(taskId);
Assert.True(optStatus.HasValue);
Assert.Equal(ProgressStatus.Pending, optStatus.Value);
// BindTask
var bindResult = service.BindTask(taskId, "conn1");
Assert.True(bindResult);
// CancelTask
var cancelResult = service.CancelTask(taskId);
Assert.True(cancelResult);
// After cancel, status should be Cancelled
var optStatus2 = service.GetProgressStatus(taskId);
Assert.True(optStatus2.HasValue);
Assert.Equal(ProgressStatus.Canceled, optStatus2.Value);
}
}

View File

@@ -1,138 +0,0 @@
using System.Net;
using System.Text;
using Common;
using Xunit.Abstractions;
namespace server.test;
public class UDPServerTest
{
const string address = "127.0.0.1";
const int port = 33000;
private static readonly UDPServer udpServer = new UDPServer(port);
private readonly ITestOutputHelper output;
public UDPServerTest(ITestOutputHelper output)
{
this.output = output;
udpServer.Start();
}
[Fact]
public void UDPDataDeepClone()
{
var udpData = new UDPData()
{
DateTime = DateTime.Now,
Address = "127.0.0.1",
Port = 1234,
Data = new byte[] { 0xf0, 00, 00, 00 },
HasRead = false
};
var cloneUdpData = udpData.DeepClone();
Assert.Equal(udpData.DateTime, cloneUdpData.DateTime);
Assert.Equal(udpData.Address, cloneUdpData.Address);
Assert.Equal(udpData.Port, cloneUdpData.Port);
Assert.Equal(udpData.Data, cloneUdpData.Data);
Assert.Equal(udpData.HasRead, cloneUdpData.HasRead);
udpData.DateTime = DateTime.Now;
udpData.Address = "192.168.1.1";
udpData.Port = 33000;
udpData.Data = new byte[] { 0xFF, 00, 00, 00 };
udpData.HasRead = true;
Assert.NotNull(cloneUdpData.DateTime);
Assert.NotNull(cloneUdpData.Address);
Assert.NotNull(cloneUdpData.Port);
Assert.NotNull(cloneUdpData.Data);
Assert.NotNull(cloneUdpData.HasRead);
Assert.NotEqual(udpData.DateTime, cloneUdpData.DateTime);
Assert.NotEqual(udpData.Address, cloneUdpData.Address);
Assert.NotEqual(udpData.Port, cloneUdpData.Port);
Assert.NotEqual(udpData.Data, cloneUdpData.Data);
Assert.NotEqual(udpData.HasRead, cloneUdpData.HasRead);
}
[Theory]
[InlineData(new object[] { new string[] { "Hello World!", "Hello Server!", "What is your problem?" } })]
public async Task UDPServerFindString(string[] textArray)
{
Assert.True(udpServer.IsRunning);
var serverEP = new IPEndPoint(IPAddress.Parse(address), port);
foreach (var text in textArray)
{
Assert.True(await UDPClientPool.SendStringAsync(serverEP, [text]));
var ret = await udpServer.FindDataAsync(address);
Assert.True(ret.HasValue);
var data = ret.Value;
Assert.Equal(address, data.Address);
Assert.Equal(text, Encoding.ASCII.GetString(data.Data));
}
}
[Theory]
[InlineData(new object[] { new UInt32[] { 0xF0_00_00_00, 0xFF_00_00_00, 0xFF_FF_FF_FF } })]
public async Task UDPServerFindBytes(UInt32[] bytesArray)
{
Assert.True(udpServer.IsRunning);
var serverEP = new IPEndPoint(IPAddress.Parse(address), port);
foreach (var number in bytesArray)
{
Assert.True(await UDPClientPool.SendBytesAsync(serverEP, Number.NumberToBytes(number, 4).Value));
var ret = await udpServer.FindDataAsync(address);
Assert.True(ret.HasValue);
var data = ret.Value;
Assert.Equal(address, data.Address);
Assert.Equal(number, Number.BytesToUInt32(data.Data).Value);
}
}
[Theory]
[InlineData(new object[] { new UInt32[] { 0xF0_00_00_00, 0xF0_01_00_00 } })]
public async Task UDPServerWaitResp(UInt32[] bytesArray)
{
Assert.True(udpServer.IsRunning);
var serverEP = new IPEndPoint(IPAddress.Parse(address), port);
foreach (var number in bytesArray)
{
Assert.True(await UDPClientPool.SendBytesAsync(serverEP, Number.NumberToBytes(number, 4).Value));
var ret = await udpServer.WaitForAckAsync(address);
Assert.True(ret.IsSuccessful);
var data = ret.Value;
Assert.True(data.IsSuccessful);
Assert.Equal(number, Number.BytesToUInt32(data.ToBytes()).Value);
}
}
[Theory]
[InlineData(new object[] { new UInt64[] { 0x0F_00_00_00_01_02_02_02, 0x0F_01_00_00_FF_FF_FF_FF } })]
public async Task UDPServerWaitData(UInt64[] bytesArray)
{
Assert.True(udpServer.IsRunning);
var serverEP = new IPEndPoint(IPAddress.Parse(address), port);
foreach (var number in bytesArray)
{
Assert.True(await UDPClientPool.SendBytesAsync(serverEP, Number.NumberToBytes(number, 8).Value));
var ret = await udpServer.WaitForDataAsync(address);
Assert.True(ret.IsSuccessful);
var data = ret.Value;
Assert.True(data.IsSuccessful);
Assert.Equal((UInt64)number, Number.BytesToUInt64(data.ToBytes()).Value);
}
}
}

View File

@@ -11,6 +11,7 @@
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>

3
server/.gitignore vendored
View File

@@ -1,5 +1,6 @@
# Generate
obj
bin
bitstream
bsdl
data

View File

@@ -1,8 +1,17 @@
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.FileProviders;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using NLog;
using NLog.Web;
using NSwag;
using NSwag.CodeGeneration.TypeScript;
using NSwag.Generation.Processors.Security;
using server.Services;
using TypedSignalR.Client.DevTools;
// Early init of NLog to allow startup and exception logging, before host is built
var logger = NLog.LogManager.Setup()
@@ -36,6 +45,71 @@ try
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
});
// Add JWT Token Authorization
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
RequireExpirationTime = true,
ValidIssuer = "dlut.edu.cn",
ValidAudience = "dlut.edu.cn",
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("my secret key 1234567890my secret key 1234567890")),
};
options.Authority = $"http://{Global.LocalHost}:5000";
options.RequireHttpsMetadata = false;
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
// Sending the access token in the query string is required when using WebSockets or ServerSentEvents
// due to a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && (
path.StartsWithSegments("/hubs/JtagHub") ||
path.StartsWithSegments("/hubs/ProgressHub") ||
path.StartsWithSegments("/hubs/DigitalTubesHub") ||
path.StartsWithSegments("/hubs/RotaryEncoderHub") ||
path.StartsWithSegments("/hubs/OscilloscopeHub")
))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
// Add JWT Token Authorization Policy
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Admin", policy =>
{
policy.RequireClaim(ClaimTypes.Role, new string[] {
Database.UserPermission.Admin.ToString(),
});
});
});
// Add CORS policy
if (builder.Environment.IsDevelopment())
{
@@ -52,13 +126,22 @@ try
{
options.AddPolicy("Users", policy => policy
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
);
options.AddPolicy("SignalR", policy => policy
.WithOrigins([$"http://{Global.LocalHost}:5173", "http://127.0.0.1:5173"])
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
});
// Use SignalR
builder.Services.AddSignalR();
// Add Swagger
builder.Services.AddControllers();
builder.Services.AddOpenApiDocument(options =>
builder.Services.AddSwaggerDocument(options =>
{
options.PostProcess = document =>
{
@@ -80,8 +163,27 @@ try
// }
};
};
// Authorization
options.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme
{
Description = "请输入token,格式为 Bearer xxxxxxxx注意中间必须有空格",
Name = "Authorization",
In = NSwag.OpenApiSecurityApiKeyLocation.Header,
Type = NSwag.OpenApiSecuritySchemeType.ApiKey,
});
options.OperationProcessors.Add(new OperationSecurityScopeProcessor("Bearer"));
});
// 添加 HTTP 视频流服务
builder.Services.AddSingleton<HttpVideoStreamService>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpVideoStreamService>());
builder.Services.AddSingleton<HttpHdmiVideoStreamService>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpHdmiVideoStreamService>());
// 添加进度跟踪服务
builder.Services.AddSingleton<ProgressTracker>();
// Application Settings
var app = builder.Build();
// Configure the HTTP request pipeline.
@@ -95,12 +197,14 @@ try
logger.Info($"Use Static Files : {Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")}");
app.UseDefaultFiles();
app.UseStaticFiles(); // Serves files from wwwroot by default
// Assets Files
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "assets")),
RequestPath = "/assets"
});
// Log Files
if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "log")))
{
@@ -111,23 +215,82 @@ try
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")),
RequestPath = "/log"
});
// Exam Files (实验静态资源)
if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "exam")))
{
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "exam")),
RequestPath = "/exam"
});
}
app.MapFallbackToFile("index.html");
}
// Add logs
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
// Swagger
app.UseOpenApi();
app.UseOpenApi(settings =>
{
settings.PostProcess = (document, httpRequest) =>
{
document.Servers.Clear();
document.Servers.Add(new NSwag.OpenApiServer { Url = $"http://{Global.LocalHost}:5000" });
};
});
app.UseSwaggerUi();
// SignalR
app.UseWebSockets();
app.UseSignalRHubSpecification();
app.UseSignalRHubDevelopmentUI();
// Router
app.MapControllers();
app.MapHub<server.Hubs.JtagHub>("/hubs/JtagHub");
app.MapHub<server.Hubs.ProgressHub>("/hubs/ProgressHub");
app.MapHub<server.Hubs.DigitalTubesHub>("/hubs/DigitalTubesHub");
app.MapHub<server.Hubs.RotaryEncoderHub>("/hubs/RotaryEncoderHub");
app.MapHub<server.Hubs.OscilloscopeHub>("/hubs/OscilloscopeHub");
// Setup Program
MsgBus.Init();
var progressTracker = app.Services.GetRequiredService<ProgressTracker>();
MsgBus.SetProgressTracker(progressTracker);
// Generate API Client
app.MapGet("GetAPIClientCode", async (HttpContext context) =>
{
try
{
var document = await OpenApiDocument.FromUrlAsync($"http://{Global.LocalHost}:5000/swagger/v1/swagger.json");
var settings = new TypeScriptClientGeneratorSettings
{
ClassName = "{controller}Client",
UseAbortSignal = false,
Template = TypeScriptTemplate.Axios,
TypeScriptGeneratorSettings = {
},
};
var generator = new TypeScriptClientGenerator(document, settings);
var code = generator.GenerateFile();
return Results.Text(code, "text/plain; charset=utf-8", Encoding.UTF8);
}
catch (Exception err)
{
logger.Error(err);
return Results.Problem(err.ToString());
}
}).RequireCors("Development");
app.Run();
}
@@ -148,4 +311,3 @@ finally
// Close Program
MsgBus.Exit();
}

View File

@@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"applicationUrl": "http://0.0.0.0:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
@@ -15,7 +15,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7278;http://localhost:5000",
"applicationUrl": "https://0.0.0.0:7278;http://0.0.0.0:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"

View File

@@ -11,13 +11,18 @@
<SpaRoot>../</SpaRoot>
<SpaProxyServerUrl>http://localhost:5173</SpaProxyServerUrl>
<SpaProxyLaunchCommand>npm run dev</SpaProxyLaunchCommand>
<NoWarn>CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNext" Version="5.19.1" />
<PackageReference Include="DotNext.Threading" Version="5.19.1" />
<PackageReference Include="ArpLookup" Version="2.0.3" />
<PackageReference Include="DotNext" Version="5.23.0" />
<PackageReference Include="DotNext.Threading" Version="5.23.0" />
<PackageReference Include="FlashCap" Version="1.11.0" />
<PackageReference Include="H264Sharp" Version="1.6.0" />
<PackageReference Include="Honoo.IO.Hashing.Crc" Version="1.3.3" />
<PackageReference Include="linq2db.AspNet" Version="5.4.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="9.0.4" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.23" />
@@ -25,7 +30,20 @@
<PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" />
<PackageReference Include="NSwag.AspNetCore" Version="14.3.0" />
<PackageReference Include="NSwag.CodeGeneration.TypeScript" Version="14.4.0" />
<PackageReference Include="SharpRTSP" Version="1.8.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="Tapper.Analyzer" Version="1.13.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="TypedSignalR.Client.DevTools" Version="1.2.4" />
<PackageReference Include="TypedSignalR.Client.TypeScript.Analyzer" Version="1.15.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="TypedSignalR.Client.TypeScript.Attributes" Version="1.15.0" />
</ItemGroup>
</Project>

634
server/src/ArpClient.cs Normal file
View File

@@ -0,0 +1,634 @@
using System.Diagnostics;
using System.Net.NetworkInformation;
using ArpLookup;
using System.Runtime.InteropServices;
using System.Net;
using System.Text.RegularExpressions;
/// <summary>
/// ARP 记录管理静态类(跨平台支持)
/// </summary>
public static class ArpClient
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 读取所有 ARP 记录
/// </summary>
/// <returns>ARP 记录列表</returns>
public static async Task<List<ArpEntry>> GetArpTableAsync()
{
var entries = new List<ArpEntry>();
try
{
string command = GetArpListCommand();
var result = await ExecuteCommandAsync(command);
if (result.IsSuccess)
{
var lines = result.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var entry = ParseArpEntry(line);
if (entry != null)
{
entries.Add(entry);
}
}
}
}
catch (Exception ex)
{
throw new Exception($"读取 ARP 表失败: {ex.Message}");
}
return entries;
}
/// <summary>
/// 动态更新指定 IP 的 ARP 记录
/// </summary>
/// <param name="ipAddress">要更新的 IP 地址</param>
/// <returns>是否成功发送 Ping</returns>
public static async Task<bool> UpdateArpEntryAsync(string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
try
{
var ret = await ArpClient.DeleteArpEntryAsync(ipAddress);
if (!ret)
{
logger.Error($"删除 ARP 记录失败: {ipAddress}");
}
PhysicalAddress? mac = await Arp.LookupAsync(IPAddress.Parse(ipAddress));
if (mac == null)
return false;
return true;
}
catch
{
return false;
}
}
/// <summary>
/// 添加 ARP 记录
/// </summary>
/// <param name="ipAddress">IP 地址</param>
/// <param name="macAddress">MAC 地址</param>
/// <param name="interfaceName">网络接口名称(可选)</param>
/// <returns>是否成功</returns>
public static async Task<bool> AddArpEntryAsync(string ipAddress, string macAddress, string? interfaceName = null)
{
if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
if (string.IsNullOrWhiteSpace(macAddress))
throw new ArgumentException("MAC 地址不能为空", nameof(macAddress));
try
{
// 格式化 MAC 地址以适配不同操作系统
string formattedMac = FormatMacAddress(macAddress);
string command = await GetArpAddCommandAsync(ipAddress, formattedMac, interfaceName);
var result = await ExecuteCommandAsync(command);
return result.IsSuccess;
}
catch (Exception ex)
{
throw new Exception($"添加 ARP 记录失败: {ex.Message}");
}
}
/// <summary>
/// 删除 ARP 记录
/// </summary>
/// <param name="ipAddress">要删除的 IP 地址</param>
/// <param name="interfaceName">网络接口名称(可选)</param>
/// <returns>是否成功</returns>
public static async Task<bool> DeleteArpEntryAsync(string ipAddress, string? interfaceName = null)
{
if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
try
{
string command = GetArpDeleteCommand(ipAddress, interfaceName);
var result = await ExecuteCommandAsync(command);
return result.IsSuccess;
}
catch (Exception ex)
{
throw new Exception($"删除 ARP 记录失败: {ex.Message}");
}
}
/// <summary>
/// 清空所有 ARP 记录
/// </summary>
/// <returns>是否成功</returns>
public static async Task<bool> ClearArpTableAsync()
{
try
{
string command = GetArpClearCommand();
var result = await ExecuteCommandAsync(command);
return result.IsSuccess;
}
catch (Exception ex)
{
throw new Exception($"清空 ARP 表失败: {ex.Message}");
}
}
/// <summary>
/// 查询特定 IP 的 ARP 记录
/// </summary>
/// <param name="ipAddress">IP 地址</param>
/// <returns>ARP 记录,如果不存在则返回 null</returns>
public static async Task<ArpEntry?> GetArpEntryAsync(string ipAddress)
{
if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
try
{
string command = GetArpQueryCommand(ipAddress);
var result = await ExecuteCommandAsync(command);
if (result.IsSuccess)
{
var lines = result.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var entry = ParseArpEntry(line);
if (entry != null && entry.IpAddress == ipAddress)
{
return entry;
}
}
}
}
catch (Exception ex)
{
throw new Exception($"查询 ARP 记录失败: {ex.Message}");
}
return null;
}
/// <summary>
/// 获取 ARP 列表命令
/// </summary>
private static string GetArpListCommand()
{
return "arp -a";
}
/// <summary>
/// 获取 ARP 添加命令
/// </summary>
private static async Task<string> GetArpAddCommandAsync(string ipAddress, string macAddress, string? interfaceName)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (!string.IsNullOrWhiteSpace(interfaceName))
{
// 通过 arp -a 获取接口索引
var interfaceIdx = await GetWindowsInterfaceIndexAsync(interfaceName);
if (interfaceIdx.HasValue)
{
return $"netsh -c i i add neighbors {interfaceIdx.Value} {ipAddress} {macAddress}";
}
}
return $"arp -s {ipAddress} {macAddress}";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return string.IsNullOrWhiteSpace(interfaceName)
? $"arp -s {ipAddress} {macAddress}"
: $"arp -s {ipAddress} {macAddress} -i {interfaceName}";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return string.IsNullOrWhiteSpace(interfaceName)
? $"arp -s {ipAddress} {macAddress}"
: $"arp -s {ipAddress} {macAddress} ifscope {interfaceName}";
}
else
{
throw new PlatformNotSupportedException("不支持的操作系统平台");
}
}
/// <summary>
/// 获取 Windows 接口索引
/// </summary>
/// <param name="interfaceIp">接口IP地址</param>
/// <returns>接口索引十进制如果未找到则返回null</returns>
private static async Task<int?> GetWindowsInterfaceIndexAsync(string interfaceIp)
{
try
{
var result = await ExecuteCommandAsync("arp -a");
if (!result.IsSuccess)
return null;
var lines = result.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
// 匹配接口行格式: Interface: 172.6.1.5 --- 0xa
var interfacePattern = @"Interface:\s+(\d+\.\d+\.\d+\.\d+)\s+---\s+(0x[a-fA-F0-9]+)";
var match = Regex.Match(line, interfacePattern);
if (match.Success && match.Groups[1].Value == interfaceIp)
{
// 将十六进制索引转换为十进制
var hexIndex = match.Groups[2].Value;
// 去掉 "0x" 前缀
var hexValue = hexIndex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
? hexIndex.Substring(2)
: hexIndex;
if (int.TryParse(hexValue, System.Globalization.NumberStyles.HexNumber, null, out int decimalIndex))
{
logger.Debug($"找到接口 {interfaceIp} 的索引: {hexIndex} -> {decimalIndex}");
return decimalIndex;
}
}
}
logger.Warn($"未找到接口 {interfaceIp} 的索引");
return null;
}
catch (Exception ex)
{
logger.Error(ex, $"获取接口 {interfaceIp} 索引失败");
return null;
}
}
/// <summary>
/// 获取 ARP 删除命令
/// </summary>
private static string GetArpDeleteCommand(string ipAddress, string? interfaceName)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return $"arp -d {ipAddress}";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return string.IsNullOrWhiteSpace(interfaceName)
? $"arp -d {ipAddress}"
: $"arp -d {ipAddress} -i {interfaceName}";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return string.IsNullOrWhiteSpace(interfaceName)
? $"arp -d {ipAddress}"
: $"arp -d {ipAddress} ifscope {interfaceName}";
}
else
{
throw new PlatformNotSupportedException("不支持的操作系统平台");
}
}
/// <summary>
/// 获取 ARP 清空命令
/// </summary>
private static string GetArpClearCommand()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "arp -d *";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "ip neigh flush all";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "arp -d -a";
}
else
{
throw new PlatformNotSupportedException("不支持的操作系统平台");
}
}
/// <summary>
/// 获取 ARP 查询命令
/// </summary>
private static string GetArpQueryCommand(string ipAddress)
{
return $"arp -a {ipAddress}";
}
/// <summary>
/// 执行系统命令
/// </summary>
/// <param name="command">命令</param>
/// <returns>命令执行结果</returns>
private static async Task<CommandResult> ExecuteCommandAsync(string command)
{
try
{
ProcessStartInfo processInfo;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
processInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c {command}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
}
else
{
processInfo = new ProcessStartInfo
{
FileName = "/bin/bash",
Arguments = $"-c \"{command}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
}
logger.Debug($"Executing command: {processInfo.FileName} {processInfo.Arguments}");
using var process = new Process { StartInfo = processInfo };
process.Start();
var output = await process.StandardOutput.ReadToEndAsync();
var error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
logger.Debug($"Command output: {output}");
if (!string.IsNullOrWhiteSpace(error))
logger.Debug($"Command error: {error}");
logger.Debug($"Command exit code: {process.ExitCode}");
return new CommandResult
{
IsSuccess = process.ExitCode == 0,
Output = output,
Error = error,
ExitCode = process.ExitCode
};
}
catch (Exception ex)
{
logger.Error(ex, $"Command execution failed: {command}");
return new CommandResult
{
IsSuccess = false,
Error = ex.Message,
ExitCode = -1
};
}
}
/// <summary>
/// 解析 ARP 记录行
/// </summary>
/// <param name="line">ARP 记录行</param>
/// <returns>解析后的 ARP 记录</returns>
private static ArpEntry? ParseArpEntry(string line)
{
if (string.IsNullOrWhiteSpace(line))
return null;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return ParseWindowsArpEntry(line);
}
else
{
return ParseUnixArpEntry(line);
}
}
/// <summary>
/// 解析 Windows ARP 记录
/// </summary>
private static ArpEntry? ParseWindowsArpEntry(string line)
{
// 跳过空行和标题行
if (string.IsNullOrWhiteSpace(line) ||
line.Contains("Interface:") ||
line.Contains("Internet Address") ||
line.Contains("Physical Address") ||
line.Contains("Type"))
{
return null;
}
// Windows arp -a 输出格式: IP地址 物理地址 类型
// 示例: 172.6.0.1 e4-3a-6e-29-c3-5b dynamic
var pattern = @"^\s*(\d+\.\d+\.\d+\.\d+)\s+([a-fA-F0-9-]{17})\s+(\w+)\s*$";
var match = Regex.Match(line, pattern);
if (match.Success)
{
return new ArpEntry
{
IpAddress = match.Groups[1].Value,
MacAddress = FormatMacAddress(match.Groups[2].Value), // 格式化 MAC 地址
Type = match.Groups[3].Value
};
}
return null;
}
/// <summary>
/// 解析 Unix/Linux ARP 记录
/// </summary>
private static ArpEntry? ParseUnixArpEntry(string line)
{
// Unix/Linux arp -a 输出格式: hostname (ip) at mac [ether] PERM on interface
var pattern = @"(\S+)\s+\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([a-fA-F0-9:]{17})\s+\[(\w+)\]\s+(\w+)\s+on\s+(\S+)";
var match = Regex.Match(line, pattern);
if (match.Success)
{
return new ArpEntry
{
Hostname = match.Groups[1].Value,
IpAddress = match.Groups[2].Value,
MacAddress = FormatMacAddress(match.Groups[3].Value), // 格式化 MAC 地址
Type = match.Groups[5].Value,
Interface = match.Groups[6].Value
};
}
// 匹配简单格式: ip mac interface
var simplePattern = @"(\d+\.\d+\.\d+\.\d+)\s+([a-fA-F0-9:]{17})\s+(\S+)";
var simpleMatch = Regex.Match(line, simplePattern);
if (simpleMatch.Success)
{
return new ArpEntry
{
IpAddress = simpleMatch.Groups[1].Value,
MacAddress = FormatMacAddress(simpleMatch.Groups[2].Value), // 格式化 MAC 地址
Interface = simpleMatch.Groups[3].Value
};
}
return null;
}
/// <summary>
/// 判断当前进程是否具有管理员(或 root权限
/// </summary>
/// <returns>如果有管理员权限返回 true否则返回 false</returns>
public static bool IsAdministrator()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Windows: 检查当前用户是否为管理员
using var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
var principal = new System.Security.Principal.WindowsPrincipal(identity);
return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
}
else
{
// Unix/Linux/macOS: 检查是否为 root 用户
return Environment.UserName == "root" || (Environment.GetEnvironmentVariable("USER") == "root");
}
}
/// <summary>
/// 检查指定 IP 是否存在对应的 MAC如果不存在则删除原有 ARP 记录并新增
/// </summary>
/// <param name="ipAddress">IP 地址</param>
/// <param name="macAddress">MAC 地址</param>
/// <param name="interfaceName">网络接口名称(可选)</param>
/// <returns>是否成功</returns>
public static async Task<bool> CheckOrAddAsync(string ipAddress, string macAddress, string? interfaceName = null)
{
if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
if (string.IsNullOrWhiteSpace(macAddress))
throw new ArgumentException("MAC 地址不能为空", nameof(macAddress));
// 格式化 MAC 地址以适配不同操作系统
string formattedMac = FormatMacAddress(macAddress);
var entry = await GetArpEntryAsync(ipAddress);
if (entry != null && string.Equals(FormatMacAddress(entry.MacAddress), formattedMac, StringComparison.OrdinalIgnoreCase))
{
// 已存在且 MAC 匹配,无需操作
return true;
}
// 若存在但 MAC 不匹配,先删除
if (entry != null)
{
await DeleteArpEntryAsync(ipAddress, interfaceName);
}
// 新增 ARP 记录
var ret = await AddArpEntryAsync(ipAddress, formattedMac, interfaceName);
if (!ret) logger.Error($"添加 ARP 记录失败: {ipAddress} -> {formattedMac} on {interfaceName}");
return true;
}
/// <summary>
/// 格式化 MAC 地址为指定平台格式
/// </summary>
/// <param name="macAddress">原始 MAC 地址</param>
/// <returns>格式化后的 MAC 地址</returns>
public static string FormatMacAddress(string macAddress)
{
if (string.IsNullOrWhiteSpace(macAddress))
return string.Empty;
var cleaned = macAddress.Replace("-", "").Replace(":", "").ToLowerInvariant();
if (cleaned.Length != 12)
return macAddress;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Windows: XX-XX-XX-XX-XX-XX
return string.Join("-", Enumerable.Range(0, 6).Select(i => cleaned.Substring(i * 2, 2)));
}
else
{
// Unix/Linux/macOS: xx:xx:xx:xx:xx:xx
return string.Join(":", Enumerable.Range(0, 6).Select(i => cleaned.Substring(i * 2, 2)));
}
}
}
/// <summary>
/// ARP 记录条目
/// </summary>
public class ArpEntry
{
/// <summary>
/// [TODO:description]
/// </summary>
public string Hostname { get; set; } = string.Empty;
/// <summary>
/// [TODO:description]
/// </summary>
public string IpAddress { get; set; } = string.Empty;
/// <summary>
/// [TODO:description]
/// </summary>
public string MacAddress { get; set; } = string.Empty;
/// <summary>
/// [TODO:description]
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// [TODO:description]
/// </summary>
public string Interface { get; set; } = string.Empty;
/// <summary>
/// [TODO:description]
/// </summary>
public override string ToString()
{
return $"{IpAddress} -> {MacAddress} ({Interface})";
}
}
/// <summary>
/// 命令执行结果
/// </summary>
public class CommandResult
{
/// <summary>
/// [TODO:description]
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
public string Output { get; set; } = string.Empty;
/// <summary>
/// [TODO:description]
/// </summary>
public string Error { get; set; } = string.Empty;
/// <summary>
/// [TODO:description]
/// </summary>
public int ExitCode { get; set; }
}

View File

@@ -1,392 +0,0 @@
using System.Collections;
using DotNext;
namespace Common
{
/// <summary>
/// 数字处理工具
/// </summary>
public class Number
{
private static readonly byte[] BitReverseTable = new byte[] {
0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff
};
/// <summary>
/// 整数转成二进制字节数组
/// </summary>
/// <param name="num">整数</param>
/// <param name="length">整数长度</param>
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
/// <returns>二进制字节数组</returns>
public static Result<byte[]> NumberToBytes(ulong num, uint length, bool isLowNumHigh = false)
{
if (length > 8)
{
return new(new ArgumentException(
"Unsigned long number can't over 8 bytes(64 bits).",
nameof(length)
));
}
var arr = new byte[length];
if (isLowNumHigh)
{
for (var i = 0; i < length; i++)
{
arr[length - 1 - i] = Convert.ToByte((num >> (i << 3)) & (0xFF));
}
}
else
{
for (var i = 0; i < length; i++)
{
arr[i] = Convert.ToByte((num >> ((int)(length - 1 - i) << 3)) & (0xFF));
}
}
return arr;
}
/// <summary>
/// 二进制字节数组转成64bits整数
/// </summary>
/// <param name="bytes">二进制字节数组</param>
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
/// <returns>整数</returns>
public static Result<UInt64> BytesToUInt64(byte[] bytes, bool isLowNumHigh = false)
{
if (bytes.Length > 8)
{
return new(new ArgumentException(
"Unsigned long number can't over 8 bytes(64 bits).",
nameof(bytes)
));
}
UInt64 num = 0;
int len = bytes.Length;
try
{
if (isLowNumHigh)
{
for (var i = 0; i < len; i++)
{
num += Convert.ToUInt64((UInt64)bytes[len - 1 - i] << (i << 3));
}
}
else
{
for (var i = 0; i < len; i++)
{
num += Convert.ToUInt64((UInt64)bytes[i] << ((int)(len - 1 - i) << 3));
}
}
return num;
}
catch (Exception error)
{
return new(error);
}
}
/// <summary>
/// 二进制字节数组转成32bits整数
/// </summary>
/// <param name="bytes">二进制字节数组</param>
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
/// <returns>整数</returns>
public static Result<UInt32> BytesToUInt32(byte[] bytes, bool isLowNumHigh = false)
{
if (bytes.Length > 4)
{
return new(new ArgumentException(
"Unsigned long number can't over 4 bytes(32 bits).",
nameof(bytes)
));
}
UInt32 num = 0;
int len = bytes.Length;
try
{
if (isLowNumHigh)
{
for (var i = 0; i < len; i++)
{
num += Convert.ToUInt32((UInt32)bytes[len - 1 - i] << (i << 3));
}
}
else
{
for (var i = 0; i < len; i++)
{
num += Convert.ToUInt32((UInt32)bytes[i] << ((int)(len - 1 - i) << 3));
}
}
return num;
}
catch (Exception error)
{
return new(error);
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="uintArray">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public static Result<byte[]> UInt32ArrayToBytes(UInt32[] uintArray)
{
byte[] byteArray = new byte[uintArray.Length * 4];
try
{
Buffer.BlockCopy(uintArray, 0, byteArray, 0, uintArray.Length * 4);
return byteArray;
}
catch (Exception error)
{
return new(error);
}
}
/// <summary>
/// 比特合并成二进制字节
/// </summary>
/// <param name="bits1">第一个比特值</param>
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
/// <param name="bits2">第二个比特值</param>
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
/// <returns>合并后的二进制字节数组</returns>
public static Result<byte[]> MultiBitsToBytes(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
{
return NumberToBytes(MultiBitsToNumber(bits1, bits1Len, bits2, bits2Len).Value,
(bits1Len + bits2Len) % 8 != 0 ? (bits1Len + bits2Len) / 8 + 1 : (bits1Len + bits2Len) / 8);
}
/// <summary>
/// 比特合并成整型
/// </summary>
/// <param name="bits1">第一个比特值</param>
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
/// <param name="bits2">第二个比特值</param>
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
/// <returns>合并后的整型值</returns>
public static Result<ulong> MultiBitsToNumber(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
{
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
ulong num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
return num;
}
/// <summary>
/// 比特合并成整型
/// </summary>
/// <param name="bits1">第一个比特值</param>
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
/// <param name="bits2">第二个比特值</param>
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
/// <returns>合并后的整型值</returns>
public static Result<uint> MultiBitsToNumber(uint bits1, uint bits1Len, uint bits2, uint bits2Len)
{
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
uint num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
return num;
}
/// <summary>
/// 比特位检查
/// </summary>
/// <param name="srcBits">源比特值</param>
/// <param name="dstBits">目标比特值</param>
/// <param name="mask">掩码默认为全1</param>
/// <returns>检查结果(是否匹配)</returns>
public static bool BitsCheck(ulong srcBits, ulong dstBits, ulong mask = 0xFFFF_FFFF_FFFF_FFFF)
{
return (srcBits & mask) == dstBits;
}
/// <summary>
/// 比特位检查
/// </summary>
/// <param name="srcBits">源比特值</param>
/// <param name="dstBits">目标比特值</param>
/// <param name="mask">掩码默认为全1</param>
/// <returns>检查结果(是否匹配)</returns>
public static bool BitsCheck(uint srcBits, uint dstBits, uint mask = 0xFFFF_FFFF)
{
return (srcBits & mask) == dstBits;
}
/// <summary>
/// 获取整型对应位置的比特
/// </summary>
/// <param name="srcBits">整型数字</param>
/// <param name="location">位置</param>
/// <returns>比特</returns>
public static Result<bool> ToBit(UInt32 srcBits, int location)
{
if (location < 0)
return new(new ArgumentException(
"Location can't be negetive", nameof(location)));
return ((srcBits >> location) & ((UInt32)0b1)) == 1;
}
/// <summary>
/// 将BitArray转化为32bits无符号整型
/// </summary>
/// <param name="bits">BitArray比特数组</param>
/// <returns>32bits无符号整型</returns>
public static Result<UInt32> BitsToNumber(BitArray bits)
{
if (bits.Length > 32)
throw new ArgumentException("Argument length shall be at most 32 bits.");
var array = new UInt32[1];
bits.CopyTo(array, 0);
return array[0];
}
/// <summary>
/// 字符串转二进制字节数组
/// </summary>
/// <param name="str">输入的字符串</param>
/// <param name="numBase">进制默认为16进制</param>
/// <returns>转换后的二进制字节数组</returns>
public static byte[] StringToBytes(string str, int numBase = 16)
{
var len = str.Length;
var bytesLen = len / 2;
var bytes = new byte[bytesLen];
for (var i = 0; i < bytesLen; i++)
{
bytes[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);
}
return bytes;
}
/// <summary>
/// 反转字节数组中的子数组
/// </summary>
/// <param name="srcBytes">源字节数组</param>
/// <param name="distance">子数组的长度(反转的步长)</param>
/// <returns>反转后的字节数组</returns>
public static Result<byte[]> ReverseBytes(byte[] srcBytes, int distance)
{
if (distance <= 0)
return new(new ArgumentException("Distance can't be negetive", nameof(distance)));
var srcBytesLen = srcBytes.Length;
if (distance > srcBytesLen)
return new(new ArgumentException(
"Distance is larger than bytesArray", nameof(distance)));
if (srcBytesLen % distance != 0)
return new(new ArgumentException(
"The length of bytes can't be divided by distance without reminder", nameof(distance)));
var dstBytes = new byte[srcBytesLen];
var buffer = new byte[distance];
for (int i = 0; i < srcBytesLen; i += distance)
{
var end = i + distance;
buffer = srcBytes[i..end];
Array.Reverse(buffer);
Array.Copy(buffer, 0, dstBytes, i, distance);
}
return dstBytes;
}
/// <summary>
/// 反转字节内比特顺序(使用查找表的方法)
/// </summary>
/// <param name="srcByte">字节</param>
/// <returns>反转后的字节</returns>
public static byte ReverseBits(byte srcByte)
{
return BitReverseTable[srcByte];
}
/// <summary>
/// 反转字节数组的字节内比特顺序(使用查找表的方法)
/// </summary>
/// <param name="srcBytes">字节数组</param>
/// <returns>反转后的字节字节数组</returns>
public static byte[] ReverseBits(byte[] srcBytes)
{
var bytesLen = srcBytes.Length;
var dstBytes = new byte[bytesLen];
for (int i = 0; i < bytesLen; i++)
{
dstBytes[i] = BitReverseTable[srcBytes[i]];
}
return dstBytes;
}
}
/// <summary>
/// 字符串处理工具
/// </summary>
public class String
{
/// <summary>
/// 反转字符串
/// </summary>
/// <param name="s">输入的字符串</param>
/// <returns>反转后的字符串</returns>
public static string Reverse(string s)
{
char[] charArray = s.ToCharArray();
Array.Reverse(charArray);
return new string(charArray);
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Net;
using System.Net.Sockets;
public static class Global
{
public static readonly string LocalHost = "127.0.0.1";
public static readonly string DataPath = Path.Combine(Environment.CurrentDirectory, "data");
public static string GetLocalIPAddress()
{
var host = Dns.GetHostEntry(Dns.GetHostName());
foreach (var ip in host.AddressList)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
return ip.ToString();
}
}
throw new Exception("No network adapters with an IPv4 address in the system!");
}
}

567
server/src/Common/Image.cs Normal file
View File

@@ -0,0 +1,567 @@
using System.Text;
using DotNext;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
namespace Common;
/// <summary>
/// 图像处理工具
/// </summary>
public class Image
{
/// <summary>
/// 将 RGB565 格式转换为 RGB24 格式
/// RGB565: 5位红色 + 6位绿色 + 5位蓝色 = 16位 (2字节)
/// RGB24: 8位红色 + 8位绿色 + 8位蓝色 = 24位 (3字节)
/// </summary>
/// <param name="rgb565Data">RGB565格式的原始数据</param>
/// <param name="width">图像宽度</param>
/// <param name="height">图像高度</param>
/// <param name="isLittleEndian">是否为小端序默认为true</param>
/// <returns>RGB24格式的转换后数据</returns>
public static Result<byte[]> ConvertRGB565ToRGB24(byte[] rgb565Data, int width, int height, bool isLittleEndian = true)
{
if (rgb565Data == null)
return new(new ArgumentNullException(nameof(rgb565Data)));
if (width <= 0 || height <= 0)
return new(new ArgumentException("Width and height must be positive"));
// 计算像素数量
var expectedPixelCount = width * height;
var actualPixelCount = rgb565Data.Length / 2;
if (actualPixelCount < expectedPixelCount)
{
return new(new ArgumentException(
$"RGB565 data length insufficient. Expected: {expectedPixelCount * 2} bytes, Actual: {rgb565Data.Length} bytes"));
}
try
{
var pixelCount = Math.Min(actualPixelCount, expectedPixelCount);
var rgb24Data = new byte[pixelCount * 3];
for (int i = 0; i < pixelCount; i++)
{
// 读取 RGB565 数据
var rgb565Index = i * 2;
if (rgb565Index + 1 >= rgb565Data.Length) break;
// 组合成16位值
UInt16 rgb565;
if (isLittleEndian)
{
rgb565 = (UInt16)(rgb565Data[rgb565Index] | (rgb565Data[rgb565Index + 1] << 8));
}
else
{
rgb565 = (UInt16)((rgb565Data[rgb565Index] << 8) | rgb565Data[rgb565Index + 1]);
}
// 提取各颜色分量
var r5 = (rgb565 >> 11) & 0x1F; // 高5位为红色
var g6 = (rgb565 >> 5) & 0x3F; // 中间6位为绿色
var b5 = rgb565 & 0x1F; // 低5位为蓝色
// 转换为8位颜色值
var r8 = (byte)((r5 * 255) / 31); // 5位扩展到8位
var g8 = (byte)((g6 * 255) / 63); // 6位扩展到8位
var b8 = (byte)((b5 * 255) / 31); // 5位扩展到8位
// 存储到 RGB24 数组
var rgb24Index = (i % 2 == 0) ? ((i + 1) * 3) : ((i - 1) * 3);
rgb24Data[rgb24Index] = r8; // R
rgb24Data[rgb24Index + 1] = g8; // G
rgb24Data[rgb24Index + 2] = b8; // B
}
return rgb24Data;
}
catch (Exception ex)
{
return new(ex);
}
}
/// <summary>
/// 将 RGB24 格式转换为 RGB565 格式
/// RGB24: 8位红色 + 8位绿色 + 8位蓝色 = 24位 (3字节)
/// RGB565: 5位红色 + 6位绿色 + 5位蓝色 = 16位 (2字节)
/// </summary>
/// <param name="rgb24Data">RGB24格式的原始数据</param>
/// <param name="width">图像宽度</param>
/// <param name="height">图像高度</param>
/// <param name="isLittleEndian">是否为小端序默认为true</param>
/// <returns>RGB565格式的转换后数据</returns>
public static Result<byte[]> ConvertRGB24ToRGB565(byte[] rgb24Data, int width, int height, bool isLittleEndian = true)
{
if (rgb24Data == null)
return new(new ArgumentNullException(nameof(rgb24Data)));
if (width <= 0 || height <= 0)
return new(new ArgumentException("Width and height must be positive"));
var expectedPixelCount = width * height;
var actualPixelCount = rgb24Data.Length / 3;
if (actualPixelCount < expectedPixelCount)
{
return new(new ArgumentException(
$"RGB24 data length insufficient. Expected: {expectedPixelCount * 3} bytes, Actual: {rgb24Data.Length} bytes"));
}
try
{
var pixelCount = Math.Min(actualPixelCount, expectedPixelCount);
var rgb565Data = new byte[pixelCount * 2];
for (int i = 0; i < pixelCount; i++)
{
var rgb24Index = i * 3;
if (rgb24Index + 2 >= rgb24Data.Length) break;
// 读取 RGB24 数据
var r8 = rgb24Data[rgb24Index];
var g8 = rgb24Data[rgb24Index + 1];
var b8 = rgb24Data[rgb24Index + 2];
// 转换为5位、6位、5位
var r5 = (UInt16)((r8 * 31) / 255);
var g6 = (UInt16)((g8 * 63) / 255);
var b5 = (UInt16)((b8 * 31) / 255);
// 组合成16位值
var rgb565 = (UInt16)((r5 << 11) | (g6 << 5) | b5);
// 存储到 RGB565 数组
var rgb565Index = i * 2;
if (isLittleEndian)
{
rgb565Data[rgb565Index] = (byte)(rgb565 & 0xFF);
rgb565Data[rgb565Index + 1] = (byte)(rgb565 >> 8);
}
else
{
rgb565Data[rgb565Index] = (byte)(rgb565 >> 8);
rgb565Data[rgb565Index + 1] = (byte)(rgb565 & 0xFF);
}
}
return rgb565Data;
}
catch (Exception ex)
{
return new(ex);
}
}
/// <summary>
/// 将 RGB24 数据转换为 JPEG 格式
/// </summary>
/// <param name="rgb24Data">RGB24格式的图像数据</param>
/// <param name="width">图像宽度</param>
/// <param name="height">图像高度</param>
/// <param name="quality">JPEG质量1-100默认80</param>
/// <returns>JPEG格式的字节数组</returns>
public static Result<byte[]> ConvertRGB24ToJpeg(byte[] rgb24Data, int width, int height, int quality = 80)
{
if (rgb24Data == null)
return new(new ArgumentNullException(nameof(rgb24Data)));
if (width <= 0 || height <= 0)
return new(new ArgumentException("Width and height must be positive"));
if (quality < 1 || quality > 100)
return new(new ArgumentException("Quality must be between 1 and 100"));
var expectedDataLength = width * height * 3;
if (rgb24Data.Length < expectedDataLength)
{
return new(new ArgumentException(
$"RGB24 data length insufficient. Expected: {expectedDataLength} bytes, Actual: {rgb24Data.Length} bytes"));
}
try
{
using var image = new SixLabors.ImageSharp.Image<Rgb24>(width, height);
// 将 RGB 数据复制到 ImageSharp 图像
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int index = (y * width + x) * 3;
if (index + 2 < rgb24Data.Length)
{
var pixel = new Rgb24(rgb24Data[index], rgb24Data[index + 1], rgb24Data[index + 2]);
image[x, y] = pixel;
}
}
}
using var stream = new MemoryStream();
image.SaveAsJpeg(stream, new JpegEncoder { Quality = quality });
return stream.ToArray();
}
catch (Exception ex)
{
return new(ex);
}
}
/// <summary>
/// 将 RGB565 数据直接转换为 JPEG 格式
/// </summary>
/// <param name="rgb565Data">RGB565格式的图像数据</param>
/// <param name="width">图像宽度</param>
/// <param name="height">图像高度</param>
/// <param name="quality">JPEG质量1-100默认80</param>
/// <param name="isLittleEndian">是否为小端序默认为true</param>
/// <returns>JPEG格式的字节数组</returns>
public static Result<byte[]> ConvertRGB565ToJpeg(byte[] rgb565Data, int width, int height, int quality = 80, bool isLittleEndian = true)
{
// 先转换为RGB24
var rgb24Result = ConvertRGB565ToRGB24(rgb565Data, width, height, isLittleEndian);
if (!rgb24Result.IsSuccessful)
{
return new(rgb24Result.Error);
}
// 再转换为JPEG
return ConvertRGB24ToJpeg(rgb24Result.Value, width, height, quality);
}
/// <summary>
/// 创建 MJPEG 帧头部
/// </summary>
/// <param name="frameDataLength">帧数据长度</param>
/// <param name="boundary">边界字符串(默认为"--boundary"</param>
/// <returns>MJPEG帧头部字节数组</returns>
public static byte[] CreateMjpegFrameHeader(int frameDataLength, string boundary = "--boundary")
{
var header = $"{boundary}\r\nContent-Type: image/jpeg\r\nContent-Length: {frameDataLength}\r\n\r\n";
return Encoding.ASCII.GetBytes(header);
}
/// <summary>
/// 创建 MJPEG 帧尾部
/// </summary>
/// <returns>MJPEG帧尾部字节数组</returns>
public static byte[] CreateMjpegFrameFooter()
{
return Encoding.ASCII.GetBytes("\r\n");
}
/// <summary>
/// 将原始 JPEG 数据补全 JPEG 头部,生成完整的 JPEG 图片
/// </summary>
/// <param name="jpegData">原始 JPEG 扫描数据(不含头尾)</param>
/// <param name="width">图像宽度</param>
/// <param name="height">图像高度</param>
/// <param name="quantizationTable">量化表数组Y0-Y63, Cb0-Cb63, Cr0-Cr63共192个值</param>
/// <returns>完整的 JPEG 图片数据</returns>
public static Result<byte[]> CompleteJpegData(byte[] jpegData, int width, int height, uint[] quantizationTable)
{
if (jpegData == null)
return new(new ArgumentNullException(nameof(jpegData)));
if (width <= 0 || height <= 0)
return new(new ArgumentException("Width and height must be positive"));
if (quantizationTable == null || quantizationTable.Length != 192)
return new(new ArgumentException("Quantization table must contain exactly 192 values (64 Y + 64 Cb + 64 Cr)"));
try
{
var jpegBytes = new List<byte>();
// SOI (Start of Image)
jpegBytes.AddRange(new byte[] { 0xFF, 0xD8 });
// APP0 段
jpegBytes.AddRange(new byte[] {
0xFF, 0xE0, // APP0 marker
0x00, 0x10, // Length (16 bytes)
0x4A, 0x46, 0x49, 0x46, 0x00, // "JFIF\0"
0x01, 0x01, // Version 1.1
0x00, // Units: 0 = no units
0x00, 0x01, // X density (1)
0x00, 0x01, // Y density (1)
0x00, // Thumbnail width
0x00 // Thumbnail height
});
// DQT (Define Quantization Table) - Y table
jpegBytes.AddRange(new byte[] { 0xFF, 0xDB }); // DQT marker
jpegBytes.AddRange(new byte[] { 0x00, 0x43 }); // Length (67 bytes)
jpegBytes.Add(0x00); // Table ID (0 = Y table)
// 添加Y量化表 (quantizationTable[0-63])
for (int i = 0; i < 64; i++)
{
jpegBytes.Add((byte)Math.Min(255, quantizationTable[i]));
}
// DQT (Define Quantization Table) - CbCr table
jpegBytes.AddRange(new byte[] { 0xFF, 0xDB }); // DQT marker
jpegBytes.AddRange(new byte[] { 0x00, 0x43 }); // Length (67 bytes)
jpegBytes.Add(0x01); // Table ID (1 = CbCr table)
// 添加Cb量化表 (quantizationTable[64-127])但这里使用Cr表的数据作为CbCr共用
for (int i = 128; i < 192; i++) // 使用Cr量化表 (quantizationTable[128-191])
{
jpegBytes.Add((byte)Math.Min(255, quantizationTable[i]));
}
// SOF0 (Start of Frame)
jpegBytes.AddRange(new byte[] {
0xFF, 0xC0, // SOF0 marker
0x00, 0x11, // Length (17 bytes)
0x08, // Precision (8 bits)
(byte)((height >> 8) & 0xFF), (byte)(height & 0xFF), // Height
(byte)((width >> 8) & 0xFF), (byte)(width & 0xFF), // Width
0x03, // Number of components
0x01, 0x11, 0x00, // Y component
0x02, 0x11, 0x01, // Cb component
0x03, 0x11, 0x01 // Cr component
});
// DHT (Define Huffman Table) - DC Y table
jpegBytes.AddRange(new byte[] {
0xFF, 0xC4, // DHT marker
0x00, 0x1F, // Length
0x00, // Table class and ID (DC table 0)
// DC Y Huffman table
0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
});
// DHT (Define Huffman Table) - AC Y table (简化版)
jpegBytes.AddRange(new byte[] {
0xFF, 0xC4, // DHT marker
0x00, 0xB5, // Length
0x10 // Table class and ID (AC table 0)
});
// AC Y Huffman table数据
jpegBytes.AddRange(new byte[] {
0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D,
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0,
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5,
0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
0xF9, 0xFA
});
// DHT (Define Huffman Table) - DC CbCr table
jpegBytes.AddRange(new byte[] {
0xFF, 0xC4, // DHT marker
0x00, 0x1F, // Length
0x01, // Table class and ID (DC table 1)
// DC CbCr Huffman table
0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
});
// DHT (Define Huffman Table) - AC CbCr table
jpegBytes.AddRange(new byte[] {
0xFF, 0xC4, // DHT marker
0x00, 0xB5, // Length
0x11 // Table class and ID (AC table 1)
});
// AC CbCr Huffman table数据与AC Y table相同
jpegBytes.AddRange(new byte[] {
0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D,
0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0,
0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5,
0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
0xF9, 0xFA
});
// SOS (Start of Scan)
jpegBytes.AddRange(new byte[] {
0xFF, 0xDA, // SOS marker
0x00, 0x0C, // Length (12 bytes)
0x03, // Number of components
0x01, 0x00, // Y component, DC/AC table
0x02, 0x11, // Cb component, DC/AC table
0x03, 0x11, // Cr component, DC/AC table
0x00, 0x3F, 0x00 // Start of spectral, End of spectral, Ah/Al
});
// 添加原始 JPEG 扫描数据
jpegBytes.AddRange(jpegData);
// EOI (End of Image)
jpegBytes.AddRange(new byte[] { 0xFF, 0xD9 });
return jpegBytes.ToArray();
}
catch (Exception ex)
{
return new(ex);
}
}
/// <summary>
/// 从 JPEG 数据生成 MJPEG 帧数据
/// </summary>
/// <param name="jpegData">完整的 JPEG 数据</param>
/// <param name="boundary">边界字符串(默认为"--boundary"</param>
/// <returns>MJPEG 帧数据</returns>
public static Result<(byte[] header, byte[] footer, byte[] data)> CreateMjpegFrameFromJpeg(
byte[] jpegData, string boundary = "--boundary")
{
if (jpegData == null)
return new(new ArgumentNullException(nameof(jpegData)));
// 验证是否为有效的 JPEG 数据
if (jpegData.Length < 4 || jpegData[0] != 0xFF || jpegData[1] != 0xD8)
{
return new(new ArgumentException("Invalid JPEG data: missing JPEG header"));
}
try
{
var header = CreateMjpegFrameHeader(jpegData.Length, boundary);
var footer = CreateMjpegFrameFooter();
var totalLength = header.Length + jpegData.Length + footer.Length;
var frameData = new byte[totalLength];
var offset = 0;
Array.Copy(header, 0, frameData, offset, header.Length);
offset += header.Length;
Array.Copy(jpegData, 0, frameData, offset, jpegData.Length);
offset += jpegData.Length;
Array.Copy(footer, 0, frameData, offset, footer.Length);
return (header, footer, frameData);
}
catch (Exception ex)
{
return new(ex);
}
}
/// <summary>
/// 创建完整的 MJPEG 帧数据
/// </summary>
/// <param name="jpegData">JPEG数据</param>
/// <param name="boundary">边界字符串(默认为"--boundary"</param>
/// <returns>完整的MJPEG帧数据</returns>
public static Result<(byte[] header, byte[] footer, byte[] data)> CreateMjpegFrame(
byte[] jpegData, string boundary = "--boundary")
{
if (jpegData == null)
return new(new ArgumentNullException(nameof(jpegData)));
try
{
var header = CreateMjpegFrameHeader(jpegData.Length, boundary);
var footer = CreateMjpegFrameFooter();
var totalLength = header.Length + jpegData.Length + footer.Length;
var frameData = new byte[totalLength];
var offset = 0;
Array.Copy(header, 0, frameData, offset, header.Length);
offset += header.Length;
Array.Copy(jpegData, 0, frameData, offset, jpegData.Length);
offset += jpegData.Length;
Array.Copy(footer, 0, frameData, offset, footer.Length);
return (header, footer, frameData);
}
catch (Exception ex)
{
return new(ex);
}
}
/// <summary>
/// 验证图像数据长度是否正确
/// </summary>
/// <param name="data">图像数据</param>
/// <param name="width">图像宽度</param>
/// <param name="height">图像高度</param>
/// <param name="bytesPerPixel">每像素字节数</param>
/// <returns>验证结果</returns>
public static bool ValidateImageDataLength(byte[] data, int width, int height, int bytesPerPixel)
{
if (data == null || width <= 0 || height <= 0 || bytesPerPixel <= 0)
return false;
var expectedLength = width * height * bytesPerPixel;
return data.Length >= expectedLength;
}
/// <summary>
/// 获取图像格式信息
/// </summary>
/// <param name="format">图像格式枚举</param>
/// <returns>格式信息</returns>
public static ImageFormatInfo GetImageFormatInfo(ImageFormat format)
{
return format switch
{
ImageFormat.RGB565 => new ImageFormatInfo("RGB565", 2, "16-bit RGB format (5R+6G+5B)"),
ImageFormat.RGB24 => new ImageFormatInfo("RGB24", 3, "24-bit RGB format (8R+8G+8B)"),
ImageFormat.RGBA32 => new ImageFormatInfo("RGBA32", 4, "32-bit RGBA format (8R+8G+8B+8A)"),
ImageFormat.Grayscale8 => new ImageFormatInfo("Grayscale8", 1, "8-bit grayscale format"),
_ => new ImageFormatInfo("Unknown", 0, "Unknown image format")
};
}
}
/// <summary>
/// 图像格式枚举
/// </summary>
public enum ImageFormat
{
/// <summary>
/// RGB565
/// </summary>
RGB565,
/// <summary>
/// RGB888 / RGB24
/// </summary>
RGB24,
/// <summary>
/// RGBA8888 / RGBA32
/// </summary>
RGBA32,
/// <summary>
/// 灰度图
/// </summary>
Grayscale8
}
/// <summary>
/// 图像格式信息
/// </summary>
public record ImageFormatInfo(string Name, int BytesPerPixel, string Description);

472
server/src/Common/Number.cs Normal file
View File

@@ -0,0 +1,472 @@
using System.Collections;
using DotNext;
namespace Common;
/// <summary>
/// 数字处理工具
/// </summary>
public class Number
{
private static readonly byte[] BitReverseTable = new byte[] {
0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff
};
/// <summary>
/// 整数转成二进制字节数组
/// </summary>
/// <param name="num">整数</param>
/// <param name="length">整数长度</param>
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
/// <returns>二进制字节数组</returns>
public static Result<byte[]> NumberToBytes(ulong num, uint length, bool isLowNumHigh = false)
{
if (length > 8)
{
return new(new ArgumentException(
"Unsigned long number can't over 8 bytes(64 bits).",
nameof(length)
));
}
var arr = new byte[length];
if (isLowNumHigh)
{
for (var i = 0; i < length; i++)
{
arr[i] = Convert.ToByte((num >> (i << 3)) & (0xFF));
}
}
else
{
for (var i = 0; i < length; i++)
{
arr[i] = Convert.ToByte((num >> ((int)(length - 1 - i) << 3)) & (0xFF));
}
}
return arr;
}
/// <summary>
/// 二进制字节数组转成64bits整数
/// </summary>
/// <param name="bytes">二进制字节数组</param>
/// <param name="offset">字节数组偏移量</param>
/// <param name="numLength">字节数组长度</param>
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
/// <returns>整数</returns>
public static Result<UInt64> BytesToUInt64(byte[] bytes, int offset = 0, int numLength = 8, bool isLowNumHigh = false)
{
if (bytes.Length < offset)
{
return new(new ArgumentException($"The Length of bytes is less than offset"));
}
if (numLength > 8)
{
return new(new ArgumentException($"The Length of bytes is greater than 8"));
}
if (numLength <= 0) return 0;
try
{
byte[] numBytes = new byte[8]; // 8字节
int copyLen = Math.Min(numLength, bytes.Length - offset);
if (isLowNumHigh)
{
// 小端:拷贝到低位
Buffer.BlockCopy(bytes, offset, numBytes, 0, copyLen);
}
else
{
// 大端:拷贝到高位
byte[] temp = new byte[copyLen];
Buffer.BlockCopy(bytes, offset, temp, 0, copyLen);
Array.Reverse(temp);
Buffer.BlockCopy(temp, 0, numBytes, 8 - copyLen, copyLen);
}
UInt64 num = BitConverter.ToUInt64(numBytes, 0);
return num;
}
catch (Exception error)
{
return new(error);
}
}
/// <summary>
/// 二进制字节数组转成64bits整数
/// </summary>
/// <param name="bytes">二进制字节数组</param>
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
/// <returns>整数</returns>
public static Result<UInt64> BytesToUInt64(byte[] bytes, bool isLowNumHigh = false)
{
return BytesToUInt64(bytes, 0, 8, isLowNumHigh);
}
/// <summary>
/// 二进制字节数组转成64bits整数
/// </summary>
/// <param name="bytes">二进制字节数组</param>
/// <returns>整数</returns>
public static Result<UInt64> BytesToUInt64(byte[] bytes)
{
return BytesToUInt64(bytes, 0, 8, false);
}
/// <summary>
/// 二进制字节数组转成32bits整数
/// </summary>
/// <param name="bytes">二进制字节数组</param>
/// <param name="offset">字节数组偏移量</param>
/// <param name="numLength">整形所占字节数组长度</param>
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
/// <returns>整数</returns>
public static Result<UInt32> BytesToUInt32(byte[] bytes, int offset = 0, int numLength = 4, bool isLowNumHigh = false)
{
if (bytes.Length < offset)
{
return new(new ArgumentException($"The Length of bytes is less than offset"));
}
if (numLength > 4)
{
return new(new ArgumentException($"The Length of bytes is greater than 4"));
}
if (bytes.Length < offset)
{
return new(new ArgumentException($"The Length of bytes is less than offset"));
}
if (numLength > 4)
{
return new(new ArgumentException($"The Length of bytes is greater than 4"));
}
if (numLength <= 0) return 0;
try
{
byte[] numBytes = new byte[4]; // 4字节
int copyLen = Math.Min(numLength, bytes.Length - offset);
if (copyLen < 0) copyLen = 0;
if (isLowNumHigh)
{
// 小端:拷贝到低位
if (copyLen > 0)
{
Buffer.BlockCopy(bytes, offset, numBytes, 0, copyLen);
}
}
else
{
// 大端:拷贝到高位
if (copyLen > 0)
{
byte[] temp = new byte[copyLen];
Buffer.BlockCopy(bytes, offset, temp, 0, copyLen);
Array.Reverse(temp);
Buffer.BlockCopy(temp, 0, numBytes, 4 - copyLen, copyLen);
}
}
UInt32 num = BitConverter.ToUInt32(numBytes, 0);
return num;
}
catch (Exception error)
{
return new(error);
}
}
/// <summary>
/// 二进制字节数组转成32bits整数
/// </summary>
/// <param name="bytes">二进制字节数组</param>
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
/// <returns>整数</returns>
public static Result<UInt32> BytesToUInt32(byte[] bytes, bool isLowNumHigh = false)
{
return BytesToUInt32(bytes, 0, 4, isLowNumHigh);
}
/// <summary>
/// 二进制字节数组转成32bits整数
/// </summary>
/// <param name="bytes">二进制字节数组</param>
/// <returns>整数</returns>
public static Result<UInt32> BytesToUInt32(byte[] bytes)
{
return BytesToUInt32(bytes, 0, 4, false);
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="uintArray">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public static Result<byte[]> UInt32ArrayToBytes(UInt32[] uintArray)
{
byte[] byteArray = new byte[uintArray.Length * 4];
try
{
Buffer.BlockCopy(uintArray, 0, byteArray, 0, uintArray.Length * 4);
return byteArray;
}
catch (Exception error)
{
return new(error);
}
}
/// <summary>
/// 比特合并成二进制字节
/// </summary>
/// <param name="bits1">第一个比特值</param>
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
/// <param name="bits2">第二个比特值</param>
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
/// <returns>合并后的二进制字节数组</returns>
public static Result<byte[]> MultiBitsToBytes(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
{
return NumberToBytes(MultiBitsToNumber(bits1, bits1Len, bits2, bits2Len).Value,
(bits1Len + bits2Len) % 8 != 0 ? (bits1Len + bits2Len) / 8 + 1 : (bits1Len + bits2Len) / 8);
}
/// <summary>
/// 比特合并成整型
/// </summary>
/// <param name="bits1">第一个比特值</param>
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
/// <param name="bits2">第二个比特值</param>
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
/// <returns>合并后的整型值</returns>
public static Result<ulong> MultiBitsToNumber(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
{
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
ulong num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
return num;
}
/// <summary>
/// 比特合并成整型
/// </summary>
/// <param name="bits1">第一个比特值</param>
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
/// <param name="bits2">第二个比特值</param>
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
/// <returns>合并后的整型值</returns>
public static Result<uint> MultiBitsToNumber(uint bits1, uint bits1Len, uint bits2, uint bits2Len)
{
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
uint num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
return num;
}
/// <summary>
/// 比特位检查
/// </summary>
/// <param name="srcBits">源比特值</param>
/// <param name="dstBits">目标比特值</param>
/// <param name="mask">掩码默认为全1</param>
/// <returns>检查结果(是否匹配)</returns>
public static bool BitsCheck(ulong srcBits, ulong dstBits, ulong mask = 0xFFFF_FFFF_FFFF_FFFF)
{
return (srcBits & mask) == dstBits;
}
/// <summary>
/// 比特位检查
/// </summary>
/// <param name="srcBits">源比特值</param>
/// <param name="dstBits">目标比特值</param>
/// <param name="mask">掩码默认为全1</param>
/// <returns>检查结果(是否匹配)</returns>
public static bool BitsCheck(uint srcBits, uint dstBits, uint mask = 0xFFFF_FFFF)
{
return (srcBits & mask) == dstBits;
}
/// <summary>
/// 获取整型对应位置的比特
/// </summary>
/// <param name="srcBits">整型数字</param>
/// <param name="location">位置</param>
/// <returns>比特</returns>
public static Result<bool> ToBit(UInt32 srcBits, int location)
{
if (location < 0)
return new(new ArgumentException(
"Location can't be negetive", nameof(location)));
return ((srcBits >> location) & ((UInt32)0b1)) == 1;
}
/// <summary>
/// 将BitArray转化为32bits无符号整型
/// </summary>
/// <param name="bits">BitArray比特数组</param>
/// <returns>32bits无符号整型</returns>
public static Result<UInt32> BitsToNumber(BitArray bits)
{
if (bits.Length > 32)
throw new ArgumentException("Argument length shall be at most 32 bits.");
var array = new UInt32[1];
bits.CopyTo(array, 0);
return array[0];
}
/// <summary>
/// 字符串转二进制字节数组
/// </summary>
/// <param name="str">输入的字符串</param>
/// <param name="numBase">进制默认为16进制</param>
/// <returns>转换后的二进制字节数组</returns>
public static byte[] StringToBytes(string str, int numBase = 16)
{
var len = str.Length;
var bytesLen = len / 2;
var bytes = new byte[bytesLen];
for (var i = 0; i < bytesLen; i++)
{
bytes[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);
}
return bytes;
}
/// <summary>
/// 反转字节数组中的子数组
/// </summary>
/// <param name="srcBytes">源字节数组</param>
/// <param name="distance">子数组的长度(反转的步长)</param>
/// <returns>反转后的字节数组</returns>
public static Result<byte[]> ReverseBytes(byte[] srcBytes, int distance)
{
if (distance <= 0)
return new(new ArgumentException("Distance can't be negetive", nameof(distance)));
var srcBytesLen = srcBytes.Length;
if (distance > srcBytesLen)
return new(new ArgumentException(
"Distance is larger than bytesArray", nameof(distance)));
if (srcBytesLen % distance != 0)
return new(new ArgumentException(
"The length of bytes can't be divided by distance without reminder", nameof(distance)));
var dstBytes = new byte[srcBytesLen];
var buffer = new byte[distance];
for (int i = 0; i < srcBytesLen; i += distance)
{
Buffer.BlockCopy(srcBytes, i, buffer, 0, distance);
Array.Reverse(buffer);
Buffer.BlockCopy(buffer, 0, dstBytes, i, distance);
}
return dstBytes;
}
/// <summary>
/// 反转字节内比特顺序(使用查找表的方法)
/// </summary>
/// <param name="srcByte">字节</param>
/// <returns>反转后的字节</returns>
public static byte ReverseBits(byte srcByte)
{
return BitReverseTable[srcByte];
}
/// <summary>
/// 反转字节数组的字节内比特顺序(使用查找表的方法)
/// </summary>
/// <param name="srcBytes">字节数组</param>
/// <returns>反转后的字节字节数组</returns>
public static byte[] ReverseBits(byte[] srcBytes)
{
var bytesLen = srcBytes.Length;
var dstBytes = new byte[bytesLen];
for (int i = 0; i < bytesLen; i++)
{
dstBytes[i] = BitReverseTable[srcBytes[i]];
}
return dstBytes;
}
/// <summary>
/// 获取数字的长度
/// </summary>
/// <param name="number">数字</param>
/// <returns>数字的长度</returns>
public static int GetLength(int number)
{
// 将整数转换为字符串
string numberString = number.ToString();
// 返回字符串的长度
return numberString.Length;
}
/// <summary>
/// 计算整形的幂
/// </summary>
/// <param name="x">底数</param>
/// <param name="pow">幂</param>
/// <returns>计算结果</returns>
public static int IntPow(int x, int pow)
{
int ret = 1;
while (pow != 0)
{
if ((pow & 1) == 1)
ret *= x;
x *= x;
pow >>= 1;
}
return ret;
}
}

View File

@@ -0,0 +1,116 @@
using System.Collections.Concurrent;
using DotNext;
namespace Common;
/// <summary>
/// [TODO:description]
/// </summary>
public class SemaphorePool
{
private SemaphoreSlim semaphore;
private ConcurrentQueue<int> queue;
private int beginNum;
/// <summary>
/// [TODO:description]
/// </summary>
public int RemainingCount { get; }
/// <summary>
/// [TODO:description]
/// </summary>
public int MaxCount { get; }
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="initialCount">[TODO:parameter]</param>
/// <param name="beginNum">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public SemaphorePool(int initialCount, int beginNum = 0)
{
semaphore = new SemaphoreSlim(initialCount);
queue = new ConcurrentQueue<int>();
this.beginNum = beginNum;
this.RemainingCount = initialCount;
this.MaxCount = initialCount;
for (int i = 0; i < initialCount; i++)
{
queue.Enqueue(beginNum + i);
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="initialCount">[TODO:parameter]</param>
/// <param name="maxCount">[TODO:parameter]</param>
/// <param name="beginNum">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public SemaphorePool(int initialCount, int maxCount, int beginNum = 0)
{
semaphore = new SemaphoreSlim(initialCount, maxCount);
queue = new ConcurrentQueue<int>();
this.beginNum = beginNum;
this.RemainingCount = initialCount;
this.MaxCount = maxCount;
for (int i = 0; i < initialCount; i++)
{
queue.Enqueue(beginNum + i);
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public Result<int> Wait()
{
semaphore.Wait();
int pop;
if (queue.TryDequeue(out pop))
{
return pop;
}
else
{
return new(new Exception($"TODO"));
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<int>> WaitAsync()
{
await semaphore.WaitAsync();
int pop;
if (queue.TryDequeue(out pop))
{
return pop;
}
else
{
return new(new Exception($"TODO"));
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public void Release()
{
semaphore.Release();
queue.Clear();
for (int i = 0; i < MaxCount; i++)
{
queue.Enqueue(beginNum + i);
}
}
}

View File

@@ -0,0 +1,29 @@
namespace Common;
/// <summary>
/// 字符串处理工具
/// </summary>
public class String
{
/// <summary>
/// 反转字符串
/// </summary>
/// <param name="s">输入的字符串</param>
/// <returns>反转后的字符串</returns>
public static string Reverse(string s)
{
char[] charArray = s.ToCharArray();
Array.Reverse(charArray);
return new string(charArray);
}
public static string BytesToString(byte[] bytes, string separator = "")
{
return BitConverter.ToString(bytes).Replace("-", separator.ToString());
}
public static string BytesToBase64(byte[] bytes)
{
return Convert.ToBase64String(bytes);
}
}

View File

@@ -1,5 +1,11 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
namespace server.Controllers;
@@ -12,58 +18,486 @@ public class DataController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly Database.UserManager _userManager = new();
// 固定的实验板IP,端口,MAC地址
private const string BOARD_IP = "169.254.109.0";
/// <summary>
/// 创建数据库表
/// 获取本机IP地址优先选择与实验板同网段的IP
/// </summary>
/// <returns>插入的记录数</returns>
[EnableCors("Development")]
[HttpPost("CreateTable")]
public IResult CreateTables()
/// <returns>本机IP地址</returns>
private IPAddress GetLocalIPAddress()
{
using var db = new Database.AppDataConnection();
db.CreateAllTables();
return TypedResults.Ok();
try
{
var boardIpSegments = BOARD_IP.Split('.').Take(3).ToArray();
// 优先选择与实验板IP前三段相同的IP
var sameSegmentIP = System.Net.NetworkInformation.NetworkInterface
.GetAllNetworkInterfaces()
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
.SelectMany(nic => nic.GetIPProperties().UnicastAddresses)
.Where(addr => addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
.Select(addr => addr.Address)
.FirstOrDefault(addr =>
{
var segments = addr.ToString().Split('.');
return segments.Length == 4 &&
segments[0] == boardIpSegments[0] &&
segments[1] == boardIpSegments[1] &&
segments[2] == boardIpSegments[2];
});
if (sameSegmentIP != null)
return sameSegmentIP;
// 如果没有找到同网段的IP返回第一个可用的IP
return System.Net.NetworkInformation.NetworkInterface
.GetAllNetworkInterfaces()
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
.SelectMany(nic => nic.GetIPProperties().UnicastAddresses)
.Where(addr => addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
.Select(addr => addr.Address)
.FirstOrDefault() ?? IPAddress.Loopback;
}
catch (Exception ex)
{
logger.Error(ex, "获取本机IP地址失败");
return IPAddress.Loopback;
}
}
/// <summary>
/// 删除数据库表
/// 用户登录,获取 JWT 令牌
/// </summary>
/// <returns>插入的记录数</returns>
[EnableCors("Development")]
[HttpDelete("DropTables")]
public IResult DropTables()
/// <param name="name">用户名</param>
/// <param name="password">用户密码</param>
/// <returns>JWT 令牌字符串</returns>
[HttpPost("Login")]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult Login(string name, string password)
{
using var db = new Database.AppDataConnection();
db.DropAllTables();
return TypedResults.Ok();
// 验证用户密码
var ret = _userManager.CheckUserPassword(name, password);
if (!ret.IsSuccessful) return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败");
if (!ret.Value.HasValue) return BadRequest("用户名或密码错误");
var user = ret.Value.Value;
// 生成 JWT
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes("my secret key 1234567890my secret key 1234567890");
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Email, user.EMail),
new Claim(ClaimTypes.Role, user.Permission.ToString())
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
Audience = "dlut.edu.cn",
Issuer = "dlut.edu.cn",
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var jwt = tokenHandler.WriteToken(token);
return Ok(jwt);
}
/// <summary>
/// 获取所有用户
/// 测试用户认证,需携带有效 JWT
/// </summary>
/// <returns>用户列表</returns>
[HttpGet("AllUsers")]
public IResult AllUsers()
/// <returns>认证成功信息</returns>
[Authorize]
[HttpGet("TestAuth")]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public IActionResult TestAuth()
{
using var db = new Database.AppDataConnection();
var ret = db.User.ToList();
return TypedResults.Ok(ret);
return Ok("认证成功!");
}
/// <summary>
/// 测试管理员用户认证,需携带有效 JWT
/// </summary>
/// <returns>认证成功信息</returns>
[Authorize("Admin")]
[HttpGet("TestAdminAuth")]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public IActionResult TestAdminAuth()
{
return Ok("认证成功!");
}
/// <summary>
/// 获取当前用户信息
/// </summary>
/// <returns>用户信息包括ID、用户名、邮箱和板卡ID</returns>
[Authorize]
[HttpGet("GetUserInfo")]
[EnableCors("Users")]
[ProducesResponseType(typeof(UserInfo), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public IActionResult GetUserInfo()
{
// Get User Name
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("未找到用户名信息");
// Get User Info
var ret = _userManager.GetUserByName(userName);
if (!ret.IsSuccessful)
return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败");
if (!ret.Value.HasValue)
return BadRequest("用户不存在");
var user = ret.Value.Value;
return Ok(new UserInfo
{
ID = user.ID,
Name = user.Name,
EMail = user.EMail,
BoardID = user.BoardID,
BoardExpireTime = user.BoardExpireTime,
});
}
/// <summary>
/// 注册新用户
/// </summary>
/// <param name="name">用户名</param>
/// <returns>操作结果</returns>
/// <param name="name">用户名不超过255个字符</param>
/// <param name="email">邮箱地址</param>
/// <param name="password">用户密码</param>
/// <returns>操作结果,成功返回 true失败返回错误信息</returns>
[HttpPost("SignUpUser")]
public IResult SignUpUser(string name)
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult SignUpUser(string name, string email, string password)
{
if (name.Length > 255)
return TypedResults.BadRequest("Name Couln't over 255 characters");
// 验证输入参数
if (string.IsNullOrWhiteSpace(name))
return BadRequest("用户名不能为空");
using var db = new Database.AppDataConnection();
var ret = db.AddUser(name);
return TypedResults.Ok(ret);
if (name.Length > 255)
return BadRequest("用户名不能超过255个字符");
if (string.IsNullOrWhiteSpace(email))
return BadRequest("邮箱不能为空");
if (string.IsNullOrWhiteSpace(password))
return BadRequest("密码不能为空");
try
{
var ret = _userManager.AddUser(name, email, password);
return Ok(ret);
}
catch (Exception ex)
{
logger.Error(ex, "注册用户时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "注册失败,请稍后重试");
}
}
/// <summary>
/// 获取一个空闲的实验板(普通用户权限)
/// </summary>
/// <param name="durationHours">绑定持续时间小时默认为1小时</param>
[Authorize]
[HttpGet("GetAvailableBoard")]
[EnableCors("Users")]
[ProducesResponseType(typeof(Database.Board), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IActionResult> GetAvailableBoard(int durationHours = 1)
{
try
{
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("未找到用户名信息");
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
return BadRequest("用户不存在");
var user = userRet.Value.Value;
var expireTime = DateTime.UtcNow.AddHours(durationHours);
var boardOpt = _userManager.GetAvailableBoard(user.ID, expireTime);
if (!boardOpt.HasValue)
return NotFound("没有可用的实验板");
var boardInfo = boardOpt.Value;
if (!(await ArpClient.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString())))
{
logger.Error($"无法配置ARP实验板可能会无法连接");
}
return Ok(boardInfo);
}
catch (Exception ex)
{
logger.Error(ex, "获取空闲实验板时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
}
}
/// <summary>
/// 解除当前用户绑定的实验板(普通用户权限)
/// </summary>
[Authorize]
[HttpPost("UnbindBoard")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult UnbindBoard()
{
try
{
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("未找到用户名信息");
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
return BadRequest("用户不存在");
var user = userRet.Value.Value;
var result = _userManager.UnbindUserFromBoard(user.ID);
return Ok(result > 0);
}
catch (Exception ex)
{
logger.Error(ex, "解除实验板绑定时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "解除失败,请稍后重试");
}
}
/// <summary>
/// 用户根据实验板ID获取实验板信息普通用户权限
/// </summary>
[Authorize]
[HttpGet("GetBoardByID")]
[EnableCors("Users")]
[ProducesResponseType(typeof(Database.Board), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> GetBoardByID(Guid id)
{
try
{
var ret = _userManager.GetBoardByID(id);
if (!ret.IsSuccessful)
return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败");
if (!ret.Value.HasValue)
return NotFound("未找到对应的实验板");
var boardInfo = ret.Value.Value;
if (!(await ArpClient.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString())))
{
logger.Error($"无法配置ARP实验板可能会无法连接");
}
return Ok(boardInfo);
}
catch (Exception ex)
{
logger.Error(ex, "获取实验板信息时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
}
}
/// <summary>
/// 新增板子(管理员权限)
/// </summary>
[Authorize("Admin")]
[HttpPost("AddBoard")]
[EnableCors("Users")]
[ProducesResponseType(typeof(Guid), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult AddBoard(string name)
{
if (string.IsNullOrWhiteSpace(name))
return BadRequest("板子名称不能为空");
try
{
var ret = _userManager.AddBoard(name);
return Ok(ret);
}
catch (Exception ex)
{
logger.Error(ex, "新增板子时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "新增失败,请稍后重试");
}
}
/// <summary>
/// 删除板子(管理员权限)
/// </summary>
[Authorize("Admin")]
[HttpDelete("DeleteBoard")]
[EnableCors("Users")]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult DeleteBoard(Guid id)
{
if (id == Guid.Empty)
return BadRequest("板子Guid不能为空");
try
{
var ret = _userManager.DeleteBoardByID(id);
return Ok(ret);
}
catch (Exception ex)
{
logger.Error(ex, "删除板子时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "删除失败,请稍后重试");
}
}
/// <summary>
/// 获取全部板子(管理员权限)
/// </summary>
[Authorize("Admin")]
[HttpGet("GetAllBoards")]
[EnableCors("Users")]
[ProducesResponseType(typeof(Database.Board[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetAllBoards()
{
try
{
var boards = _userManager.GetAllBoard();
return Ok(boards);
}
catch (Exception ex)
{
logger.Error(ex, "获取全部板子时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
}
}
/// <summary>
/// 更新板卡名称(管理员权限)
/// </summary>
[Authorize("Admin")]
[HttpPost("UpdateBoardName")]
[EnableCors("Users")]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult UpdateBoardName(Guid boardId, string newName)
{
if (boardId == Guid.Empty)
return BadRequest("板子Guid不能为空");
if (string.IsNullOrWhiteSpace(newName))
return BadRequest("新名称不能为空");
try
{
var result = _userManager.UpdateBoardName(boardId, newName);
return Ok(result);
}
catch (Exception ex)
{
logger.Error(ex, "更新板卡名称时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "更新失败,请稍后重试");
}
}
/// <summary>
/// 更新板卡状态(管理员权限)
/// </summary>
[Authorize("Admin")]
[HttpPost("UpdateBoardStatus")]
[EnableCors("Users")]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult UpdateBoardStatus(Guid boardId, Database.BoardStatus newStatus)
{
if (boardId == Guid.Empty)
return BadRequest("板子Guid不能为空");
try
{
var result = _userManager.UpdateBoardStatus(boardId, newStatus);
return Ok(result);
}
catch (Exception ex)
{
logger.Error(ex, "更新板卡状态时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "更新失败,请稍后重试");
}
}
[HttpPost("AddEmptyBoard")]
[EnableCors("Development")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult AddEmptyBoard()
{
try
{
var boardId = _userManager.AddBoard("Test");
var result = _userManager.UpdateBoardStatus(boardId, Database.BoardStatus.Available);
return Ok();
}
catch (Exception ex)
{
logger.Error(ex, "新增板子时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "新增失败,请稍后重试");
}
}
/// <summary>
/// [TODO:description]
/// </summary>
public class UserInfo
{
/// <summary>
/// 用户的唯一标识符
/// </summary>
public Guid ID { get; set; }
/// <summary>
/// 用户的名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 用户的电子邮箱
/// </summary>
public required string EMail { get; set; }
/// <summary>
/// 用户关联的板卡ID
/// </summary>
public Guid BoardID { get; set; }
/// <summary>
/// 用户绑定板子的过期时间
/// </summary>
public DateTime? BoardExpireTime { get; set; }
}
}

View File

@@ -0,0 +1,468 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Peripherals.DebuggerClient;
namespace server.Controllers;
/// <summary>
/// FPGA调试器控制器提供信号捕获、触发、数据读取等调试相关API
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class DebuggerController : ControllerBase
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly Database.UserManager _userManager = new();
/// <summary>
/// 获取当前用户绑定的调试器实例
/// </summary>
private DebuggerClient? GetDebugger()
{
try
{
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return null;
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
return null;
var user = userRet.Value.Value;
if (user.BoardID == Guid.Empty)
return null;
var boardRet = _userManager.GetBoardByID(user.BoardID);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
return null;
var board = boardRet.Value.Value;
return new DebuggerClient(board.IpAddr, board.Port, 7);
}
catch (Exception ex)
{
logger.Error(ex, "获取调试器实例时发生异常");
return null;
}
}
/// <summary>
/// 设置指定信号线的捕获模式
/// </summary>
/// <param name="wireNum">信号线编号0~511</param>
/// <param name="mode">捕获模式</param>
[HttpPost("SetMode")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetMode(UInt32 wireNum, CaptureMode mode)
{
if (wireNum > 512)
{
return BadRequest($"最多只能建立512位信号线");
}
try
{
var debugger = GetDebugger();
if (debugger == null)
return BadRequest("用户未绑定有效的实验板");
var result = await debugger.SetMode(wireNum, mode);
if (!result.IsSuccessful)
{
logger.Error($"设置捕获模式失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置捕获模式失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "设置捕获模式时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 为每个通道中的每根线设置捕获模式
/// </summary>
/// <param name="config">调试器配置信息,包含所有通道的捕获模式设置</param>
[HttpPost("SetChannelsMode")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetChannelsMode([FromBody] DebuggerConfig config)
{
if (config == null || config.channelConfigs == null)
return BadRequest("配置无效");
try
{
var debugger = GetDebugger();
if (debugger == null)
return BadRequest("用户未绑定有效的实验板");
foreach (var channel in config.channelConfigs)
{
// 检查每个通道的配置
if (channel.wireWidth > 32 ||
channel.wireStartIndex > 32 ||
channel.wireStartIndex + channel.wireWidth > 32)
{
return BadRequest($"通道 {channel.name} 配置错误");
}
for (uint i = 0; i < channel.wireWidth; i++)
{
var result = await debugger.SetMode(channel.wireStartIndex * (channel.parentPort * 32) + i, channel.mode);
if (!result.IsSuccessful)
{
logger.Error($"设置通道 {channel.name} 第 {i} 根线捕获模式失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"设置通道 {channel.name} 第 {i} 根线捕获模式失败");
}
}
}
return Ok(true);
}
catch (Exception ex)
{
logger.Error(ex, "为每个通道中的每根线设置捕获模式时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 启动触发器,开始信号捕获
/// </summary>
[HttpPost("StartTrigger")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> StartTrigger()
{
try
{
var debugger = GetDebugger();
if (debugger == null)
return BadRequest("用户未绑定有效的实验板");
var result = await debugger.StartTrigger();
if (!result.IsSuccessful)
{
logger.Error($"启动触发器失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "启动触发器失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "启动触发器时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 读取触发器状态标志
/// </summary>
[HttpGet("ReadFlag")]
[EnableCors("Users")]
[ProducesResponseType(typeof(byte), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> ReadFlag()
{
try
{
var debugger = GetDebugger();
if (debugger == null)
return BadRequest("用户未绑定有效的实验板");
var result = await debugger.ReadFlag();
if (!result.IsSuccessful)
{
logger.Error($"读取触发器状态标志失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "读取触发器状态标志失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "读取触发器状态标志时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 清除触发器状态标志
/// </summary>
[HttpPost("ClearFlag")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> ClearFlag()
{
try
{
var debugger = GetDebugger();
if (debugger == null)
return BadRequest("用户未绑定有效的实验板");
var result = await debugger.ClearFlag();
if (!result.IsSuccessful)
{
logger.Error($"清除触发器状态标志失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "清除触发器状态标志失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "清除触发器状态标志时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 读取捕获数据(等待触发完成后返回各通道采样数据)
/// </summary>
/// <param name="config">调试器配置信息,包含采样深度、端口数、通道配置等</param>
/// <param name="cancellationToken">取消操作的令牌</param>
[HttpPost("ReadData")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ChannelCaptureData[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> ReadData([FromBody] DebuggerConfig config, CancellationToken cancellationToken)
{
// 检查每个通道的配置
foreach (var channel in config.channelConfigs)
{
if (channel.wireWidth > 32 ||
channel.wireStartIndex > 32 ||
channel.wireStartIndex + channel.wireWidth > 32)
{
return BadRequest($"通道 {channel.name} 配置错误");
}
}
try
{
var debugger = GetDebugger();
if (debugger == null)
return BadRequest("用户未绑定有效的实验板");
// 等待捕获标志位
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var flagResult = await debugger.ReadFlag();
if (!flagResult.IsSuccessful)
{
logger.Error($"读取捕获标志失败: {flagResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "读取捕获标志失败");
}
if (flagResult.Value == 1)
{
var clearResult = await debugger.ClearFlag();
if (!clearResult.IsSuccessful)
{
logger.Error($"清除捕获标志失败: {clearResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "清除捕获标志失败");
}
break;
}
await Task.Delay(500, cancellationToken);
}
var dataResult = await debugger.ReadData(config.totalPortNum);
if (!dataResult.IsSuccessful)
{
logger.Error($"读取捕获数据失败: {dataResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "读取捕获数据失败");
}
var freshResult = await debugger.Refresh();
if (!freshResult.IsSuccessful)
{
logger.Error($"刷新调试器状态失败: {freshResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "刷新调试器状态失败");
}
var rawData = dataResult.Value;
// logger.Debug($"rawData: {BitConverter.ToString(rawData)}");
int depth = (int)config.captureDepth;
int portDataLen = 4 * depth;
int portNum = (int)config.totalPortNum;
var channelDataList = new List<ChannelCaptureData>();
foreach (var channel in config.channelConfigs)
{
int port = (int)channel.parentPort;
int wireStart = (int)channel.wireStartIndex;
int wireWidth = (int)channel.wireWidth;
// 每个port的数据长度
int portOffset = port * portDataLen;
var channelUintArr = new UInt32[depth];
for (int i = 0; i < depth; i++)
{
// 取出该port的第i个采样点的4字节
int sampleOffset = portOffset + i * 4;
if (sampleOffset + 4 > rawData.Length)
{
logger.Error($"数据越界: port {port}, sample {i}");
return StatusCode(StatusCodes.Status500InternalServerError, "数据越界");
}
var sampleBytes = rawData[sampleOffset..(sampleOffset + 4)];
UInt32 sample = Common.Number.BytesToUInt32(sampleBytes, true).Value;
// 提取wireWidth位
UInt32 mask = (wireWidth == 32) ? 0xFFFFFFFF : ((1u << wireWidth) - 1u);
channelUintArr[i] = (sample >> wireStart) & mask;
}
var channelBytes = new byte[4 * depth];
Buffer.BlockCopy(channelUintArr, 0, channelBytes, 0, channelBytes.Length);
// logger.Debug($"{channel.name} HexData: {BitConverter.ToString(channelBytes)}");
var base64 = Convert.ToBase64String(channelBytes);
channelDataList.Add(new ChannelCaptureData { name = channel.name, data = base64 });
}
return Ok(channelDataList.ToArray());
}
catch (OperationCanceledException)
{
logger.Info("读取捕获数据请求被取消");
return StatusCode(StatusCodes.Status499ClientClosedRequest, "客户端已取消请求");
}
catch (Exception ex)
{
logger.Error(ex, "读取捕获数据时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 刷新调试器状态(重置采集状态等)
/// </summary>
[HttpPost("Refresh")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> Refresh()
{
try
{
var debugger = GetDebugger();
if (debugger == null)
return BadRequest("用户未绑定有效的实验板");
var result = await debugger.Refresh();
if (!result.IsSuccessful)
{
logger.Error($"刷新调试器状态失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "刷新调试器状态失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "刷新调试器状态时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 表示单个信号通道的配置信息
/// </summary>
public class ChannelConfig
{
/// <summary>
/// 通道名称
/// </summary>
required public string name;
/// <summary>
/// 通道显示颜色(如前端波形显示用)
/// </summary>
required public string color;
/// <summary>
/// 通道信号线宽度(位数)
/// </summary>
required public UInt32 wireWidth;
/// <summary>
/// 信号线在父端口中的起始索引bit
/// </summary>
required public UInt32 wireStartIndex;
/// <summary>
/// 父端口编号
/// </summary>
required public UInt32 parentPort;
/// <summary>
/// 捕获模式(如上升沿、下降沿等)
/// </summary>
required public CaptureMode mode;
}
/// <summary>
/// 调试器整体配置信息
/// </summary>
public class DebuggerConfig
{
/// <summary>
/// 时钟频率
/// </summary>
required public UInt32 clkFreq;
/// <summary>
/// 总端口数量
/// </summary>
required public UInt32 totalPortNum;
/// <summary>
/// 捕获深度(采样点数)
/// </summary>
required public UInt32 captureDepth;
/// <summary>
/// 触发器数量
/// </summary>
required public UInt32 triggerNum;
/// <summary>
/// 所有信号通道的配置信息
/// </summary>
required public ChannelConfig[] channelConfigs;
}
/// <summary>
/// 单个通道的捕获数据
/// </summary>
public class ChannelCaptureData
{
/// <summary>
/// 通道名称
/// </summary>
required public string name;
/// <summary>
/// 通道捕获到的数据Base64编码的UInt32数组
/// </summary>
required public string data;
}
}

View File

@@ -0,0 +1,518 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using DotNext;
using Database;
namespace server.Controllers;
/// <summary>
/// 实验控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ExamController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly ExamManager _examManager = new();
private readonly ResourceManager _resourceManager = new();
private readonly UserManager _userManager = new();
/// <summary>
/// 获取所有实验列表
/// </summary>
/// <returns>实验列表</returns>
[Authorize]
[HttpGet("list")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ExamInfo[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetExamList()
{
try
{
var exams = _examManager.GetAllExams();
var examInfos = exams.Select(exam => new ExamInfo(exam)).ToArray();
logger.Info($"成功获取实验列表,共 {examInfos.Length} 个实验");
return Ok(examInfos);
}
catch (Exception ex)
{
logger.Error($"获取实验列表时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据实验ID获取实验详细信息
/// </summary>
/// <param name="examId">实验ID</param>
/// <returns>实验详细信息</returns>
[Authorize]
[HttpGet("{examId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ExamInfo), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetExam(string examId)
{
if (string.IsNullOrWhiteSpace(examId))
return BadRequest("实验ID不能为空");
try
{
var result = _examManager.GetExamByID(examId);
if (!result.IsSuccessful)
{
logger.Error($"获取实验时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {result.Error.Message}");
}
if (!result.Value.HasValue)
{
logger.Warn($"实验不存在: {examId}");
return NotFound($"实验 {examId} 不存在");
}
var exam = result.Value.Value;
var examInfo = new ExamInfo(exam);
logger.Info($"成功获取实验信息: {examId}");
return Ok(examInfo);
}
catch (Exception ex)
{
logger.Error($"获取实验 {examId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {ex.Message}");
}
}
/// <summary>
/// 创建新实验
/// </summary>
/// <param name="request">创建实验请求</param>
/// <returns>创建结果</returns>
[Authorize("Admin")]
[HttpPost("create")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult CreateExam([FromBody] ExamDto request)
{
if (string.IsNullOrWhiteSpace(request.ID) || string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Description))
return BadRequest("实验ID、名称和描述不能为空");
try
{
var result = _examManager.CreateExam(request.ID, request.Name, request.Description, request.Tags, request.Difficulty, request.IsVisibleToUsers);
if (!result.IsSuccessful)
{
if (result.Error.Message.Contains("已存在"))
return Conflict(result.Error.Message);
logger.Error($"创建实验时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {result.Error.Message}");
}
var exam = result.Value;
var examInfo = new ExamInfo(exam);
logger.Info($"成功创建实验: {request.ID}");
return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo);
}
catch (Exception ex)
{
logger.Error($"创建实验 {request.ID} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}");
}
}
/// <summary>
/// 更新实验信息
/// </summary>
/// <param name="request">更新实验请求</param>
/// <returns>更新结果</returns>
[Authorize("Admin")]
[HttpPost("update")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ExamInfo), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult UpdateExam([FromBody] ExamDto request)
{
var examId = request.ID;
try
{
// 首先检查实验是否存在
var existingExamResult = _examManager.GetExamByID(examId);
if (!existingExamResult.IsSuccessful)
{
logger.Error($"检查实验是否存在时出错: {existingExamResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"检查实验失败: {existingExamResult.Error.Message}");
}
if (!existingExamResult.Value.HasValue)
{
logger.Warn($"要更新的实验不存在: {examId}");
return NotFound($"实验 {examId} 不存在");
}
// 执行更新
var updateResult = _examManager.UpdateExam(
examId,
request.Name,
request.Description,
request.Tags,
request.Difficulty,
request.IsVisibleToUsers
);
if (!updateResult.IsSuccessful)
{
logger.Error($"更新实验时出错: {updateResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"更新实验失败: {updateResult.Error.Message}");
}
// 获取更新后的实验信息并返回
var updatedExamResult = _examManager.GetExamByID(examId);
if (!updatedExamResult.IsSuccessful || !updatedExamResult.Value.HasValue)
{
logger.Error($"获取更新后的实验信息失败: {examId}");
return StatusCode(StatusCodes.Status500InternalServerError, "更新成功但获取更新后信息失败");
}
var updatedExam = updatedExamResult.Value.Value;
var examInfo = new ExamInfo(updatedExam);
logger.Info($"成功更新实验: {examId},更新记录数: {updateResult.Value}");
return Ok(examInfo);
}
catch (Exception ex)
{
logger.Error($"更新实验 {examId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"更新实验失败: {ex.Message}");
}
}
/// <summary>
/// 提交作业
/// </summary>
/// <param name="examId">实验ID</param>
/// <param name="file">提交的文件</param>
/// <returns>提交结果</returns>
[Authorize]
[HttpPost("commit/{examId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> Commit(string examId, IFormFile file)
{
if (string.IsNullOrWhiteSpace(examId))
return BadRequest("实验ID不能为空");
if (file == null || file.Length == 0)
return BadRequest("文件不能为空");
try
{
// 获取当前用户信息
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = _userManager.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 检查实验是否存在
var examResult = _examManager.GetExamByID(examId);
if (!examResult.IsSuccessful)
{
logger.Error($"检查实验是否存在时出错: {examResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"检查实验失败: {examResult.Error.Message}");
}
if (!examResult.Value.HasValue)
{
logger.Warn($"实验不存在: {examId}");
return NotFound($"实验 {examId} 不存在");
}
// 读取文件内容
byte[] fileData;
using (var memoryStream = new MemoryStream())
{
await file.CopyToAsync(memoryStream);
fileData = memoryStream.ToArray();
}
// 提交作业
var commitResult = _resourceManager.AddResource(
user.ID, ResourceTypes.Compression, ResourcePurpose.Homework,
file.FileName, fileData, examId);
if (!commitResult.IsSuccessful)
{
logger.Error($"提交作业时出错: {commitResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"提交作业失败: {commitResult.Error.Message}");
}
var commit = new ResourceInfo(commitResult.Value);
logger.Info($"用户 {userName} 成功提交实验 {examId} 的作业Commit ID: {commit.ID}");
return CreatedAtAction(nameof(GetCommitsByExamId), new { examId = examId }, commit);
}
catch (Exception ex)
{
logger.Error($"提交实验 {examId} 作业时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"提交作业失败: {ex.Message}");
}
}
/// <summary>
/// 获取用户在指定实验中的提交记录
/// </summary>
/// <param name="examId">实验ID</param>
/// <returns>提交记录列表</returns>
[Authorize]
[HttpGet("commits/{examId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetCommitsByExamId(string examId)
{
if (string.IsNullOrWhiteSpace(examId))
return BadRequest("实验ID不能为空");
try
{
// 获取当前用户信息
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = _userManager.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 检查实验是否存在
var examResult = _examManager.GetExamByID(examId);
if (!examResult.IsSuccessful)
{
logger.Error($"检查实验是否存在时出错: {examResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"检查实验失败: {examResult.Error.Message}");
}
if (!examResult.Value.HasValue)
{
logger.Warn($"实验不存在: {examId}");
return NotFound($"实验 {examId} 不存在");
}
// 获取用户的提交记录
var commitsResult = _resourceManager.GetResourceListByType(
ResourceTypes.Compression, ResourcePurpose.Homework, examId);
if (!commitsResult.IsSuccessful)
{
logger.Error($"获取提交记录时出错: {commitsResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取提交记录失败: {commitsResult.Error.Message}");
}
var commits = commitsResult.Value.Select(x => new ResourceInfo(x)).ToArray();
logger.Info($"成功获取用户 {userName} 在实验 {examId} 中的提交记录,共 {commits.Length} 条");
return Ok(commits);
}
catch (Exception ex)
{
logger.Error($"获取实验 {examId} 提交记录时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取提交记录失败: {ex.Message}");
}
}
/// <summary>
/// 删除提交记录
/// </summary>
/// <param name="commitId">提交记录ID</param>
/// <returns>删除结果</returns>
[Authorize]
[HttpDelete("commit/{commitId}")]
[EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult DeleteCommit(Guid commitId)
{
try
{
// 获取当前用户信息
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = _userManager.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 检查是否是管理员
var isAdmin = user.Permission == UserPermission.Admin;
// 如果不是管理员,检查提交记录是否属于当前用户
if (!isAdmin)
{
var commitResult = _resourceManager.GetResourceById(commitId);
if (!commitResult.HasValue)
{
logger.Warn($"提交记录不存在: {commitId}");
return NotFound($"提交记录 {commitId} 不存在");
}
var commit = commitResult.Value;
if (commit.UserID != user.ID)
{
logger.Warn($"用户 {userName} 尝试删除不属于自己的提交记录: {commitId}");
return Forbid("您只能删除自己的提交记录");
}
}
// 执行删除
var deleteResult = _resourceManager.DeleteResource(commitId);
if (!deleteResult)
{
logger.Warn($"提交记录不存在: {commitId}");
return NotFound($"提交记录 {commitId} 不存在");
}
logger.Info($"用户 {userName} 成功删除提交记录: {commitId}");
return Ok($"提交记录 {commitId} 已成功删除");
}
catch (Exception ex)
{
logger.Error($"删除提交记录 {commitId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"删除提交记录失败: {ex.Message}");
}
}
}
/// <summary>
/// 实验信息
/// </summary>
public class ExamInfo
{
/// <summary>
/// 实验的唯一标识符
/// </summary>
public string ID { get; set; }
/// <summary>
/// 实验名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 实验描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 实验创建时间
/// </summary>
public DateTime CreatedTime { get; set; }
/// <summary>
/// 实验最后更新时间
/// </summary>
public DateTime UpdatedTime { get; set; }
/// <summary>
/// 实验标签
/// </summary>
public string[] Tags { get; set; } = Array.Empty<string>();
/// <summary>
/// 实验难度1-5
/// </summary>
public int Difficulty { get; set; } = 1;
/// <summary>
/// 普通用户是否可见
/// </summary>
public bool IsVisibleToUsers { get; set; } = true;
public ExamInfo(Exam exam)
{
ID = exam.ID;
Name = exam.Name;
Description = exam.Description;
CreatedTime = exam.CreatedTime;
UpdatedTime = exam.UpdatedTime;
Tags = exam.GetTagsList();
Difficulty = exam.Difficulty;
IsVisibleToUsers = exam.IsVisibleToUsers;
}
}
/// <summary>
/// 统一的实验数据传输对象
/// </summary>
public class ExamDto
{
/// <summary>
/// 实验的唯一标识符
/// </summary>
public required string ID { get; set; }
/// <summary>
/// 实验名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 实验描述
/// </summary>
public required string Description { get; set; }
/// <summary>
/// 实验标签
/// </summary>
public string[] Tags { get; set; } = Array.Empty<string>();
/// <summary>
/// 实验难度1-5
/// </summary>
public int Difficulty { get; set; } = 1;
/// <summary>
/// 普通用户是否可见
/// </summary>
public bool IsVisibleToUsers { get; set; } = true;
}

View File

@@ -0,0 +1,90 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using server.Services;
namespace server.Controllers;
[ApiController]
[Route("api/[controller]")]
[EnableCors("Users")]
public class HdmiVideoStreamController : ControllerBase
{
private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly HttpHdmiVideoStreamService _videoStreamService;
private readonly Database.UserManager _userManager = new();
public HdmiVideoStreamController(HttpHdmiVideoStreamService videoStreamService)
{
_videoStreamService = videoStreamService;
}
// 管理员获取所有板子的 endpoints
[HttpGet("AllEndpoints")]
[Authorize("Admin")]
public ActionResult<List<HdmiVideoStreamEndpoint>> GetAllEndpoints()
{
var endpoints = _videoStreamService.GetAllVideoEndpoints();
if (endpoints == null)
return NotFound("No boards found.");
return Ok(endpoints);
}
// 用户获取自己板子的 endpoint
[HttpGet("MyEndpoint")]
[Authorize]
public ActionResult<HdmiVideoStreamEndpoint> GetMyEndpoint()
{
var userName = User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrEmpty(userName))
return Unauthorized("User name not found in claims.");
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
return NotFound("User not found.");
var user = userRet.Value.Value;
var boardId = user.BoardID;
if (boardId == Guid.Empty)
return NotFound("No board bound to this user.");
var boardRet = _userManager.GetBoardByID(boardId);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
return NotFound("Board not found.");
var endpoint = _videoStreamService.GetVideoEndpoint(boardId.ToString());
return Ok(endpoint);
}
// 禁用指定板子的 HDMI 传输
[HttpPost("DisableHdmiTransmission")]
[Authorize]
public async Task<IActionResult> DisableHdmiTransmission()
{
var userName = User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrEmpty(userName))
return Unauthorized("User name not found in claims.");
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
return NotFound("User not found.");
var user = userRet.Value.Value;
var boardId = user.BoardID;
if (boardId == Guid.Empty)
return NotFound("No board bound to this user.");
try
{
await _videoStreamService.DisableHdmiTransmissionAsync(boardId.ToString());
return Ok($"HDMI transmission for board {boardId} disabled.");
}
catch (Exception ex)
{
logger.Error(ex, $"Failed to disable HDMI transmission for board {boardId}");
return StatusCode(500, $"Error disabling HDMI transmission: {ex.Message}");
}
}
}

View File

@@ -1,164 +1,214 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Database;
using server.Services;
namespace server.Controllers;
/// <summary>
/// Jtag API
/// JTAG 控制器 - 提供 JTAG 相关的 API 操作
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Authorize] // 添加用户认证要求
public class JtagController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly ProgressTracker _tracker = MsgBus.ProgressTracker;
private readonly UserManager _userManager = new();
private readonly ResourceManager _resourceManager = new();
private const string BITSTREAM_PATH = "bitstream/Jtag";
/// <summary>
/// 页面
/// 控制器首页信息
/// </summary>
/// <returns>控制器描述信息</returns>
[HttpGet]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
public string Index()
{
logger.Info($"User {User.Identity?.Name} accessed Jtag controller index");
return "This is Jtag Controller";
}
/// <summary>
/// 获取Jtag ID Code
/// 获取 JTAG 设备的 ID Code
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
/// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param>
/// <returns>设备的 ID Code</returns>
[HttpGet("GetDeviceIDCode")]
[EnableCors("Users")]
[ProducesResponseType(typeof(uint), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> GetDeviceIDCode(string address, int port)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.ReadIDCode();
if (ret.IsSuccessful)
{
logger.Info($"Get device {address} ID code: 0x{ret.Value:X4}");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// 获取状态寄存器
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
[HttpGet("ReadStatusReg")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> ReadStatusReg(string address, int port)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.ReadStatusReg();
if (ret.IsSuccessful)
{
var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
var decodeValue = new JtagClient.JtagStatusReg(ret.Value);
logger.Info($"Read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
return TypedResults.Ok(new
{
original = ret.Value,
binaryValue,
decodeValue,
});
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// 上传比特流文件
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="file">比特流文件</param>
[HttpPost("UploadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
{
if (file == null || file.Length == 0)
return TypedResults.BadRequest("未选择文件");
// 生成安全的文件名(避免路径遍历攻击)
var fileName = Path.GetRandomFileName();
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
// 如果存在文件,则删除原文件再上传
if (Directory.Exists(uploadsFolder))
{
Directory.Delete(uploadsFolder, true);
}
Directory.CreateDirectory(uploadsFolder);
var filePath = Path.Combine(uploadsFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
logger.Info($"Device {address} Upload Bitstream Successfully");
return TypedResults.Ok(true);
}
/// <summary>
/// 通过Jtag下载比特流文件
/// </summary>
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
[HttpPost("DownloadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> DownloadBitstream(string address, int port)
{
// 检查文件
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
if (!Directory.Exists(fileDir))
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
logger.Info($"User {User.Identity?.Name} requesting device ID code from {address}:{port}");
try
{
// 读取文件
var filePath = Directory.GetFiles(fileDir)[0];
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.ReadIDCode();
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
if (ret.IsSuccessful)
{
if (fileStream is null || fileStream.Length <= 0)
return TypedResults.BadRequest("Wrong bitstream, Please upload it again");
logger.Info($"User {User.Identity?.Name} successfully got device {address} ID code: 0x{ret.Value:X8}");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error($"User {User.Identity?.Name} failed to get device {address} ID code: {ret.Error}");
return TypedResults.InternalServerError(ret.Error);
}
}
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while getting device {address} ID code");
return TypedResults.InternalServerError(ex);
}
}
/// <summary>
/// 读取 JTAG 设备的状态寄存器
/// </summary>
/// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param>
/// <returns>状态寄存器的原始值、二进制表示和解码值</returns>
[HttpGet("ReadStatusReg")]
[EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> ReadStatusReg(string address, int port)
{
logger.Info($"User {User.Identity?.Name} requesting status register from {address}:{port}");
try
{
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.ReadStatusReg();
if (ret.IsSuccessful)
{
var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
var decodeValue = new Peripherals.JtagClient.JtagStatusReg(ret.Value);
logger.Info($"User {User.Identity?.Name} successfully read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
return TypedResults.Ok(new
{
original = ret.Value,
binaryValue,
decodeValue,
});
}
else
{
logger.Error($"User {User.Identity?.Name} failed to read device {address} status register: {ret.Error}");
return TypedResults.InternalServerError(ret.Error);
}
}
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while reading device {address} status register");
return TypedResults.InternalServerError(ex);
}
}
/// <summary>
/// 通过 JTAG 下载比特流文件到 FPGA 设备
/// </summary>
/// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param>
/// <param name="bitstreamId">比特流ID</param>
/// <param name="cancelToken">取消令牌</param>
/// <returns>进度跟踪TaskID</returns>
[HttpPost("DownloadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public IResult DownloadBitstream(string address, int port, Guid bitstreamId, CancellationToken cancelToken)
{
logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
try
{
// 获取当前用户名
var username = User.Identity?.Name;
if (string.IsNullOrEmpty(username))
{
logger.Warn("Anonymous user attempted to download bitstream");
return TypedResults.Unauthorized();
}
// 从数据库获取用户信息
var userResult = _userManager.GetUserByName(username);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
{
logger.Error($"User {username} not found in database");
return TypedResults.BadRequest("用户不存在");
}
// 从数据库获取比特流
var user = userResult.Value.Value;
var resourceRet = _resourceManager.GetResourceById(bitstreamId);
if (!resourceRet.HasValue)
{
logger.Warn($"User {username} attempted to download non-existent bitstream ID: {bitstreamId}");
return TypedResults.BadRequest("比特流不存在");
}
// 处理比特流数据
var resource = resourceRet.Value;
var bitstreamRet = _resourceManager.ReadBytesFromPath(resource.Path);
if (!bitstreamRet.IsSuccessful)
{
logger.Error($"User {username} failed to read bitstream file: {bitstreamRet.Error}");
return TypedResults.InternalServerError($"比特流读取失败: {bitstreamRet.Error?.Message}");
}
var fileBytes = bitstreamRet.Value;
if (fileBytes == null || fileBytes.Length == 0)
{
logger.Warn($"User {username} found empty bitstream data for ID: {bitstreamId}");
return TypedResults.BadRequest("比特流数据为空,请重新上传");
}
logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes");
// 定义进度跟踪
var taskId = _tracker.CreateTask(8000);
_tracker.AdvanceProgress(taskId, 10);
_ = Task.Run(async () =>
{
// 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = new byte[32 * 1024];
long totalBytesRead = 0;
long totalBytesProcessed = 0;
// 使用异步流读取文件
using (var memoryStream = new MemoryStream())
// 使用内存流处理文件
using (var inputStream = new MemoryStream(fileBytes))
using (var outputStream = new MemoryStream())
{
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
// 反转 32bits
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful)
return TypedResults.InternalServerError(retBuffer.Error);
{
logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
_tracker.FailProgress(taskId,
$"User {username} failed to reverse bytes: {retBuffer.Error}");
return;
}
revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
@@ -166,113 +216,159 @@ public class JtagController : ControllerBase
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead;
await outputStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesProcessed += bytesRead;
}
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
var fileBytes = memoryStream.ToArray();
// 获取处理后的数据
var processedBytes = outputStream.ToArray();
logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}");
_tracker.AdvanceProgress(taskId, 20);
// 下载比特流
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(fileBytes);
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(processedBytes, taskId);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} dowload bitstream successfully");
return TypedResults.Ok(ret.Value);
logger.Info($"User {username} successfully downloaded bitstream '{resource.ResourceName}' to device {address}");
_tracker.CompleteProgress(taskId);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
_tracker.FailProgress(taskId,
$"User {username} failed to download bitstream to device {address}: {ret.Error}");
}
}
});
}
return TypedResults.Ok(taskId);
}
catch (Exception error)
catch (Exception ex)
{
return TypedResults.InternalServerError(error);
}
finally
{
logger.Error(ex, $"User encountered exception while downloading bitstream to device {address}");
return TypedResults.InternalServerError(ex);
}
}
/// <summary>
/// [TODO:description]
/// 执行边界扫描,获取所有端口状态
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
/// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param>
/// <returns>边界扫描结果</returns>
[HttpPost("BoundaryScanAllPorts")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> BoundaryScanAllPorts(string address, int port)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.BoundaryScan();
if (!ret.IsSuccessful)
{
if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error);
else return TypedResults.InternalServerError(ret.Error);
}
logger.Info($"User {User.Identity?.Name} initiating boundary scan for all ports on device {address}:{port}");
return TypedResults.Ok(ret.Value);
try
{
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.BoundaryScan();
if (!ret.IsSuccessful)
{
logger.Error($"User {User.Identity?.Name} boundary scan failed for device {address}: {ret.Error}");
if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error);
else
return TypedResults.InternalServerError(ret.Error);
}
logger.Info($"User {User.Identity?.Name} successfully completed boundary scan for device {address}");
return TypedResults.Ok(ret.Value);
}
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} encountered exception during boundary scan for device {address}");
return TypedResults.InternalServerError(ex);
}
}
/// <summary>
/// [TODO:description]
/// 执行逻辑端口边界扫描
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
/// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param>
/// <returns>逻辑端口状态字典</returns>
[HttpPost("BoundaryScanLogicalPorts")]
[EnableCors("Users")]
[ProducesResponseType(typeof(Dictionary<string, bool>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> BoundaryScanLogicalPorts(string address, int port)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.BoundaryScanLogicalPorts();
if (!ret.IsSuccessful)
{
if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error);
else return TypedResults.InternalServerError(ret.Error);
}
logger.Info($"User {User.Identity?.Name} initiating logical ports boundary scan on device {address}:{port}");
return TypedResults.Ok(ret.Value);
try
{
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.BoundaryScanLogicalPorts();
if (!ret.IsSuccessful)
{
logger.Error($"User {User.Identity?.Name} logical ports boundary scan failed for device {address}: {ret.Error}");
if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error);
else
return TypedResults.InternalServerError(ret.Error);
}
logger.Info($"User {User.Identity?.Name} successfully completed logical ports boundary scan for device {address}, found {ret.Value?.Count} ports");
return TypedResults.Ok(ret.Value);
}
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} encountered exception during logical ports boundary scan for device {address}");
return TypedResults.InternalServerError(ex);
}
}
/// <summary>
/// [TODO:description]
/// 设置 JTAG 时钟速度
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="speed">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
/// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param>
/// <param name="speed">时钟速度 (Hz)</param>
/// <returns>设置结果</returns>
[HttpPost("SetSpeed")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> SetSpeed(string address, int port, UInt32 speed)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.SetSpeed(speed);
if (!ret.IsSuccessful)
{
if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error);
else return TypedResults.InternalServerError(ret.Error);
}
logger.Info($"User {User.Identity?.Name} setting JTAG speed to {speed} Hz for device {address}:{port}");
return TypedResults.Ok(ret.Value);
try
{
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.SetSpeed(speed);
if (!ret.IsSuccessful)
{
logger.Error($"User {User.Identity?.Name} failed to set speed for device {address}: {ret.Error}");
if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error);
else
return TypedResults.InternalServerError(ret.Error);
}
logger.Info($"User {User.Identity?.Name} successfully set JTAG speed to {speed} Hz for device {address}");
return TypedResults.Ok(ret.Value);
}
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while setting speed for device {address}");
return TypedResults.InternalServerError(ex);
}
}
}

View File

@@ -0,0 +1,427 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Peripherals.LogicAnalyzerClient;
namespace server.Controllers;
/// <summary>
/// 逻辑分析仪控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class LogicAnalyzerController : ControllerBase
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly Database.UserManager _userManager = new();
/// <summary>
/// 获取逻辑分析仪实例
/// </summary>
private Analyzer? GetAnalyzer()
{
try
{
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return null;
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
return null;
var user = userRet.Value.Value;
if (user.BoardID == Guid.Empty)
return null;
var boardRet = _userManager.GetBoardByID(user.BoardID);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
return null;
var board = boardRet.Value.Value;
return new Analyzer(board.IpAddr, board.Port, 11);
}
catch (Exception ex)
{
logger.Error(ex, "获取逻辑分析仪实例时发生异常");
return null;
}
}
/// <summary>
/// 设置捕获模式
/// </summary>
/// <param name="captureOn">是否开始捕获</param>
/// <param name="force">是否强制捕获</param>
/// <returns>操作结果</returns>
[HttpPost("SetCaptureMode")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetCaptureMode(bool captureOn, bool force = false)
{
try
{
var analyzer = GetAnalyzer();
if (analyzer == null)
return BadRequest("用户未绑定有效的实验板");
var result = await analyzer.SetCaptureMode(captureOn, force);
if (!result.IsSuccessful)
{
logger.Error($"设置捕获模式失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置捕获模式失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "设置捕获模式时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 读取捕获状态
/// </summary>
/// <returns>捕获状态</returns>
[HttpGet("GetCaptureStatus")]
[EnableCors("Users")]
[ProducesResponseType(typeof(CaptureStatus), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetCaptureStatus()
{
try
{
var analyzer = GetAnalyzer();
if (analyzer == null)
return BadRequest("用户未绑定有效的实验板");
var result = await analyzer.ReadCaptureStatus();
if (!result.IsSuccessful)
{
logger.Error($"读取捕获状态失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "读取捕获状态失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "读取捕获状态时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 设置全局触发模式
/// </summary>
/// <param name="mode">全局触发模式</param>
/// <returns>操作结果</returns>
[HttpPost("SetGlobalTrigMode")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetGlobalTrigMode(GlobalCaptureMode mode)
{
try
{
var analyzer = GetAnalyzer();
if (analyzer == null)
return BadRequest("用户未绑定有效的实验板");
var result = await analyzer.SetGlobalTrigMode(mode);
if (!result.IsSuccessful)
{
logger.Error($"设置全局触发模式失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置全局触发模式失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "设置全局触发模式时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 设置信号触发模式
/// </summary>
/// <param name="signalIndex">信号索引 (0-7)</param>
/// <param name="op">操作符</param>
/// <param name="val">信号值</param>
/// <returns>操作结果</returns>
[HttpPost("SetSignalTrigMode")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetSignalTrigMode(int signalIndex, SignalOperator op, SignalValue val)
{
try
{
if (signalIndex < 0 || signalIndex > 31)
return BadRequest("信号索引必须在0-31之间");
var analyzer = GetAnalyzer();
if (analyzer == null)
return BadRequest("用户未绑定有效的实验板");
var result = await analyzer.SetSignalTrigMode(signalIndex, op, val);
if (!result.IsSuccessful)
{
logger.Error($"设置信号触发模式失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置信号触发模式失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "设置信号触发模式时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 设置深度、预采样深度、有效通道
/// </summary>
/// <param name="capture_length">深度</param>
/// <param name="pre_capture_length">预采样深度</param>
/// <param name="channel_div">有效通道(0-[1],1-[2],2-[4],3-[8],4-[16],5-[32])</param>
/// <param name="clock_div">采样时钟分频系数</param>
/// <returns>操作结果</returns>
[HttpPost("SetCaptureParams")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetCaptureParams(int capture_length, int pre_capture_length, AnalyzerChannelDiv channel_div, AnalyzerClockDiv clock_div)
{
try
{
//DDR深度为 32'h01000000 - 32'h0FFFFFFF
if (capture_length < 0 || capture_length > 0x10000000 - 0x01000000)
return BadRequest("采样深度设置错误");
if (pre_capture_length < 0 || pre_capture_length >= capture_length)
return BadRequest("预采样深度必须小于捕获深度");
var analyzer = GetAnalyzer();
if (analyzer == null)
return BadRequest("用户未绑定有效的实验板");
var result = await analyzer.SetCaptureParams(capture_length, pre_capture_length, channel_div, clock_div);
if (!result.IsSuccessful)
{
logger.Error($"设置深度、预采样深度、有效通道、时钟分频失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置深度、预采样深度、有效通道、时钟分频失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "设置深度、预采样深度、有效通道、时钟分频失败时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 批量配置捕获参数
/// </summary>
/// <param name="config">捕获配置</param>
/// <returns>操作结果</returns>
[HttpPost("ConfigureCapture")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> ConfigureCapture([FromBody] CaptureConfig config)
{
try
{
if (config == null)
return BadRequest("配置参数不能为空");
var analyzer = GetAnalyzer();
if (analyzer == null)
return BadRequest("用户未绑定有效的实验板");
// 设置全局触发模式
var globalResult = await analyzer.SetGlobalTrigMode(config.GlobalMode);
if (!globalResult.IsSuccessful)
{
logger.Error($"设置全局触发模式失败: {globalResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置全局触发模式失败");
}
// 设置信号触发模式
foreach (var signalConfig in config.SignalConfigs)
{
if (signalConfig.SignalIndex < 0 || signalConfig.SignalIndex > 31)
return BadRequest($"信号索引{signalConfig.SignalIndex}超出范围0-31");
var signalResult = await analyzer.SetSignalTrigMode(
signalConfig.SignalIndex, signalConfig.Operator, signalConfig.Value);
if (!signalResult.IsSuccessful)
{
logger.Error($"设置信号{signalConfig.SignalIndex}触发模式失败: {signalResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError,
$"设置信号{signalConfig.SignalIndex}触发模式失败");
}
}
// 设置深度、预采样深度、有效通道
var paramsResult = await analyzer.SetCaptureParams(
config.CaptureLength, config.PreCaptureLength, config.ChannelDiv, config.ClockDiv);
if (!paramsResult.IsSuccessful)
{
logger.Error($"设置深度、预采样深度、有效通道失败: {paramsResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置深度、预采样深度、有效通道失败");
}
return Ok(true);
}
catch (Exception ex)
{
logger.Error(ex, "配置捕获参数时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 强制捕获
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("ForceCapture")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> ForceCapture()
{
try
{
var analyzer = GetAnalyzer();
if (analyzer == null)
return BadRequest("用户未绑定有效的实验板");
var result = await analyzer.SetCaptureMode(true, true);
if (!result.IsSuccessful)
{
logger.Error($"强制捕获失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "强制捕获失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "强制捕获时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 读取捕获数据
/// </summary>
/// <returns>捕获的波形数据Base64编码</returns>
[HttpGet("GetCaptureData")]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetCaptureData(int capture_length = 2048 * 32)
{
try
{
var analyzer = GetAnalyzer();
if (analyzer == null)
return BadRequest("用户未绑定有效的实验板");
var result = await analyzer.ReadCaptureData(capture_length);
if (!result.IsSuccessful)
{
logger.Error($"读取捕获数据失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "读取捕获数据失败");
}
// 将二进制数据编码为Base64字符串返回
var base64Data = Convert.ToBase64String(result.Value);
return Ok(base64Data);
}
catch (Exception ex)
{
logger.Error(ex, "读取捕获数据时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 信号触发配置
/// </summary>
public class SignalTriggerConfig
{
/// <summary>
/// 信号索引 (0-7)
/// </summary>
public int SignalIndex { get; set; }
/// <summary>
/// 操作符
/// </summary>
public SignalOperator Operator { get; set; }
/// <summary>
/// 信号值
/// </summary>
public SignalValue Value { get; set; }
}
/// <summary>
/// 捕获配置
/// </summary>
public class CaptureConfig
{
/// <summary>
/// 全局触发模式
/// </summary>
public GlobalCaptureMode GlobalMode { get; set; }
/// <summary>
/// 捕获深度
/// </summary>
public int CaptureLength { get; set; } = 2048 * 32;
/// <summary>
/// 预采样深度
/// </summary>
public int PreCaptureLength { get; set; } = 2048;
/// <summary>
/// 有效通道
/// </summary>
public AnalyzerChannelDiv ChannelDiv { get; set; } = AnalyzerChannelDiv.EIGHT;
/// <summary>
/// 时钟分频系数
/// </summary>
public AnalyzerClockDiv ClockDiv { get; set; } = AnalyzerClockDiv.DIV1;
/// <summary>
/// 信号触发配置列表
/// </summary>
public SignalTriggerConfig[] SignalConfigs { get; set; } = Array.Empty<SignalTriggerConfig>();
}
}

View File

@@ -0,0 +1,776 @@
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Peripherals.NetConfigClient;
namespace server.Controllers;
/// <summary>
/// 网络配置控制器(仅管理员权限)
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Authorize("Admin")]
public class NetConfigController : ControllerBase
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
// 固定的实验板IP,端口,MAC地址
private const string BOARD_IP = "169.254.109.0";
private const int BOARD_PORT = 1234;
// 本机网络信息
private readonly IPAddress _localIP = IPAddress.Any;
private readonly byte[] _localMAC = new byte[6];
private readonly string _localIPString;
private readonly string _localMACString;
private readonly string _localInterface;
public NetConfigController()
{
// 初始化本机IP地址
_localIP = GetLocalIPAddress();
_localIPString = _localIP?.ToString() ?? "未知";
// 初始化本机MAC地址
_localMAC = GetLocalMACAddress();
_localMACString = _localMAC != null ? BitConverter.ToString(_localMAC).Replace("-", ":") : "未知";
// 获取本机网络接口名称
_localInterface = GetLocalNetworkInterface();
logger.Info($"NetConfigController 初始化完成 - 本机IP: {_localIPString}, 本机MAC: {_localMACString}, 接口: {_localInterface}");
}
/// <summary>
/// 获取本机IP地址优先选择与实验板同网段的IP
/// </summary>
/// <returns>本机IP地址</returns>
private IPAddress GetLocalIPAddress()
{
try
{
var boardIpSegments = BOARD_IP.Split('.').Take(3).ToArray();
// 优先选择与实验板IP前三段相同的IP
var sameSegmentIP = System.Net.NetworkInformation.NetworkInterface
.GetAllNetworkInterfaces()
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
.SelectMany(nic => nic.GetIPProperties().UnicastAddresses)
.Where(addr => addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
.Select(addr => addr.Address)
.FirstOrDefault(addr =>
{
var segments = addr.ToString().Split('.');
return segments.Length == 4 &&
segments[0] == boardIpSegments[0] &&
segments[1] == boardIpSegments[1] &&
segments[2] == boardIpSegments[2];
});
if (sameSegmentIP != null)
return sameSegmentIP;
// 如果没有找到同网段的IP返回第一个可用的IP
return System.Net.NetworkInformation.NetworkInterface
.GetAllNetworkInterfaces()
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
.SelectMany(nic => nic.GetIPProperties().UnicastAddresses)
.Where(addr => addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
.Select(addr => addr.Address)
.FirstOrDefault() ?? IPAddress.Loopback;
}
catch (Exception ex)
{
logger.Error(ex, "获取本机IP地址失败");
return IPAddress.Loopback;
}
}
/// <summary>
/// 获取本机MAC地址
/// </summary>
/// <returns>本机MAC地址字节数组</returns>
private byte[] GetLocalMACAddress()
{
try
{
return System.Net.NetworkInformation.NetworkInterface
.GetAllNetworkInterfaces()
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
.Select(nic => nic.GetPhysicalAddress()?.GetAddressBytes())
.FirstOrDefault(bytes => bytes != null && bytes.Length == 6) ?? new byte[6];
}
catch (Exception ex)
{
logger.Error(ex, "获取本机MAC地址失败");
return new byte[6];
}
}
/// <summary>
/// 获取本机网络接口名称
/// </summary>
/// <returns>网络接口名称</returns>
private string GetLocalNetworkInterface()
{
return GetLocalIPAddress().ToString();
}
/// <summary>
/// 初始化ARP记录
/// </summary>
/// <returns>是否成功</returns>
private async Task<bool> InitializeArpAsync()
{
try
{
return await ArpClient.UpdateArpEntryAsync(BOARD_IP);
}
catch (Exception ex)
{
logger.Error(ex, "初始化ARP记录失败");
return false;
}
}
/// <summary>
/// 获取主机IP地址
/// </summary>
/// <returns>主机IP地址</returns>
[HttpGet("GetHostIP")]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> GetHostIP()
{
try
{
if (!(await InitializeArpAsync()))
{
throw new Exception("无法配置ARP记录");
}
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
var result = await netConfig.GetHostIP();
if (!result.IsSuccessful)
{
logger.Error($"获取主机IP失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取失败: {result.Error}");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "获取主机IP时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
}
}
/// <summary>
/// 获取板卡IP地址
/// </summary>
/// <returns>板卡IP地址</returns>
[HttpGet("GetBoardIP")]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> GetBoardIP()
{
try
{
if (!(await InitializeArpAsync()))
{
throw new Exception("无法配置ARP记录");
}
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
var result = await netConfig.GetBoardIP();
if (!result.IsSuccessful)
{
logger.Error($"获取板卡IP失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取失败: {result.Error}");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "获取板卡IP时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
}
}
/// <summary>
/// 获取主机MAC地址
/// </summary>
/// <returns>主机MAC地址</returns>
[HttpGet("GetHostMAC")]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> GetHostMAC()
{
try
{
if (!(await InitializeArpAsync()))
{
throw new Exception("无法配置ARP记录");
}
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
var result = await netConfig.GetHostMAC();
if (!result.IsSuccessful)
{
logger.Error($"获取主机MAC地址失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取失败: {result.Error}");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "获取主机MAC地址时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
}
}
/// <summary>
/// 获取板卡MAC地址
/// </summary>
/// <returns>板卡MAC地址</returns>
[HttpGet("GetBoardMAC")]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> GetBoardMAC()
{
try
{
if (!(await InitializeArpAsync()))
{
throw new Exception("无法配置ARP记录");
}
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
var result = await netConfig.GetBoardMAC();
if (!result.IsSuccessful)
{
logger.Error($"获取板卡MAC地址失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取失败: {result.Error}");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "获取板卡MAC地址时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
}
}
/// <summary>
/// 获取所有网络配置信息
/// </summary>
/// <returns>网络配置信息</returns>
[HttpGet("GetNetworkConfig")]
[EnableCors("Users")]
[ProducesResponseType(typeof(NetworkConfigDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> GetNetworkConfig()
{
try
{
if (!(await InitializeArpAsync()))
{
throw new Exception("无法配置ARP记录");
}
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
var hostIPResult = await netConfig.GetHostIP();
var boardIPResult = await netConfig.GetBoardIP();
var hostMACResult = await netConfig.GetHostMAC();
var boardMACResult = await netConfig.GetBoardMAC();
var config = new NetworkConfigDto
{
HostIP = hostIPResult.IsSuccessful ? hostIPResult.Value : "获取失败",
BoardIP = boardIPResult.IsSuccessful ? boardIPResult.Value : "获取失败",
HostMAC = hostMACResult.IsSuccessful ? hostMACResult.Value : "获取失败",
BoardMAC = boardMACResult.IsSuccessful ? boardMACResult.Value : "获取失败"
};
return Ok(config);
}
catch (Exception ex)
{
logger.Error(ex, "获取网络配置信息时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
}
}
/// <summary>
/// 获取本机所有网络接口信息
/// </summary>
/// <returns>网络接口信息列表</returns>
[HttpGet("GetLocalNetworkInterfaces")]
[EnableCors("Users")]
[ProducesResponseType(typeof(List<NetworkInterfaceDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetLocalNetworkInterfaces()
{
try
{
var interfaces = System.Net.NetworkInformation.NetworkInterface
.GetAllNetworkInterfaces()
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
.Select(nic => new NetworkInterfaceDto
{
Name = nic.Name,
Description = nic.Description,
Type = nic.NetworkInterfaceType.ToString(),
Status = nic.OperationalStatus.ToString(),
IPAddresses = nic.GetIPProperties().UnicastAddresses
.Where(addr => addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
.Select(addr => addr.Address.ToString())
.ToList(),
MACAddress = nic.GetPhysicalAddress().ToString()
})
.ToList();
return Ok(interfaces);
}
catch (Exception ex)
{
logger.Error(ex, "获取本机网络接口信息时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
}
}
/// <summary>
/// 设置主机IP地址
/// </summary>
/// <param name="hostIp">主机IP地址</param>
/// <returns>操作结果</returns>
[HttpPost("SetHostIP")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> SetHostIP(string hostIp)
{
if (string.IsNullOrWhiteSpace(hostIp))
return BadRequest("主机IP地址不能为空");
if (!IPAddress.TryParse(hostIp, out var hostIpAddress))
return BadRequest("主机IP地址格式不正确");
try
{
if (!(await InitializeArpAsync()))
{
throw new Exception("无法配置ARP记录");
}
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
var result = await netConfig.SetHostIP(hostIpAddress);
if (!result.IsSuccessful)
{
logger.Error($"设置主机IP失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "设置主机IP时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
}
}
/// <summary>
/// 设置板卡IP地址
/// </summary>
/// <param name="newBoardIp">新的板卡IP地址</param>
/// <returns>操作结果</returns>
[HttpPost("SetBoardIP")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> SetBoardIP(string newBoardIp)
{
if (string.IsNullOrWhiteSpace(newBoardIp))
return BadRequest("新的板卡IP地址不能为空");
if (!IPAddress.TryParse(newBoardIp, out var newIpAddress))
return BadRequest("新的板卡IP地址格式不正确");
try
{
if (!(await InitializeArpAsync()))
{
throw new Exception("无法配置ARP记录");
}
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
var result = await netConfig.SetBoardIP(newIpAddress);
if (!result.IsSuccessful)
{
logger.Error($"设置板卡IP失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "设置板卡IP时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
}
}
/// <summary>
/// 设置板卡MAC地址
/// </summary>
/// <param name="boardMac">板卡MAC地址格式AA:BB:CC:DD:EE:FF</param>
/// <returns>操作结果</returns>
[HttpPost("SetBoardMAC")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> SetBoardMAC(string boardMac)
{
if (string.IsNullOrWhiteSpace(boardMac))
return BadRequest("板卡MAC地址不能为空");
// 解析MAC地址
if (!TryParseMacAddress(boardMac, out var macBytes))
return BadRequest("MAC地址格式不正确请使用格式AA:BB:CC:DD:EE:FF");
try
{
if (!(await InitializeArpAsync()))
{
throw new Exception("无法配置ARP记录");
}
// 创建网络配置客户端
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
var result = await netConfig.SetBoardMAC(macBytes);
if (!result.IsSuccessful)
{
logger.Error($"设置板卡MAC地址失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "设置板卡MAC地址时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
}
}
/// <summary>
/// 设置主机MAC地址
/// </summary>
/// <param name="hostMac">主机MAC地址格式AA:BB:CC:DD:EE:FF</param>
/// <returns>操作结果</returns>
[HttpPost("SetHostMAC")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> SetHostMAC(string hostMac)
{
if (string.IsNullOrWhiteSpace(hostMac))
return BadRequest("主机MAC地址不能为空");
// 解析MAC地址
if (!TryParseMacAddress(hostMac, out var macBytes))
return BadRequest("MAC地址格式不正确请使用格式AA:BB:CC:DD:EE:FF");
try
{
if (!(await InitializeArpAsync()))
{
throw new Exception("无法配置ARP记录");
}
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
var result = await netConfig.SetHostMAC(macBytes);
if (!result.IsSuccessful)
{
logger.Error($"设置主机MAC地址失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "设置主机MAC地址时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
}
}
/// <summary>
/// 自动获取本机IP地址并设置为实验板主机IP
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("UpdateHostIP")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> UpdateHostIP()
{
try
{
if (_localIP == null)
return StatusCode(StatusCodes.Status500InternalServerError, "无法获取本机IP地址");
if (!(await InitializeArpAsync()))
{
throw new Exception("无法配置ARP记录");
}
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
var result = await netConfig.SetHostIP(_localIP);
if (!result.IsSuccessful)
{
logger.Error($"自动设置主机IP失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "自动设置主机IP时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
}
}
/// <summary>
/// 更新主机MAC地址
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("UpdateHostMAC")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> UpdateHostMAC()
{
try
{
if (_localMAC == null || _localMAC.Length != 6)
return StatusCode(StatusCodes.Status500InternalServerError, "无法获取本机MAC地址");
if (!(await InitializeArpAsync()))
{
throw new Exception("无法配置ARP记录");
}
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
var result = await netConfig.SetHostMAC(_localMAC);
if (!result.IsSuccessful)
{
logger.Error($"设置主机MAC地址失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "设置主机MAC地址时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
}
}
/// <summary>
/// 获取本机网络信息
/// </summary>
/// <returns>本机网络信息</returns>
[HttpGet("GetLocalNetworkInfo")]
[EnableCors("Users")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
public IActionResult GetLocalNetworkInfo()
{
return Ok(new
{
LocalIP = _localIPString,
LocalMAC = _localMACString,
LocalInterface = _localInterface
});
}
/// <summary>
/// 解析MAC地址字符串为字节数组
/// </summary>
/// <param name="macAddress">MAC地址字符串</param>
/// <param name="macBytes">解析后的字节数组</param>
/// <returns>是否解析成功</returns>
private static bool TryParseMacAddress(string macAddress, out byte[] macBytes)
{
macBytes = Array.Empty<byte>();
if (string.IsNullOrWhiteSpace(macAddress))
return false;
// 移除可能的分隔符并统一为冒号
var cleanMac = macAddress.Replace("-", ":").Replace(" ", "").ToUpper();
// 验证格式
if (cleanMac.Length != 17 || cleanMac.Count(c => c == ':') != 5)
return false;
var parts = cleanMac.Split(':');
if (parts.Length != 6)
return false;
try
{
macBytes = new byte[6];
for (int i = 0; i < 6; i++)
{
macBytes[i] = Convert.ToByte(parts[i], 16);
}
return true;
}
catch
{
macBytes = Array.Empty<byte>();
return false;
}
}
}
/// <summary>
/// 网络配置数据传输对象
/// </summary>
public class NetworkConfigDto
{
/// <summary>
/// 主机IP地址
/// </summary>
public string? HostIP { get; set; }
/// <summary>
/// 板卡IP地址
/// </summary>
public string? BoardIP { get; set; }
/// <summary>
/// 主机MAC地址
/// </summary>
public string? HostMAC { get; set; }
/// <summary>
/// 板卡MAC地址
/// </summary>
public string? BoardMAC { get; set; }
}
/// <summary>
/// 网络配置操作结果
/// </summary>
public class NetworkConfigResult
{
/// <summary>
/// 主机IP设置结果
/// </summary>
public bool? HostIPResult { get; set; }
/// <summary>
/// 主机IP设置错误信息
/// </summary>
public string? HostIPError { get; set; }
/// <summary>
/// 板卡IP设置结果
/// </summary>
public bool? BoardIPResult { get; set; }
/// <summary>
/// 板卡IP设置错误信息
/// </summary>
public string? BoardIPError { get; set; }
/// <summary>
/// 主机MAC设置结果
/// </summary>
public bool? HostMACResult { get; set; }
/// <summary>
/// 主机MAC设置错误信息
/// </summary>
public string? HostMACError { get; set; }
/// <summary>
/// 板卡MAC设置结果
/// </summary>
public bool? BoardMACResult { get; set; }
/// <summary>
/// 板卡MAC设置错误信息
/// </summary>
public string? BoardMACError { get; set; }
}
/// <summary>
/// 网络接口信息数据传输对象
/// </summary>
public class NetworkInterfaceDto
{
/// <summary>
/// 网络接口名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 网络接口描述
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// 网络接口类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 网络接口状态
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// IP地址列表
/// </summary>
public List<string> IPAddresses { get; set; } = new();
/// <summary>
/// MAC地址
/// </summary>
public string MACAddress { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,413 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Peripherals.OscilloscopeClient;
using server.Hubs;
namespace server.Controllers;
/// <summary>
/// 示波器API控制器 - 普通用户权限
/// </summary>
[ApiController]
[EnableCors("Development")]
[Route("api/[controller]")]
[Authorize]
public class OscilloscopeApiController : ControllerBase
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly Database.UserManager _userManager = new();
/// <summary>
/// 获取示波器实例
/// </summary>
private OscilloscopeCtrl? GetOscilloscope()
{
try
{
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return null;
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
return null;
var user = userRet.Value.Value;
if (user.BoardID == Guid.Empty)
return null;
var boardRet = _userManager.GetBoardByID(user.BoardID);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
return null;
var board = boardRet.Value.Value;
return new OscilloscopeCtrl(board.IpAddr, board.Port);
}
catch (Exception ex)
{
logger.Error(ex, "获取示波器实例时发生异常");
return null;
}
}
/// <summary>
/// 初始化示波器
/// </summary>
/// <param name="config">示波器配置</param>
/// <returns>操作结果</returns>
[HttpPost("Initialize")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> Initialize([FromBody] OscilloscopeConfig config)
{
try
{
if (config == null)
return BadRequest("配置参数不能为空");
if (config.HorizontalShift > 1023)
return BadRequest("水平偏移量必须在0-1023之间");
if (config.DecimationRate > 1023)
return BadRequest("抽样率必须在0-1023之间");
var oscilloscope = GetOscilloscope();
if (oscilloscope == null)
return BadRequest("用户未绑定有效的实验板");
// 首先关闭捕获
var stopResult = await oscilloscope.SetCaptureEnable(false);
if (!stopResult.IsSuccessful)
{
logger.Error($"关闭捕获失败: {stopResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "关闭捕获失败");
}
// 设置触发电平
var levelResult = await oscilloscope.SetTriggerLevel(config.TriggerLevel);
if (!levelResult.IsSuccessful)
{
logger.Error($"设置触发电平失败: {levelResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置触发电平失败");
}
// 设置触发边沿
var edgeResult = await oscilloscope.SetTriggerEdge(config.TriggerRisingEdge);
if (!edgeResult.IsSuccessful)
{
logger.Error($"设置触发边沿失败: {edgeResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置触发边沿失败");
}
// 设置水平偏移量
var shiftResult = await oscilloscope.SetHorizontalShift(config.HorizontalShift);
if (!shiftResult.IsSuccessful)
{
logger.Error($"设置水平偏移量失败: {shiftResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置水平偏移量失败");
}
// 设置抽样率
var rateResult = await oscilloscope.SetDecimationRate(config.DecimationRate);
if (!rateResult.IsSuccessful)
{
logger.Error($"设置抽样率失败: {rateResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置抽样率失败");
}
// // 刷新RAM
// if (config.AutoRefreshRAM)
// {
// var refreshResult = await oscilloscope.RefreshRAM();
// if (!refreshResult.IsSuccessful)
// {
// logger.Error($"刷新RAM失败: {refreshResult.Error}");
// return StatusCode(StatusCodes.Status500InternalServerError, "刷新RAM失败");
// }
// }
// 设置捕获开关
var captureResult = await oscilloscope.SetCaptureEnable(config.CaptureEnabled);
if (!captureResult.IsSuccessful)
{
logger.Error($"设置捕获开关失败: {captureResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置捕获开关失败");
}
return Ok(true);
}
catch (Exception ex)
{
logger.Error(ex, "初始化示波器时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 启动捕获
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("StartCapture")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> StartCapture()
{
try
{
var oscilloscope = GetOscilloscope();
if (oscilloscope == null)
return BadRequest("用户未绑定有效的实验板");
var result = await oscilloscope.SetCaptureEnable(true);
if (!result.IsSuccessful)
{
logger.Error($"启动捕获失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "启动捕获失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "启动捕获时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 停止捕获
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("StopCapture")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> StopCapture()
{
try
{
var oscilloscope = GetOscilloscope();
if (oscilloscope == null)
return BadRequest("用户未绑定有效的实验板");
var result = await oscilloscope.SetCaptureEnable(false);
if (!result.IsSuccessful)
{
logger.Error($"停止捕获失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "停止捕获失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "停止捕获时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 获取示波器数据和状态
/// </summary>
/// <returns>示波器数据和状态信息</returns>
[HttpGet("GetData")]
[ProducesResponseType(typeof(OscilloscopeDataResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetData()
{
try
{
var oscilloscope = GetOscilloscope();
if (oscilloscope == null)
return BadRequest("用户未绑定有效的实验板");
var freqResult = await oscilloscope.GetADFrequency();
var vppResult = await oscilloscope.GetADVpp();
var maxResult = await oscilloscope.GetADMax();
var minResult = await oscilloscope.GetADMin();
var waveformResult = await oscilloscope.GetWaveformData();
if (!freqResult.IsSuccessful)
{
logger.Error($"获取AD采样频率失败: {freqResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "获取AD采样频率失败");
}
if (!vppResult.IsSuccessful)
{
logger.Error($"获取AD采样幅度失败: {vppResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "获取AD采样幅度失败");
}
if (!maxResult.IsSuccessful)
{
logger.Error($"获取AD采样最大值失败: {maxResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "获取AD采样最大值失败");
}
if (!minResult.IsSuccessful)
{
logger.Error($"获取AD采样最小值失败: {minResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "获取AD采样最小值失败");
}
if (!waveformResult.IsSuccessful)
{
logger.Error($"获取波形数据失败: {waveformResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "获取波形数据失败");
}
var response = new OscilloscopeDataResponse
{
AdFrequency = freqResult.Value,
AdVpp = vppResult.Value,
AdMax = maxResult.Value,
AdMin = minResult.Value,
WaveformData = Convert.ToBase64String(waveformResult.Value)
};
return Ok(response);
}
catch (Exception ex)
{
logger.Error(ex, "获取示波器数据时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 更新触发参数
/// </summary>
/// <param name="level">触发电平0-255</param>
/// <param name="risingEdge">触发边沿true为上升沿false为下降沿</param>
/// <returns>操作结果</returns>
[HttpPost("UpdateTrigger")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> UpdateTrigger(byte level, bool risingEdge)
{
try
{
var oscilloscope = GetOscilloscope();
if (oscilloscope == null)
return BadRequest("用户未绑定有效的实验板");
// 设置触发电平
var levelResult = await oscilloscope.SetTriggerLevel(level);
if (!levelResult.IsSuccessful)
{
logger.Error($"设置触发电平失败: {levelResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置触发电平失败");
}
// 设置触发边沿
var edgeResult = await oscilloscope.SetTriggerEdge(risingEdge);
if (!edgeResult.IsSuccessful)
{
logger.Error($"设置触发边沿失败: {edgeResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置触发边沿失败");
}
return Ok(true);
}
catch (Exception ex)
{
logger.Error(ex, "更新触发参数时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 更新采样参数
/// </summary>
/// <param name="horizontalShift">水平偏移量0-1023</param>
/// <param name="decimationRate">抽样率0-1023</param>
/// <returns>操作结果</returns>
[HttpPost("UpdateSampling")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> UpdateSampling(ushort horizontalShift, ushort decimationRate)
{
try
{
if (horizontalShift > 1023)
return BadRequest("水平偏移量必须在0-1023之间");
if (decimationRate > 1023)
return BadRequest("抽样率必须在0-1023之间");
var oscilloscope = GetOscilloscope();
if (oscilloscope == null)
return BadRequest("用户未绑定有效的实验板");
// 设置水平偏移量
var shiftResult = await oscilloscope.SetHorizontalShift(horizontalShift);
if (!shiftResult.IsSuccessful)
{
logger.Error($"设置水平偏移量失败: {shiftResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置水平偏移量失败");
}
// 设置抽样率
var rateResult = await oscilloscope.SetDecimationRate(decimationRate);
if (!rateResult.IsSuccessful)
{
logger.Error($"设置抽样率失败: {rateResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置抽样率失败");
}
return Ok(true);
}
catch (Exception ex)
{
logger.Error(ex, "更新采样参数时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 手动刷新RAM
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("RefreshRAM")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> RefreshRAM()
{
try
{
var oscilloscope = GetOscilloscope();
if (oscilloscope == null)
return BadRequest("用户未绑定有效的实验板");
var result = await oscilloscope.RefreshRAM();
if (!result.IsSuccessful)
{
logger.Error($"刷新RAM失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "刷新RAM失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "刷新RAM时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Collections;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace server.Controllers;
/// <summary>
/// 矩阵键控制器,用于管理矩阵键的启用、禁用和状态设置
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class PowerController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="enable">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("SetPowerOnOff")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SetPowerOnOff(string address, int port, bool enable)
{
var powerCtrl = new Peripherals.PowerClient.Power(address, port);
var ret = await powerCtrl.SetPowerOnOff(enable);
if (ret.IsSuccessful)
{
var powerStatus = enable ? "ON" : "OFF";
logger.Info($"Set device {address}:{port.ToString()} power {powerStatus} finished: {ret.Value}.");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
}

View File

@@ -1,4 +1,5 @@
using DotNext;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
@@ -24,6 +25,7 @@ public class RemoteUpdateController : ControllerBase
/// <param name="bitstream2">比特流文件2</param>
/// <param name="bitstream3">比特流文件3</param>
/// <returns>上传结果</returns>
[Authorize("Admin")]
[HttpPost("UploadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
@@ -129,6 +131,7 @@ public class RemoteUpdateController : ControllerBase
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
/// <param name="bitstreamNum"> 比特流位号 </param>
[Authorize("Admin")]
[HttpPost("DownloadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
@@ -150,7 +153,7 @@ public class RemoteUpdateController : ControllerBase
if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error);
// 下载比特流
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port);
var ret = await remoteUpdater.UpdateBitstream(bitstreamNum, fileBytes.Value);
if (ret.IsSuccessful)
@@ -179,6 +182,7 @@ public class RemoteUpdateController : ControllerBase
/// <param name="port">设备端口</param>
/// <param name="bitstreamNum">比特流编号</param>
/// <returns>总共上传比特流的数量</returns>
[Authorize("Admin")]
[HttpPost("DownloadMultiBitstreams")]
[EnableCors("Users")]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
@@ -210,7 +214,7 @@ public class RemoteUpdateController : ControllerBase
}
// 下载比特流
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port);
{
var ret = await remoteUpdater.UploadBitstreams(bitstreams[0], bitstreams[1], bitstreams[2], bitstreams[3]);
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
@@ -239,6 +243,7 @@ public class RemoteUpdateController : ControllerBase
/// <param name="port">设备端口</param>
/// <param name="bitstreamNum">比特流编号</param>
/// <returns>操作结果</returns>
[Authorize("Admin")]
[HttpPost("HotResetBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
@@ -246,7 +251,7 @@ public class RemoteUpdateController : ControllerBase
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> HotResetBitstream(string address, int port, int bitstreamNum)
{
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port);
var ret = await remoteUpdater.HotResetBitstream(bitstreamNum);
if (ret.IsSuccessful)
@@ -267,6 +272,7 @@ public class RemoteUpdateController : ControllerBase
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[Authorize("Admin")]
[HttpPost("GetFirmwareVersion")]
[EnableCors("Users")]
[ProducesResponseType(typeof(UInt32), StatusCodes.Status200OK)]
@@ -274,7 +280,7 @@ public class RemoteUpdateController : ControllerBase
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> GetFirmwareVersion(string address, int port)
{
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port);
var ret = await remoteUpdater.GetVersion();
if (ret.IsSuccessful)

View File

@@ -0,0 +1,355 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using DotNext;
using Database;
namespace server.Controllers;
/// <summary>
/// 资源控制器 - 提供统一的资源管理API
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ResourceController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly UserManager _userManager = new();
private readonly ResourceManager _resourceManager = new();
/// <summary>
/// 添加资源(文件上传)
/// </summary>
/// <param name="request">添加资源请求</param>
/// <param name="file">资源文件</param>
/// <returns>添加结果</returns>
[Authorize]
[HttpPost]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> AddResource([FromForm] AddResourceRequest request, IFormFile file)
{
if (string.IsNullOrWhiteSpace(request.ResourceType) || file == null)
return BadRequest("资源类型、资源用途和文件不能为空");
// 模板资源需要管理员权限
if (request.ResourcePurpose == ResourcePurpose.Template && !User.IsInRole("Admin"))
return Forbid("只有管理员可以添加模板资源");
try
{
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = _userManager.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 读取文件数据
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);
var fileData = memoryStream.ToArray();
var result = _resourceManager.AddResource(
user.ID, request.ResourceType, request.ResourcePurpose,
file.FileName, fileData, request.ExamID);
if (!result.IsSuccessful)
{
if (result.Error.Message.Contains("不存在"))
return NotFound(result.Error.Message);
logger.Error($"添加资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {result.Error.Message}");
}
var resourceInfo = new ResourceInfo(result.Value);
logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName} ID: {resourceInfo.ID}");
return CreatedAtAction(nameof(GetResourceById), new { resourceId = resourceInfo.ID }, resourceInfo);
}
catch (Exception ex)
{
logger.Error($"添加资源 {request.ResourceType}/{request.ResourcePurpose}/{file.FileName} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {ex.Message}");
}
}
/// <summary>
/// 获取资源列表
/// </summary>
/// <param name="examId">实验ID可选</param>
/// <param name="resourceType">资源类型(可选)</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <returns>资源列表</returns>
[Authorize]
[HttpGet]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetResourceList(
[FromQuery] string? examId = null,
[FromQuery] string? resourceType = null,
[FromQuery] ResourcePurpose? resourcePurpose = null)
{
try
{
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = _userManager.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
Result<List<Resource>> result;
// 管理员
if (user.Permission == UserPermission.Admin)
{
result = _resourceManager.GetFullResourceList(examId, resourceType, resourcePurpose);
}
// 用户
else if (resourcePurpose == ResourcePurpose.User)
{
result = _resourceManager.GetFullResourceList(examId, resourceType, resourcePurpose, user.ID);
}
// 模板
else if (resourcePurpose == ResourcePurpose.Template)
{
result = _resourceManager.GetFullResourceList(examId, resourceType, resourcePurpose);
}
// 其他
else
{
// 这种情况下需要分别查询并合并结果
var userResourcesResult = _resourceManager.GetFullResourceList(
examId, resourceType, ResourcePurpose.User, user.ID);
var templateResourcesResult = _resourceManager.GetFullResourceList(
examId, resourceType, ResourcePurpose.Template, null);
if (!userResourcesResult.IsSuccessful || !templateResourcesResult.IsSuccessful)
{
logger.Error($"获取资源列表时出错");
return StatusCode(StatusCodes.Status500InternalServerError, "获取资源列表失败");
}
var allResources = userResourcesResult.Value.Concat(templateResourcesResult.Value)
.OrderByDescending(r => r.UploadTime);
var mergedResourceInfos = allResources.Select(r => new ResourceInfo(r)).ToArray();
logger.Info($"成功获取资源列表,共 {mergedResourceInfos.Length} 个资源");
return Ok(mergedResourceInfos);
}
if (!result.IsSuccessful)
{
logger.Error($"获取资源列表时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {result.Error.Message}");
}
var resources = result.Value.Select(r => new ResourceInfo(r)).ToArray();
logger.Info($"成功获取资源列表,共 {resources.Length} 个资源");
return Ok(resources);
}
catch (Exception ex)
{
logger.Error($"获取资源列表时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据资源ID下载资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源文件</returns>
[HttpGet("{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetResourceById(Guid resourceId)
{
try
{
var result = _resourceManager.GetResourceById(resourceId);
if (!result.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = result.Value;
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
var dataRet = _resourceManager.ReadBytesFromPath(resource.Path);
if (!dataRet.IsSuccessful)
{
logger.Error($"读取资源数据时出错: {dataRet.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"读取资源数据失败: {dataRet.Error.Message}");
}
return File(dataRet.Value, resource.MimeType ?? "application/octet-stream", resource.ResourceName);
}
catch (Exception ex)
{
logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}");
}
}
/// <summary>
/// 删除资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>删除结果</returns>
[Authorize]
[HttpDelete("{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult DeleteResource(Guid resourceId)
{
try
{
// 获取当前用户信息
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = _userManager.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 先获取资源信息以验证权限
var resourceResult = _resourceManager.GetResourceById(resourceId);
if (!resourceResult.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = resourceResult.Value;
// 权限检查:管理员可以删除所有资源,普通用户只能删除自己的用户资源
if (!User.IsInRole("Admin"))
{
if (resource.Purpose == ResourcePurpose.Template)
return Forbid("普通用户不能删除模板资源");
if (resource.UserID != user.ID)
return Forbid("只能删除自己的资源");
}
var deleteResult = _resourceManager.DeleteResource(resourceId);
if (!deleteResult.IsSuccessful)
{
logger.Error($"删除资源时出错: {deleteResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {deleteResult.Error.Message}");
}
logger.Info($"成功删除资源: {resourceId} ({resource.ResourceName})");
return NoContent();
}
catch (Exception ex)
{
logger.Error($"删除资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {ex.Message}");
}
}
}
/// <summary>
/// 资源信息类
/// </summary>
public class ResourceInfo
{
/// <summary>
/// 资源ID
/// </summary>
public Guid ID { get; set; }
/// <summary>
/// 资源名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 资源类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 资源用途template/user
/// </summary>
public ResourcePurpose Purpose { get; set; }
/// <summary>
/// 上传时间
/// </summary>
public DateTime UploadTime { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
/// <summary>
/// MIME类型
/// </summary>
public string? MimeType { get; set; }
public ResourceInfo(Resource resource)
{
ID = resource.ID;
Name = resource.ResourceName;
Type = resource.ResourceType;
Purpose = resource.Purpose;
UploadTime = resource.UploadTime;
ExamID = resource.ExamID;
MimeType = resource.MimeType;
}
}
/// <summary>
/// 添加资源请求类
/// </summary>
public class AddResourceRequest
{
/// <summary>
/// 资源类型
/// </summary>
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required ResourcePurpose ResourcePurpose { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
}

View File

@@ -0,0 +1,127 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Cors;
using Peripherals.SwitchClient;
namespace server.Controllers;
[ApiController]
[Route("api/[controller]")]
public class SwitchController : ControllerBase
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly Database.UserManager _userManager = new();
/// <summary>
/// 获取示波器实例
/// </summary>
private SwitchCtrl? GetSwitchCtrl()
{
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return null;
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
return null;
var user = userRet.Value.Value;
if (user.BoardID == Guid.Empty)
return null;
var boardRet = _userManager.GetBoardByID(user.BoardID);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
return null;
var board = boardRet.Value.Value;
return new SwitchCtrl(board.IpAddr, board.Port, 0);
}
/// <summary>
/// 启用或禁用 Switch 外设
/// </summary>
/// <param name="enable">是否启用</param>
/// <returns>操作结果</returns>
[HttpPost("enable")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetEnable([FromQuery] bool enable)
{
var switchCtrl = GetSwitchCtrl();
if (switchCtrl == null)
return BadRequest("Can't get user or board info");
var result = await switchCtrl.SetEnable(enable);
if (!result.IsSuccessful)
{
logger.Error(result.Error, "SetEnable failed");
return StatusCode(500, result.Error);
}
return Ok(result.Value);
}
/// <summary>
/// 控制指定编号的 Switch 开关
/// </summary>
/// <param name="num">开关编号</param>
/// <param name="onOff">开/关</param>
/// <returns>操作结果</returns>
[HttpPost("switch")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetSwitchOnOff([FromQuery] int num, [FromQuery] bool onOff)
{
if (num <= 0 || num > 6)
return BadRequest(new ArgumentException($"Switch num should be 1~5, instead of {num}"));
var switchCtrl = GetSwitchCtrl();
if (switchCtrl == null)
return BadRequest("Can't get user or board info");
var result = await switchCtrl.SetSwitchOnOff(num, onOff);
if (!result.IsSuccessful)
{
logger.Error(result.Error, $"SetSwitchOnOff({num}, {onOff}) failed");
return StatusCode(500, result.Error);
}
return Ok(result.Value);
}
/// <summary>
/// 控制 Switch 开关
/// </summary>
/// <param name="keyStatus">开关状态</param>
/// <returns>操作结果</returns>
[HttpPost("MultiSwitch")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetMultiSwitchsOnOff(bool[] keyStatus)
{
if (keyStatus.Length == 0 || keyStatus.Length > 6) return BadRequest(
new ArgumentException($"Switch num should be 1~5, instead of {keyStatus.Length}"));
var switchCtrl = GetSwitchCtrl();
if (switchCtrl == null)
return BadRequest("Can't get user or board info");
for (int i = 0; i < keyStatus.Length; i++)
{
var result = await switchCtrl.SetSwitchOnOff(i + 1, keyStatus[i]);
if (!result.IsSuccessful)
{
logger.Error(result.Error, $"SetSwitchOnOff({i}, {keyStatus[i]}) failed");
return StatusCode(500, result.Error);
}
if (!result.Value) return Ok(false);
}
return Ok(true);
}
}

View File

@@ -14,6 +14,11 @@ public class TutorialController : ControllerBase
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IWebHostEnvironment _environment;
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="environment">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public TutorialController(IWebHostEnvironment environment)
{
_environment = environment;

View File

@@ -106,15 +106,16 @@ public class UDPController : ControllerBase
}
/// <summary>
/// 获取指定IP地址接的数据列表
/// 获取指定IP地址接的数据列表
/// </summary>
/// <param name="address">IP地址</param>
/// <param name="taskID">任务ID</param>
[HttpGet("GetRecvDataArray")]
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> GetRecvDataArray(string address)
public async ValueTask<IResult> GetRecvDataArray(string address, int taskID)
{
var ret = await MsgBus.UDPServer.GetDataArrayAsync(address);
var ret = await MsgBus.UDPServer.GetDataArrayAsync(address, taskID);
if (ret.HasValue)
{

View File

@@ -0,0 +1,380 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using DotNext;
using server.Services;
/// <summary>
/// 视频流控制器,支持动态配置摄像头连接
/// </summary>
[ApiController]
[Authorize]
[EnableCors("Users")]
[Route("api/[controller]")]
public class VideoStreamController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly HttpVideoStreamService _videoStreamService;
private readonly Database.UserManager _userManager = new();
public class AvailableResolutionsResponse
{
public int Width { get; set; }
public int Height { get; set; }
public string Name { get; set; } = string.Empty;
public string Value => $"{Width}x{Height}";
}
/// <summary>
/// 初始化HTTP视频流控制器
/// </summary>
/// <param name="videoStreamService">HTTP视频流服务</param>
public VideoStreamController(HttpVideoStreamService videoStreamService)
{
logger.Info("创建VideoStreamController命名空间{Namespace}", this.GetType().Namespace);
_videoStreamService = videoStreamService;
}
private Optional<string> TryGetBoardId()
{
var userName = User.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrEmpty(userName))
{
logger.Error("User name not found in claims.");
return Optional<string>.None;
}
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
{
logger.Error("User not found.");
return Optional<string>.None;
}
var user = userRet.Value.Value;
var boardId = user.BoardID;
if (boardId == Guid.Empty)
{
logger.Error("No board bound to this user.");
return Optional<string>.None;
}
return boardId.ToString();
}
/// <summary>
/// 获取 HTTP 视频流服务状态
/// </summary>
/// <returns>服务状态信息</returns>
[HttpGet("ServiceStatus")]
[ProducesResponseType(typeof(VideoStreamServiceStatus), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public IResult GetServiceStatus()
{
try
{
// 使用HttpVideoStreamService提供的状态信息
var status = _videoStreamService.GetServiceStatus();
// 转换为小写首字母的JSON属性符合前端惯例
return TypedResults.Ok(status);
}
catch (Exception ex)
{
logger.Error(ex, "获取 HTTP 视频流服务状态失败");
return TypedResults.InternalServerError(ex.Message);
}
}
[HttpGet("MyEndpoint")]
[ProducesResponseType(typeof(VideoStreamEndpoint), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public IResult MyEndpoint()
{
try
{
var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
var endpoint = _videoStreamService.GetVideoEndpoint(boardId);
return TypedResults.Ok(endpoint);
}
catch (Exception ex)
{
logger.Error(ex, "获取摄像头配置失败");
return TypedResults.InternalServerError(ex.Message);
}
}
/// <summary>
/// 测试 HTTP 视频流连接
/// </summary>
/// <returns>连接测试结果</returns>
[HttpPost("TestConnection")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async Task<IResult> TestConnection()
{
try
{
var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
var endpoint = _videoStreamService.GetVideoEndpoint(boardId);
// 尝试通过HTTP请求检查视频流服务是否可访问
bool isConnected = false;
using (var httpClient = new HttpClient())
{
httpClient.Timeout = TimeSpan.FromSeconds(2); // 设置较短的超时时间
var response = await httpClient.GetAsync(endpoint.MjpegUrl);
// 只要能连接上就认为成功,不管返回状态
isConnected = response.IsSuccessStatusCode;
}
var ret = await _videoStreamService.TestCameraConnection(boardId);
return TypedResults.Ok(ret);
}
catch (Exception ex)
{
logger.Error(ex, "HTTP 视频流连接测试失败");
// 连接失败但不抛出异常,而是返回连接失败的结果
return TypedResults.Ok(false);
}
}
[HttpPost("SetVideoStreamEnable")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> SetVideoStreamEnable(bool enable)
{
try
{
var boardId = TryGetBoardId().OrThrow(() => new ArgumentException("Board ID is required"));
await _videoStreamService.SetVideoStreamEnableAsync(boardId.ToString(), enable);
return Ok($"HDMI transmission for board {boardId} {enable.ToString()}.");
}
catch (Exception ex)
{
logger.Error(ex, $"Failed to disable HDMI transmission for board");
return StatusCode(500, $"Error disabling HDMI transmission: {ex.Message}");
}
}
/// <summary>
/// 设置视频流分辨率
/// </summary>
/// <param name="request">分辨率配置请求</param>
/// <returns>设置结果</returns>
[HttpPost("Resolution")]
[EnableCors("Users")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
public async Task<IResult> SetResolution([FromBody] ResolutionConfigRequest request)
{
try
{
var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
var ret = await _videoStreamService.SetResolutionAsync(boardId, request.Width, request.Height);
if (ret.IsSuccessful && ret.Value)
{
return TypedResults.Ok(new
{
success = true,
message = $"成功设置分辨率为 {request.Width}x{request.Height}",
width = request.Width,
height = request.Height,
timestamp = DateTime.Now
});
}
else
{
return TypedResults.BadRequest(new
{
success = false,
message = ret.Error?.ToString() ?? "未知错误",
timestamp = DateTime.Now
});
}
}
catch (Exception ex)
{
logger.Error(ex, $"设置分辨率为 {request.Width}x{request.Height} 失败");
return TypedResults.InternalServerError($"设置分辨率失败: {ex.Message}");
}
}
/// <summary>
/// 获取支持的分辨率列表
/// </summary>
/// <returns>支持的分辨率列表</returns>
[HttpGet("SupportedResolutions")]
[EnableCors("Users")]
[ProducesResponseType(typeof(AvailableResolutionsResponse[]), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
public IResult GetSupportedResolutions()
{
// (640, 480, "640x480 (VGA)"),
// (960, 540, "960x540 (qHD)"),
// (1280, 720, "1280x720 (HD)"),
// (1280, 960, "1280x960 (SXGA)"),
// (1920, 1080, "1920x1080 (Full HD)")
return TypedResults.Ok(new AvailableResolutionsResponse[]
{
new AvailableResolutionsResponse { Width = 640, Height = 480, Name = "640x480(VGA)" },
new AvailableResolutionsResponse { Width = 960, Height = 480, Name = "960x480(qHD)" },
new AvailableResolutionsResponse { Width = 1280, Height = 720, Name = "1280x720(HD)" },
new AvailableResolutionsResponse { Width = 1280, Height = 960, Name = "1280x960(SXGA)" },
new AvailableResolutionsResponse { Width = 1920, Height = 1080, Name = "1920x1080(Full HD)" }
});
}
/// <summary>
/// 初始化摄像头自动对焦功能
/// </summary>
/// <returns>初始化结果</returns>
[HttpPost("InitAutoFocus")]
[EnableCors("Users")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
public async Task<IResult> InitAutoFocus()
{
try
{
var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
var result = await _videoStreamService.InitAutoFocusAsync(boardId);
if (result)
{
logger.Info("自动对焦初始化成功");
return TypedResults.Ok(new
{
success = true,
message = "自动对焦初始化成功",
timestamp = DateTime.Now
});
}
else
{
logger.Warn("自动对焦初始化失败");
return TypedResults.BadRequest(new
{
success = false,
message = "自动对焦初始化失败",
timestamp = DateTime.Now
});
}
}
catch (Exception ex)
{
logger.Error(ex, "初始化自动对焦时发生异常");
return TypedResults.InternalServerError($"初始化自动对焦失败: {ex.Message}");
}
}
/// <summary>
/// 执行自动对焦
/// </summary>
/// <returns>对焦结果</returns>
[HttpPost("AutoFocus")]
[EnableCors("Users")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
public async Task<IResult> AutoFocus()
{
try
{
var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
var result = await _videoStreamService.PerformAutoFocusAsync(boardId);
if (result)
{
logger.Info("自动对焦执行成功");
return TypedResults.Ok(new
{
success = true,
message = "自动对焦执行成功",
timestamp = DateTime.Now
});
}
else
{
logger.Warn("自动对焦执行失败");
return TypedResults.BadRequest(new
{
success = false,
message = "自动对焦执行失败",
timestamp = DateTime.Now
});
}
}
catch (Exception ex)
{
logger.Error(ex, "执行自动对焦时发生异常");
return TypedResults.InternalServerError($"执行自动对焦失败: {ex.Message}");
}
}
/// <summary>
/// 配置摄像头连接参数
/// </summary>
/// <returns>配置结果</returns>
[HttpPost("ConfigureCamera")]
[EnableCors("Users")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async Task<IResult> ConfigureCamera()
{
try
{
var boardId = TryGetBoardId().OrThrow(() => new Exception("Board ID not found"));
var ret = await _videoStreamService.ConfigureCameraAsync(boardId);
if (ret)
{
return TypedResults.Ok(new { Message = "配置成功" });
}
else
{
return TypedResults.BadRequest(new { Message = "配置失败" });
}
}
catch (Exception ex)
{
logger.Error(ex, "配置摄像头连接失败");
return TypedResults.InternalServerError(ex.Message);
}
}
/// <summary>
/// 分辨率配置请求模型
/// </summary>
public class ResolutionConfigRequest
{
/// <summary>
/// 宽度
/// </summary>
[Required]
[Range(640, 1920, ErrorMessage = "宽度必须在640-1920范围内")]
public int Width { get; set; }
/// <summary>
/// 高度
/// </summary>
[Required]
[Range(480, 1080, ErrorMessage = "高度必须在480-1080范围内")]
public int Height { get; set; }
}
}

View File

@@ -1,113 +0,0 @@
using LinqToDB;
using LinqToDB.Data;
using LinqToDB.Mapping;
namespace Database;
/// <summary>
/// 用户类,表示用户信息
/// </summary>
public class User
{
/// <summary>
/// 用户的唯一标识符
/// </summary>
[PrimaryKey]
public Guid ID { get; set; } = Guid.NewGuid();
/// <summary>
/// 用户的名称
/// </summary>
[NotNull]
public required string Name { get; set; }
}
/// <summary>
/// FPGA 板子类,表示板子信息
/// </summary>
public class Board
{
/// <summary>
/// FPGA 板子的唯一标识符
/// </summary>
[PrimaryKey]
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// FPGA 板子的名称
/// </summary>
[NotNull]
public required string BoardName { get; set; }
}
/// <summary>
/// 应用程序数据连接类,用于与数据库交互
/// </summary>
public class AppDataConnection : DataConnection
{
static readonly LinqToDB.DataOptions options =
new LinqToDB.DataOptions()
.UseSQLite($"Data Source={Environment.CurrentDirectory}/Database.sqlite");
/// <summary>
/// 初始化应用程序数据连接
/// </summary>
public AppDataConnection() : base(options) { }
/// <summary>
/// 创建所有数据库表
/// </summary>
public void CreateAllTables()
{
this.CreateTable<User>();
this.CreateTable<Board>();
}
/// <summary>
/// 删除所有数据库表
/// </summary>
public void DropAllTables()
{
this.DropTable<User>();
this.DropTable<Board>();
}
/// <summary>
/// 添加一个新的用户到数据库
/// </summary>
/// <param name="name">用户的名称</param>
/// <returns>插入的记录数</returns>
public int AddUser(string name)
{
var user = new User()
{
Name = name
};
return this.Insert(user);
}
/// <summary>
/// 添加一块新的 FPGA 板子到数据库
/// </summary>
/// <param name="name">FPGA 板子的名称</param>
/// <returns>插入的记录数</returns>
public int AddBoard(string name)
{
var board = new Board()
{
BoardName = name
};
return this.Insert(board);
}
/// <summary>
/// 用户表
/// </summary>
public ITable<User> User => this.GetTable<User>();
/// <summary>
/// FPGA 板子表
/// </summary>
public ITable<Board> Board => this.GetTable<Board>();
}

View File

@@ -0,0 +1,98 @@
using DotNext;
using LinqToDB;
using LinqToDB.Data;
namespace Database;
/// <summary>
/// 应用程序数据连接类,用于与数据库交互
/// </summary>
public class AppDataConnection : DataConnection
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
static readonly string DATABASE_FILEPATH = $"{Global.DataPath}/Database.sqlite";
static readonly LinqToDB.DataOptions options =
new LinqToDB.DataOptions().UseSQLite($"Data Source={DATABASE_FILEPATH}");
/// <summary>
/// 用户表
/// </summary>
public ITable<User> UserTable => this.GetTable<User>();
/// <summary>
/// FPGA 板子表
/// </summary>
public ITable<Board> BoardTable => this.GetTable<Board>();
/// <summary>
/// 实验表
/// </summary>
public ITable<Exam> ExamTable => this.GetTable<Exam>();
/// <summary>
/// 资源表(统一管理实验资源、用户比特流等)
/// </summary>
public ITable<Resource> ResourceTable => this.GetTable<Resource>();
/// <summary>
/// 初始化应用程序数据连接
/// </summary>
public AppDataConnection() : base(options)
{
var filePath = Path.GetDirectoryName(DATABASE_FILEPATH);
if (!string.IsNullOrEmpty(filePath) && !Directory.Exists(filePath))
{
Directory.CreateDirectory(filePath);
}
if (!Path.Exists(DATABASE_FILEPATH))
{
logger.Info($"数据库文件不存在,正在创建新数据库: {DATABASE_FILEPATH}");
LinqToDB.DataProvider.SQLite.SQLiteTools.CreateDatabase(DATABASE_FILEPATH);
this.CreateAllTables();
var user = new User()
{
Name = "Admin",
EMail = "selfconfusion@gmail.com",
Password = "12345678",
Permission = Database.UserPermission.Admin,
};
this.Insert(user);
logger.Info("默认管理员用户已创建");
}
else
{
logger.Info($"数据库连接已建立: {DATABASE_FILEPATH}");
}
}
/// <summary>
/// 创建所有数据库表
/// </summary>
public void CreateAllTables()
{
logger.Info("正在创建数据库表...");
this.CreateTable<User>();
this.CreateTable<Board>();
this.CreateTable<Exam>();
this.CreateTable<Resource>();
logger.Info("数据库表创建完成");
}
/// <summary>
/// 删除所有数据库表
/// </summary>
public void DropAllTables()
{
logger.Warn("正在删除所有数据库表...");
this.DropTable<User>();
this.DropTable<Board>();
this.DropTable<Exam>();
this.DropTable<Resource>();
logger.Warn("所有数据库表已删除");
}
}

View File

@@ -0,0 +1,149 @@
using DotNext;
using LinqToDB;
using LinqToDB.Data;
namespace Database;
public class ExamManager
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private AppDataConnection _db = new();
/// <summary>
/// 创建新实验
/// </summary>
/// <param name="id">实验ID</param>
/// <param name="name">实验名称</param>
/// <param name="description">实验描述</param>
/// <param name="tags">实验标签</param>
/// <param name="difficulty">实验难度</param>
/// <param name="isVisibleToUsers">普通用户是否可见</param>
/// <returns>创建的实验</returns>
public Result<Exam> CreateExam(string id, string name, string description, string[]? tags = null, int difficulty = 1, bool isVisibleToUsers = true)
{
try
{
// 检查实验ID是否已存在
var existingExam = _db.ExamTable.Where(e => e.ID.ToString() == id).FirstOrDefault();
if (existingExam != null)
{
logger.Error($"实验ID已存在: {id}");
return new(new Exception($"实验ID已存在: {id}"));
}
var exam = new Exam
{
ID = id,
Name = name,
Description = description,
Difficulty = Math.Max(1, Math.Min(5, difficulty)),
IsVisibleToUsers = isVisibleToUsers,
CreatedTime = DateTime.Now,
UpdatedTime = DateTime.Now
};
if (tags != null)
{
exam.SetTagsList(tags);
}
_db.Insert(exam);
logger.Info($"新实验已创建: {id} ({name})");
return new(exam);
}
catch (Exception ex)
{
logger.Error($"创建实验时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 更新实验信息
/// </summary>
/// <param name="id">实验ID</param>
/// <param name="name">实验名称</param>
/// <param name="description">实验描述</param>
/// <param name="tags">实验标签</param>
/// <param name="difficulty">实验难度</param>
/// <param name="isVisibleToUsers">普通用户是否可见</param>
/// <returns>更新的记录数</returns>
public Result<int> UpdateExam(string id, string? name = null, string? description = null, string[]? tags = null, int? difficulty = null, bool? isVisibleToUsers = null)
{
try
{
int result = 0;
if (name != null)
{
result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.Name, name).Update();
}
if (description != null)
{
result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.Description, description).Update();
}
if (tags != null)
{
var tagsString = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim()));
result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.Tags, tagsString).Update();
}
if (difficulty.HasValue)
{
result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.Difficulty, Math.Max(1, Math.Min(5, difficulty.Value))).Update();
}
if (isVisibleToUsers.HasValue)
{
result += _db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.IsVisibleToUsers, isVisibleToUsers.Value).Update();
}
// 更新时间
_db.ExamTable.Where(e => e.ID.ToString() == id).Set(e => e.UpdatedTime, DateTime.Now).Update();
logger.Info($"实验已更新: {id},更新记录数: {result}");
return new(result);
}
catch (Exception ex)
{
logger.Error($"更新实验时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 获取所有实验信息
/// </summary>
/// <returns>所有实验的数组</returns>
public Exam[] GetAllExams()
{
var exams = _db.ExamTable.OrderBy(e => e.ID).ToArray();
logger.Debug($"获取所有实验,共 {exams.Length} 个");
return exams;
}
/// <summary>
/// 根据实验ID获取实验信息
/// </summary>
/// <param name="examId">实验ID</param>
/// <returns>包含实验信息的结果,如果未找到则返回空</returns>
public Result<Optional<Exam>> GetExamByID(string examId)
{
var exams = _db.ExamTable.Where(exam => exam.ID.ToString() == examId).ToArray();
if (exams.Length > 1)
{
logger.Error($"数据库中存在多个相同ID的实验: {examId}");
return new(new Exception($"数据库中存在多个相同ID的实验: {examId}"));
}
if (exams.Length == 0)
{
logger.Info($"未找到ID对应的实验: {examId}");
return new(Optional<Exam>.None);
}
logger.Debug($"成功获取实验信息: {examId}");
return new(exams[0]);
}
}

View File

@@ -0,0 +1,350 @@
using DotNext;
using LinqToDB;
using LinqToDB.Data;
using System.Security.Cryptography;
namespace Database;
public class ResourceManager
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly AppDataConnection _db = new();
/// <summary>
/// 根据文件扩展名获取MIME类型
/// </summary>
/// <param name="extension">文件扩展名</param>
/// <param name="fileName">文件名(可选,用于特殊文件判断)</param>
/// <returns>MIME类型</returns>
private string GetMimeTypeFromExtension(string extension, string fileName = "")
{
// 特殊文件名处理
if (!string.IsNullOrEmpty(fileName) && fileName.Equals("diagram.json", StringComparison.OrdinalIgnoreCase))
{
return "application/json";
}
return extension.ToLowerInvariant() switch
{
".png" => "image/png",
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
".bmp" => "image/bmp",
".svg" => "image/svg+xml",
".bit" => "application/octet-stream",
".sbit" => "application/octet-stream",
".bin" => "application/octet-stream",
".mcs" => "application/octet-stream",
".hex" => "text/plain",
".json" => "application/json",
".zip" => "application/zip",
".md" => "text/markdown",
_ => "application/octet-stream"
};
}
/// <summary>
/// 将二进制数据写入指定路径
/// </summary>
/// <param name="path">目标文件路径</param>
/// <param name="data">要写入的二进制数据</param>
/// <returns>写入是否成功</returns>
public Result<bool> WriteBytesToPath(string path, byte[] data)
{
try
{
var filePath = Path.Combine(Global.DataPath, path);
var directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllBytes(filePath, data);
logger.Info($"成功写入文件: {filePath},大小: {data.Length} bytes");
return new(true);
}
catch (Exception ex)
{
logger.Error($"写入文件时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 从指定路径读取二进制数据
/// </summary>
/// <param name="path">要读取的文件路径</param>
/// <returns>读取到的二进制数据</returns>
public Result<byte[]> ReadBytesFromPath(string path)
{
try
{
var filePath = Path.Combine(Global.DataPath, path);
if (!File.Exists(filePath))
{
logger.Error($"文件不存在: {filePath}");
return new(new Exception($"文件不存在: {filePath}"));
}
var data = File.ReadAllBytes(filePath);
logger.Info($"成功读取文件: {filePath},大小: {data.Length} bytes");
return new(data);
}
catch (Exception ex)
{
logger.Error($"读取文件时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 添加资源
/// </summary>
/// <param name="userId">上传用户ID</param>
/// <param name="resourceType">资源类型</param>
/// <param name="resourcePurpose">资源用途template 或 user</param>
/// <param name="resourceName">资源名称</param>
/// <param name="data">资源二进制数据</param>
/// <param name="examId">所属实验ID可选</param>
/// <param name="mimeType">MIME类型可选将根据文件扩展名自动确定</param>
/// <returns>创建的资源</returns>
public Result<Resource> AddResource(
Guid userId, string resourceType, ResourcePurpose resourcePurpose,
string resourceName, byte[] data, string? examId = null, string? mimeType = null)
{
try
{
// 验证用户是否存在
var user = _db.UserTable.Where(u => u.ID == userId).FirstOrDefault();
if (user == null)
{
logger.Error($"用户不存在: {userId}");
return new(new Exception($"用户不存在: {userId}"));
}
// 如果指定了实验ID验证实验是否存在
if (!string.IsNullOrEmpty(examId))
{
var exam = _db.ExamTable.Where(e => e.ID.ToString() == examId).FirstOrDefault();
if (exam == null)
{
logger.Error($"实验不存在: {examId}");
return new(new Exception($"实验不存在: {examId}"));
}
}
// 验证资源用途
if (resourcePurpose != ResourcePurpose.Template &&
resourcePurpose != ResourcePurpose.User &&
resourcePurpose != ResourcePurpose.Homework)
{
logger.Error($"无效的资源用途: {resourcePurpose}");
return new(new Exception($"无效的资源用途: {resourcePurpose}"));
}
// 如果未指定MIME类型根据文件扩展名自动确定
if (string.IsNullOrEmpty(mimeType))
{
var extension = Path.GetExtension(resourceName).ToLowerInvariant();
mimeType = GetMimeTypeFromExtension(extension, resourceName);
}
// 计算数据的SHA256
var sha256Bytes = SHA256.HashData(data);
var sha256 = Common.String.BytesToBase64(sha256Bytes);
if (string.IsNullOrEmpty(sha256))
{
logger.Error($"SHA256计算失败");
return new(new Exception("SHA256计算失败"));
}
var duplicateResource = _db.ResourceTable.Where(r => r.SHA256 == sha256).FirstOrDefault();
if (duplicateResource != null && duplicateResource.ResourceName == resourceName)
{
logger.Info($"资源已存在: {resourceName}, ID: {duplicateResource.ID}, UserID: {duplicateResource.UserID}");
return duplicateResource;
}
var nowTime = DateTime.Now;
var resource = new Resource
{
UserID = userId,
ExamID = examId,
ResourceType = resourceType,
Purpose = resourcePurpose,
ResourceName = resourceName,
Path = duplicateResource == null ?
Path.Combine(resourceType, nowTime.ToString("yyyyMMddHH"), resourceName) :
duplicateResource.Path,
SHA256 = sha256,
MimeType = mimeType,
UploadTime = nowTime
};
var insertedId = _db.Insert(resource);
var writeRet = WriteBytesToPath(resource.Path, data);
if (writeRet.IsSuccessful && writeRet.Value)
{
logger.Info($"新资源已添加: {userId}/{resourceType}/{resourcePurpose}/{resourceName} ({data.Length} bytes)" +
(examId != null ? $" [实验: {examId}]" : "") + $" [ID: {resource.ID}]");
return new(resource);
}
else
{
_db.ResourceTable.Where(r => r.ID == resource.ID).Delete();
logger.Error($"写入资源文件时出错: {writeRet.Error}");
return new(new Exception(writeRet.Error?.ToString() ?? $"写入失败"));
}
}
catch (Exception ex)
{
logger.Error($"添加资源时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 获取资源信息列表返回ID和名称
/// <param name="resourceType">资源类型</param>
/// <param name="examId">实验ID可选</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <param name="userId">用户ID可选</param>
/// </summary>
/// <returns>资源信息列表</returns>
public Result<Resource[]> GetResourceListByType(
string resourceType,
ResourcePurpose? resourcePurpose = null,
string? examId = null,
Guid? userId = null)
{
try
{
var query = _db.ResourceTable.Where(r => r.ResourceType == resourceType);
if (examId != null)
{
query = query.Where(r => r.ExamID == examId);
}
if (resourcePurpose != null)
{
query = query.Where(r => r.Purpose == resourcePurpose);
}
if (userId != null)
{
query = query.Where(r => r.UserID == userId);
}
var resources = query.ToArray();
logger.Info($"获取资源列表: {resourceType}" +
(examId != null ? $"/{examId}" : "") +
($"/{resourcePurpose.ToString()}") +
(userId != null ? $"/{userId}" : "") +
$",共 {resources.Length} 个资源");
return new(resources);
}
catch (Exception ex)
{
logger.Error($"获取资源列表时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 获取完整的资源列表
/// </summary>
/// <param name="examId">实验ID可选</param>
/// <param name="resourceType">资源类型(可选)</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <param name="userId">用户ID可选</param>
/// <returns>完整的资源对象列表</returns>
public Result<List<Resource>> GetFullResourceList(
string? examId = null,
string? resourceType = null,
ResourcePurpose? resourcePurpose = null,
Guid? userId = null)
{
try
{
var query = _db.ResourceTable.AsQueryable();
if (examId != null)
{
query = query.Where(r => r.ExamID == examId);
}
if (resourceType != null)
{
query = query.Where(r => r.ResourceType == resourceType);
}
if (resourcePurpose != null)
{
query = query.Where(r => r.Purpose == resourcePurpose);
}
if (userId != null)
{
query = query.Where(r => r.UserID == userId);
}
var resources = query.OrderByDescending(r => r.UploadTime).ToList();
logger.Info($"获取完整资源列表" +
(examId != null ? $" [实验: {examId}]" : "") +
(resourceType != null ? $" [类型: {resourceType}]" : "") +
($" [用途: {resourcePurpose.ToString()}]") +
(userId != null ? $" [用户: {userId}]" : "") +
$",共 {resources.Count} 个资源");
return new(resources);
}
catch (Exception ex)
{
logger.Error($"获取完整资源列表时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 根据资源ID获取资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源数据</returns>
public Optional<Resource> GetResourceById(Guid resourceId)
{
var resource = _db.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
if (resource == null)
{
logger.Info($"未找到资源: {resourceId}");
return new(null);
}
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
return new(resource);
}
/// <summary>
/// 删除资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>删除的记录数</returns>
public Result<int> DeleteResource(Guid resourceId)
{
try
{
var result = _db.ResourceTable.Where(r => r.ID == resourceId).Delete();
logger.Info($"资源已删除: {resourceId},删除记录数: {result}");
return new(result);
}
catch (Exception ex)
{
logger.Error($"删除资源时出错: {ex.Message}");
return new(ex);
}
}
}

352
server/src/Database/Type.cs Normal file
View File

@@ -0,0 +1,352 @@
using DotNext;
using LinqToDB;
using LinqToDB.Mapping;
using Tapper;
namespace Database;
/// <summary>
/// 用户权限枚举
/// </summary>
public enum UserPermission
{
/// <summary>
/// 管理员权限,可以管理用户和实验板
/// </summary>
Admin,
/// <summary>
/// 普通用户权限,只能使用实验板
/// </summary>
Normal,
}
/// <summary>
/// 用户类,表示用户信息
/// </summary>
public class User
{
/// <summary>
/// 用户的唯一标识符
/// </summary>
[PrimaryKey]
public Guid ID { get; set; } = Guid.NewGuid();
/// <summary>
/// 用户的名称
/// </summary>
[NotNull]
public required string Name { get; set; }
/// <summary>
/// 用户的电子邮箱
/// </summary>
[NotNull]
public required string EMail { get; set; }
/// <summary>
/// 用户的密码(应该进行哈希处理)
/// </summary>
[NotNull]
public required string Password { get; set; }
/// <summary>
/// 用户权限等级
/// </summary>
[NotNull]
public required UserPermission Permission { get; set; }
/// <summary>
/// 绑定的实验板ID如果未绑定则为空
/// </summary>
[Nullable]
public Guid BoardID { get; set; }
/// <summary>
/// 用户绑定板子的过期时间
/// </summary>
[Nullable]
public DateTime? BoardExpireTime { get; set; }
}
/// <summary>
/// FPGA 板子状态枚举
/// </summary>
public enum BoardStatus
{
/// <summary>
/// 未启用状态,无法被使用
/// </summary>
Disabled,
/// <summary>
/// 繁忙状态,正在被用户使用
/// </summary>
Busy,
/// <summary>
/// 可用状态,可以被分配给用户
/// </summary>
Available,
}
/// <summary>
/// FPGA 板子类,表示板子信息
/// </summary>
public class Board
{
/// <summary>
/// FPGA 板子的唯一标识符
/// </summary>
[PrimaryKey]
public Guid ID { get; set; } = Guid.NewGuid();
/// <summary>
/// FPGA 板子的名称
/// </summary>
[NotNull]
public required string BoardName { get; set; }
/// <summary>
/// FPGA 板子的IP地址
/// </summary>
[NotNull]
public required string IpAddr { get; set; }
/// <summary>
/// FPGA 板子的MAC地址
/// </summary>
[NotNull]
public required string MacAddr { get; set; }
/// <summary>
/// FPGA 板子的通信端口
/// </summary>
[NotNull]
public int Port { get; set; } = 1234;
/// <summary>
/// FPGA 板子的当前状态
/// </summary>
[NotNull]
public required BoardStatus Status { get; set; }
/// <summary>
/// 占用该板子的用户的唯一标识符
/// </summary>
[Nullable]
public Guid OccupiedUserID { get; set; }
/// <summary>
/// 占用该板子的用户的用户名
/// </summary>
[Nullable]
public string? OccupiedUserName { get; set; }
/// <summary>
/// FPGA 板子的固件版本号
/// </summary>
[NotNull]
public string FirmVersion { get; set; } = "1.0.0";
}
/// <summary>
/// 实验类,表示实验信息
/// </summary>
public class Exam
{
/// <summary>
/// 实验的唯一标识符
/// </summary>
[PrimaryKey]
public required string ID { get; set; }
/// <summary>
/// 实验名称
/// </summary>
[NotNull]
public required string Name { get; set; }
/// <summary>
/// 实验描述
/// </summary>
[NotNull]
public required string Description { get; set; }
/// <summary>
/// 实验创建时间
/// </summary>
[NotNull]
public DateTime CreatedTime { get; set; } = DateTime.Now;
/// <summary>
/// 实验最后更新时间
/// </summary>
[NotNull]
public DateTime UpdatedTime { get; set; } = DateTime.Now;
/// <summary>
/// 实验标签(以逗号分隔的字符串)
/// </summary>
[NotNull]
public string Tags { get; set; } = "";
/// <summary>
/// 实验难度1-51为最简单
/// </summary>
[NotNull]
public int Difficulty { get; set; } = 1;
/// <summary>
/// 普通用户是否可见
/// </summary>
[NotNull]
public bool IsVisibleToUsers { get; set; } = true;
/// <summary>
/// 获取标签列表
/// </summary>
/// <returns>标签数组</returns>
public string[] GetTagsList()
{
if (string.IsNullOrWhiteSpace(Tags))
return Array.Empty<string>();
return Tags.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(tag => tag.Trim())
.Where(tag => !string.IsNullOrEmpty(tag))
.ToArray();
}
/// <summary>
/// 设置标签列表
/// </summary>
/// <param name="tags">标签数组</param>
public void SetTagsList(string[] tags)
{
Tags = string.Join(",", tags.Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim()));
}
}
/// <summary>
/// 资源类型枚举
/// </summary>
[TranspilationSource]
public static class ResourceTypes
{
/// <summary>
/// 图片资源类型
/// </summary>
public const string Images = "images";
/// <summary>
/// Markdown文档资源类型
/// </summary>
public const string Markdown = "markdown";
/// <summary>
/// 比特流文件资源类型
/// </summary>
public const string Bitstream = "bitstream";
/// <summary>
/// 原理图资源类型
/// </summary>
public const string Diagram = "diagram";
/// <summary>
/// 项目文件资源类型
/// </summary>
public const string Project = "project";
/// <summary>
/// 压缩文件资源类型
/// </summary>
public const string Compression = "compression";
}
public enum ResourcePurpose : int
{
/// <summary>
/// 模板资源,通常由管理员上传,供用户参考
/// </summary>
Template,
/// <summary>
/// 用户上传的资源
/// </summary>
User,
/// <summary>
/// 用户提交的作业
/// </summary>
Homework
}
/// <summary>
/// 资源类,统一管理实验资源、用户比特流等各类资源
/// </summary>
public class Resource
{
/// <summary>
/// 资源的唯一标识符
/// </summary>
[PrimaryKey]
public Guid ID { get; set; } = Guid.NewGuid();
/// <summary>
/// 上传资源的用户ID
/// </summary>
[NotNull]
public required Guid UserID { get; set; }
/// <summary>
/// 所属实验ID可选如果不属于特定实验则为空
/// </summary>
[Nullable]
public string? ExamID { get; set; }
/// <summary>
/// 资源类型images, markdown, bitstream, diagram, project等
/// </summary>
[NotNull]
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template模板或 user用户上传
/// </summary>
[NotNull]
public required ResourcePurpose Purpose { get; set; }
/// <summary>
/// 资源名称(包含文件扩展名)
/// </summary>
[NotNull]
public required string ResourceName { get; set; }
/// <summary>
/// 资源路径(包含文件名和扩展名)
/// </summary>
[NotNull]
public required string Path { get; set; }
/// <summary>
/// 资源SHA256哈希值
/// </summary>
[NotNull]
public required string SHA256 { get; set; }
/// <summary>
/// 资源创建/上传时间
/// </summary>
[NotNull]
public DateTime UploadTime { get; set; } = DateTime.Now;
/// <summary>
/// 资源的MIME类型
/// </summary>
[NotNull]
public string MimeType { get; set; } = "application/octet-stream";
}

View File

@@ -0,0 +1,453 @@
using DotNext;
using LinqToDB;
using LinqToDB.Data;
namespace Database;
public class UserManager
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly AppDataConnection _db = new();
/// <summary>
/// 添加一个新的用户到数据库
/// </summary>
/// <param name="name">用户的名称</param>
/// <param name="email">用户的电子邮箱地址</param>
/// <param name="password">用户的密码</param>
/// <returns>插入的记录数</returns>
public int AddUser(string name, string email, string password)
{
var user = new User()
{
Name = name,
EMail = email,
Password = password,
Permission = UserPermission.Normal,
};
var result = _db.Insert(user);
logger.Info($"新用户已添加: {name} ({email})");
return result;
}
/// <summary>
/// 根据用户名获取用户信息
/// </summary>
/// <param name="name">用户名</param>
/// <returns>包含用户信息的结果,如果未找到或出错则返回相应状态</returns>
public Result<Optional<User>> GetUserByName(string name)
{
var user = _db.UserTable.Where((user) => user.Name == name).ToArray();
if (user.Length > 1)
{
logger.Error($"数据库中存在多个同名用户: {name}");
return new(new Exception($"数据库中存在多个同名用户: {name}"));
}
if (user.Length == 0)
{
logger.Info($"未找到用户: {name}");
return new(Optional<User>.None);
}
logger.Debug($"成功获取用户信息: {name}");
return new(user[0]);
}
/// <summary>
/// 根据电子邮箱获取用户信息
/// </summary>
/// <param name="email">用户的电子邮箱地址</param>
/// <returns>包含用户信息的结果,如果未找到或出错则返回相应状态</returns>
public Result<Optional<User>> GetUserByEMail(string email)
{
var user = _db.UserTable.Where((user) => user.EMail == email).ToArray();
if (user.Length > 1)
{
logger.Error($"数据库中存在多个相同邮箱的用户: {email}");
return new(new Exception($"数据库中存在多个相同邮箱的用户: {email}"));
}
if (user.Length == 0)
{
logger.Info($"未找到邮箱对应的用户: {email}");
return new(Optional<User>.None);
}
logger.Debug($"成功获取用户信息: {email}");
return new(user[0]);
}
/// <summary>
/// 验证用户密码
/// </summary>
/// <param name="name">用户名</param>
/// <param name="password">用户密码</param>
/// <returns>如果密码正确返回用户信息,否则返回空</returns>
public Result<Optional<User>> CheckUserPassword(string name, string password)
{
var ret = GetUserByName(name);
if (!ret.IsSuccessful)
return new(ret.Error);
if (!ret.Value.HasValue)
return new(Optional<User>.None);
var user = ret.Value.Value;
if (user.Password == password)
{
logger.Info($"用户 {name} 密码验证成功");
return new(user);
}
else
{
logger.Warn($"用户 {name} 密码验证失败");
return new(Optional<User>.None);
}
}
/// <summary>
/// 绑定用户与实验板
/// </summary>
/// <param name="userId">用户的唯一标识符</param>
/// <param name="boardId">实验板的唯一标识符</param>
/// <param name="expireTime">绑定过期时间</param>
/// <returns>更新的记录数</returns>
public int BindUserToBoard(Guid userId, Guid boardId, DateTime expireTime)
{
// 获取用户信息
var user = _db.UserTable.Where(u => u.ID == userId).FirstOrDefault();
if (user == null)
{
logger.Error($"未找到用户: {userId}");
return 0;
}
// 更新用户的板子绑定信息
var userResult = _db.UserTable
.Where(u => u.ID == userId)
.Set(u => u.BoardID, boardId)
.Set(u => u.BoardExpireTime, expireTime)
.Update();
// 更新板子的用户绑定信息
var boardResult = _db.BoardTable
.Where(b => b.ID == boardId)
.Set(b => b.Status, BoardStatus.Busy)
.Set(b => b.OccupiedUserID, userId)
.Set(b => b.OccupiedUserName, user.Name)
.Update();
logger.Info($"用户 {userId} ({user.Name}) 已绑定到实验板 {boardId},过期时间: {expireTime}");
return userResult + boardResult;
}
/// <summary>
/// 解除用户与实验板的绑定
/// </summary>
/// <param name="userId">用户的唯一标识符</param>
/// <returns>更新的记录数</returns>
public int UnbindUserFromBoard(Guid userId)
{
// 获取用户当前绑定的板子ID
var user = _db.UserTable.Where(u => u.ID == userId).FirstOrDefault();
Guid boardId = user?.BoardID ?? Guid.Empty;
// 清空用户的板子绑定信息
var userResult = _db.UserTable
.Where(u => u.ID == userId)
.Set(u => u.BoardID, Guid.Empty)
.Set(u => u.BoardExpireTime, (DateTime?)null)
.Update();
// 如果用户原本绑定了板子,则清空板子的用户绑定信息
int boardResult = 0;
if (boardId != Guid.Empty)
{
boardResult = _db.BoardTable
.Where(b => b.ID == boardId)
.Set(b => b.Status, BoardStatus.Available)
.Set(b => b.OccupiedUserID, Guid.Empty)
.Set(b => b.OccupiedUserName, (string?)null)
.Update();
logger.Info($"实验板 {boardId} 状态已设置为空闲,用户绑定信息已清空");
}
logger.Info($"用户 {userId} 已解除实验板绑定");
return userResult + boardResult;
}
/// <summary>
/// 自动分配一个未被占用的IP地址
/// </summary>
/// <returns>分配的IP地址字符串</returns>
public string AllocateIpAddr()
{
var usedIps = _db.BoardTable.Select(b => b.IpAddr).ToArray();
for (int i = 1; i <= 254; i++)
{
string ip = $"169.254.109.{i}";
if (!usedIps.Contains(ip))
return ip;
}
throw new Exception("没有可用的IP地址");
}
/// <summary>
/// 自动分配一个未被占用的MAC地址
/// </summary>
/// <returns>分配的MAC地址字符串</returns>
public string AllocateMacAddr()
{
var usedMacs = _db.BoardTable.Select(b => b.MacAddr).ToArray();
// 以 02-00-00-xx-xx-xx 格式分配02 表示本地管理地址
for (int i = 1; i <= 0xFFFFFF; i++)
{
string mac = $"02-00-00-{(i >> 16) & 0xFF:X2}-{(i >> 8) & 0xFF:X2}-{i & 0xFF:X2}";
if (!usedMacs.Contains(mac))
return mac;
}
throw new Exception("没有可用的MAC地址");
}
/// <summary>
/// 添加一块新的 FPGA 板子到数据库
/// </summary>
/// <param name="name">FPGA 板子的名称</param>
/// <returns>插入的记录数</returns>
public Guid AddBoard(string name)
{
if (string.IsNullOrWhiteSpace(name) || name.Contains('\'') || name.Contains(';'))
{
logger.Error("实验板名称非法,包含不允许的字符");
throw new ArgumentException("实验板名称非法");
}
var board = new Board()
{
BoardName = name,
IpAddr = AllocateIpAddr(),
MacAddr = AllocateMacAddr(),
Status = BoardStatus.Disabled,
};
var result = _db.Insert(board);
logger.Info($"新实验板已添加: {name} ({board.IpAddr}:{board.MacAddr})");
return board.ID;
}
/// <summary>
/// 根据名称删除实验板
/// </summary>
/// <param name="name">实验板的名称</param>
/// <returns>删除的记录数</returns>
public int DeleteBoardByName(string name)
{
// 先获取要删除的板子信息
var board = _db.BoardTable.Where(b => b.BoardName == name).FirstOrDefault();
if (board == null)
{
logger.Warn($"未找到名称为 {name} 的实验板");
return 0;
}
// 如果板子被占用,先解除绑定
if (board.OccupiedUserID != Guid.Empty)
{
_db.UserTable
.Where(u => u.ID == board.OccupiedUserID)
.Set(u => u.BoardID, Guid.Empty)
.Set(u => u.BoardExpireTime, (DateTime?)null)
.Update();
logger.Info($"已解除用户 {board.OccupiedUserID} 与实验板 {name} 的绑定");
}
var result = _db.BoardTable.Where(b => b.BoardName == name).Delete();
logger.Info($"实验板已删除: {name},删除记录数: {result}");
return result;
}
/// <summary>
/// 根据ID删除实验板
/// </summary>
/// <param name="id">实验板的唯一标识符</param>
/// <returns>删除的记录数</returns>
public int DeleteBoardByID(Guid id)
{
// 先获取要删除的板子信息
var board = _db.BoardTable.Where(b => b.ID == id).FirstOrDefault();
if (board == null)
{
logger.Warn($"未找到ID为 {id} 的实验板");
return 0;
}
// 如果板子被占用,先解除绑定
if (board.OccupiedUserID != Guid.Empty)
{
_db.UserTable
.Where(u => u.ID == board.OccupiedUserID)
.Set(u => u.BoardID, Guid.Empty)
.Set(u => u.BoardExpireTime, (DateTime?)null)
.Update();
logger.Info($"已解除用户 {board.OccupiedUserID} 与实验板 {id} 的绑定");
}
var result = _db.BoardTable.Where(b => b.ID == id).Delete();
logger.Info($"实验板已删除: {id},删除记录数: {result}");
return result;
}
/// <summary>
/// 根据实验板ID获取实验板信息
/// </summary>
/// <param name="id">实验板的唯一标识符</param>
/// <returns>包含实验板信息的结果,如果未找到则返回空</returns>
public Result<Optional<Board>> GetBoardByID(Guid id)
{
var boards = _db.BoardTable.Where(board => board.ID == id).ToArray();
if (boards.Length > 1)
{
logger.Error($"数据库中存在多个相同ID的实验板: {id}");
return new(new Exception($"数据库中存在多个相同ID的实验板: {id}"));
}
if (boards.Length == 0)
{
logger.Info($"未找到ID对应的实验板: {id}");
return new(Optional<Board>.None);
}
logger.Debug($"成功获取实验板信息: {id}");
return new(boards[0]);
}
/// <summary>
/// 根据用户名获取实验板信息
/// </summary>
/// <param name="userName">用户名</param>
/// <returns>包含实验板信息的结果,如果未找到则返回空</returns>
public Result<Optional<Board>> GetBoardByUserName(string userName)
{
var boards = _db.BoardTable.Where(board => board.OccupiedUserName == userName).ToArray();
if (boards.Length > 1)
{
logger.Error($"数据库中存在多个相同用户名的实验板: {userName}");
return new(new Exception($"数据库中存在多个相同用户名的实验板: {userName}"));
}
if (boards.Length == 0)
{
logger.Info($"未找到用户名对应的实验板: {userName}");
return new(Optional<Board>.None);
}
logger.Debug($"成功获取实验板信息: {userName}");
return new(boards[0]);
}
/// <summary>
/// 获取所有实验板信息
/// </summary>
/// <returns>所有实验板的数组</returns>
public Board[] GetAllBoard()
{
var boards = _db.BoardTable.ToArray();
logger.Debug($"获取所有实验板,共 {boards.Length} 块");
return boards;
}
/// <summary>
/// 获取一块可用的实验板并将其状态设置为繁忙
/// </summary>
/// <param name="userId">要分配板子的用户ID</param>
/// <param name="expireTime">绑定过期时间</param>
/// <returns>可用的实验板,如果没有可用的板子则返回空</returns>
public Optional<Board> GetAvailableBoard(Guid userId, DateTime expireTime)
{
var boards = _db.BoardTable.Where(
(board) => board.Status == BoardStatus.Available
).ToArray();
if (boards.Length == 0)
{
logger.Warn("没有可用的实验板");
return new(null);
}
else
{
var board = boards[0];
var user = _db.UserTable.Where(u => u.ID == userId).FirstOrDefault();
if (user == null)
{
logger.Error($"未找到用户: {userId}");
return new(null);
}
// 更新板子状态和用户绑定信息
_db.BoardTable
.Where(target => target.ID == board.ID)
.Set(target => target.Status, BoardStatus.Busy)
.Set(target => target.OccupiedUserID, userId)
.Set(target => target.OccupiedUserName, user.Name)
.Update();
// 更新用户的板子绑定信息
_db.UserTable
.Where(u => u.ID == userId)
.Set(u => u.BoardID, board.ID)
.Set(u => u.BoardExpireTime, expireTime)
.Update();
board.Status = BoardStatus.Busy;
board.OccupiedUserID = userId;
board.OccupiedUserName = user.Name;
logger.Info($"实验板 {board.BoardName} ({board.ID}) 已分配给用户 {user.Name} ({userId}),过期时间: {expireTime}");
return new(board);
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="boardId">[TODO:parameter]</param>
/// <param name="newName">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public int UpdateBoardName(Guid boardId, string newName)
{
if (string.IsNullOrWhiteSpace(newName) || newName.Contains('\'') || newName.Contains(';'))
{
logger.Error("实验板名称非法,包含不允许的字符");
return 0;
}
var result = _db.BoardTable
.Where(b => b.ID == boardId)
.Set(b => b.BoardName, newName)
.Update();
logger.Info($"实验板名称已更新: {boardId} -> {newName}");
return result;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="boardId">[TODO:parameter]</param>
/// <param name="newStatus">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public int UpdateBoardStatus(Guid boardId, BoardStatus newStatus)
{
var result = _db.BoardTable
.Where(b => b.ID == boardId)
.Set(b => b.Status, newStatus)
.Update();
logger.Info($"TODO");
return result;
}
}

View File

@@ -0,0 +1,260 @@
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Cors;
using TypedSignalR.Client;
using Tapper;
using DotNext;
using Peripherals.SevenDigitalTubesClient;
using System.Collections.Concurrent;
#pragma warning disable 1998
namespace server.Hubs;
[Hub]
public interface IDigitalTubesHub
{
Task<bool> StartScan();
Task<bool> StopScan();
Task<bool> SetFrequency(int frequency);
Task<DigitalTubeTaskStatus?> GetStatus();
}
[Receiver]
public interface IDigitalTubesReceiver
{
Task OnReceive(byte[] data);
}
[TranspilationSource]
public class DigitalTubeTaskStatus
{
public int Frequency { get; set; } = 100;
public bool IsRunning { get; set; } = false;
}
class DigitalTubesScanTaskInfo
{
public string BoardID { get; set; }
public string ClientID { get; set; }
public Task? ScanTask { get; set; }
public SevenDigitalTubesCtrl TubeClient { get; set; }
public CancellationTokenSource CTS { get; set; } = new();
public int Frequency { get; set; } = 100;
public bool IsRunning { get; set; } = false;
public DigitalTubesScanTaskInfo(
string boardID, string clientID, SevenDigitalTubesCtrl client)
{
BoardID = boardID;
ClientID = clientID;
TubeClient = client;
}
public DigitalTubeTaskStatus ToDigitalTubeTaskStatus()
{
return new DigitalTubeTaskStatus
{
Frequency = Frequency,
IsRunning = IsRunning
};
}
}
[Authorize]
[EnableCors("SignalR")]
public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IHubContext<DigitalTubesHub, IDigitalTubesReceiver> _hubContext;
private readonly Database.UserManager _userManager = new();
private static ConcurrentDictionary<string, DigitalTubesScanTaskInfo> _scanTasks = new();
public DigitalTubesHub(IHubContext<DigitalTubesHub, IDigitalTubesReceiver> hubContext)
{
_hubContext = hubContext;
}
private Optional<Database.Board> TryGetBoard()
{
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrEmpty(userName))
{
logger.Error("User name is null or empty");
return null;
}
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
{
logger.Error($"User '{userName}' not found");
return null;
}
var user = userRet.Value.Value;
var boardRet = _userManager.GetBoardByID(user.BoardID);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
{
logger.Error($"Board not found");
return null;
}
return boardRet.Value.Value;
}
private Task ScanAllTubes(DigitalTubesScanTaskInfo scanInfo)
{
var token = scanInfo.CTS.Token;
return Task.Run(async () =>
{
var cntError = 0;
while (!token.IsCancellationRequested)
{
var beginTime = DateTime.Now;
var waitTime = TimeSpan.FromMilliseconds(1000 / scanInfo.Frequency);
var dataRet = await scanInfo.TubeClient.ScanAllTubes();
if (!dataRet.IsSuccessful)
{
logger.Error($"Failed to scan tubes: {dataRet.Error}");
cntError++;
if (cntError > 3)
{
logger.Error($"Too many errors, stopping scan");
break;
}
}
await _hubContext.Clients.Client(scanInfo.ClientID).OnReceive(dataRet.Value);
var processTime = DateTime.Now - beginTime;
if (processTime < waitTime)
{
await Task.Delay(waitTime - processTime, token);
}
}
scanInfo.IsRunning = false;
}, token)
.ContinueWith((task) =>
{
if (task.IsFaulted)
{
logger.Error(
$"Digital tubes scan operation failesj for board {task.Exception}");
}
else if (task.IsCanceled)
{
logger.Info(
$"Digital tubes scan operation cancelled for board {scanInfo.BoardID}");
}
else
{
logger.Info(
$"Digital tubes scan completed successfully for board {scanInfo.BoardID}");
}
});
}
public async Task<bool> StartScan()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var key = board.ID.ToString();
if (_scanTasks.TryGetValue(key, out var existing) && existing.IsRunning)
return true;
var cts = new CancellationTokenSource();
var scanTaskInfo = new DigitalTubesScanTaskInfo(
board.ID.ToString(), Context.ConnectionId,
new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 6)
);
scanTaskInfo.ScanTask = ScanAllTubes(scanTaskInfo);
_scanTasks[key] = scanTaskInfo;
return true;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to start scan");
return false;
}
}
public async Task<bool> StopScan()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var key = board.ID.ToString();
if (_scanTasks.TryRemove(key, out var scanInfo))
{
scanInfo.IsRunning = false;
scanInfo.CTS.Cancel();
if (scanInfo.ScanTask != null)
await scanInfo.ScanTask;
scanInfo.CTS.Dispose();
}
return true;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to stop scan");
return false;
}
}
public async Task<bool> SetFrequency(int frequency)
{
try
{
if (frequency < 1 || frequency > 1000)
return false;
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var key = board.ID.ToString();
if (_scanTasks.TryGetValue(key, out var scanInfo) && scanInfo.IsRunning)
{
scanInfo.Frequency = frequency;
return true;
}
else
{
logger.Warn($"SetFrequency called but no running scan for board {board.ID} and client {Context.ConnectionId}");
return false;
}
}
catch (Exception ex)
{
logger.Error(ex, "Failed to set frequency");
return false;
}
}
public async Task<DigitalTubeTaskStatus?> GetStatus()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var key = board.ID.ToString();
if (_scanTasks.TryGetValue(key, out var scanInfo))
{
return scanInfo.ToDigitalTubeTaskStatus();
}
else
{
return null;
}
}
catch (Exception ex)
{
logger.Error(ex, "Failed to get status");
throw new Exception("Failed to get status", ex);
}
}
}

203
server/src/Hubs/JtagHub.cs Normal file
View File

@@ -0,0 +1,203 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
using DotNext;
using System.Collections.Concurrent;
using TypedSignalR.Client;
using Tapper;
namespace server.Hubs;
[Hub]
public interface IJtagHub
{
Task<bool> SetBoundaryScanFreq(int freq);
Task<bool> StartBoundaryScan(int freq = 100);
Task<bool> StopBoundaryScan();
}
[Receiver]
public interface IJtagReceiver
{
Task OnReceiveBoundaryScanData(Dictionary<string, bool> msg);
}
[Authorize]
[EnableCors("SignalR")]
public class JtagHub : Hub<IJtagReceiver>, IJtagHub
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IHubContext<JtagHub, IJtagReceiver> _hubContext;
private readonly Database.UserManager _userManager = new();
private static ConcurrentDictionary<string, int> FreqTable = new();
private static ConcurrentDictionary<string, CancellationTokenSource> CancellationTokenSourceTable = new();
public JtagHub(IHubContext<JtagHub, IJtagReceiver> hubContext)
{
_hubContext = hubContext;
}
private Optional<Peripherals.JtagClient.Jtag> GetJtagClient(string userName)
{
try
{
var board = _userManager.GetBoardByUserName(userName);
if (!board.IsSuccessful)
{
logger.Error($"Find Board {board.Value.Value.ID} failed because {board.Error}");
return new(null);
}
if (!board.Value.HasValue)
{
logger.Error($"Board {board.Value.Value.ID} not found");
return new(null);
}
var jtag = new Peripherals.JtagClient.Jtag(board.Value.Value.IpAddr, board.Value.Value.Port);
return new(jtag);
}
catch (Exception error)
{
logger.Error(error);
return new(null);
}
}
public async Task<bool> SetBoundaryScanFreq(int freq)
{
return await Task.Run(() =>
{
try
{
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
if (userName is null)
{
logger.Error("Can't get user info");
return false;
}
FreqTable.AddOrUpdate(userName, freq, (key, value) => freq);
return true;
}
catch (Exception error)
{
logger.Error(error);
return false;
}
});
}
public async Task<bool> StartBoundaryScan(int freq = 100)
{
try
{
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
if (userName is null)
{
logger.Error("No Such User");
return false;
}
await SetBoundaryScanFreq(freq);
var cts = new CancellationTokenSource();
CancellationTokenSourceTable.AddOrUpdate(userName, cts, (key, value) => cts);
_ = Task.Run(
() => BoundaryScanLogicPorts(Context.ConnectionId, userName, cts.Token),
cts.Token)
.ContinueWith((task) =>
{
if (task.IsFaulted)
{
// 遍历所有异常
foreach (var ex in task.Exception.InnerExceptions)
{
if (ex is OperationCanceledException)
{
logger.Info($"Boundary scan operation cancelled for user {userName}");
}
else
{
logger.Error($"Boundary scan operation failed for user {userName}: {ex}");
}
}
}
else if (task.IsCanceled)
{
logger.Info($"Boundary scan operation cancelled for user {userName}");
}
else
{
logger.Info($"Boundary scan completed successfully for user {userName}");
}
});
logger.Info($"Boundary scan started for user {userName}");
return true;
}
catch (Exception error)
{
logger.Error(error);
return false;
}
}
public async Task<bool> StopBoundaryScan()
{
return await Task.Run(() =>
{
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
if (userName is null)
{
logger.Error("No Such User");
return false;
}
if (!CancellationTokenSourceTable.TryGetValue(userName, out var cts))
{
return false;
}
cts.Cancel();
cts.Token.WaitHandle.WaitOne();
logger.Info($"Boundary scan stopped for user {userName}");
return true;
});
}
private async Task BoundaryScanLogicPorts(string connectionID, string userName, CancellationToken cancellationToken)
{
var jtagCtrl = GetJtagClient(userName).OrThrow(() => new InvalidOperationException("JTAG client not found"));
var cntFail = 0;
while (true && cntFail < 5)
{
cancellationToken.ThrowIfCancellationRequested();
var ret = await jtagCtrl.BoundaryScanLogicalPorts();
if (!ret.IsSuccessful)
{
logger.Error($"User {userName} boundary scan failed for device {jtagCtrl.address}: {ret.Error}");
cntFail++;
continue;
}
await _hubContext.Clients.Client(connectionID).OnReceiveBoundaryScanData(ret.Value);
// logger.Info($"User {userName} successfully completed boundary scan for device {jtagCtrl.address}");
await Task.Delay(FreqTable.TryGetValue(userName, out var freq) ? 1000 / freq : 1000 / 100, cancellationToken);
}
if (cntFail >= 5)
{
logger.Error($"User {userName} boundary scan failed for device {jtagCtrl.address} after 5 attempts");
throw new InvalidOperationException("Boundary scan failed");
}
}
}

View File

@@ -0,0 +1,403 @@
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Cors;
using TypedSignalR.Client;
using DotNext;
using Tapper;
using System.Collections.Concurrent;
using Peripherals.OscilloscopeClient;
#pragma warning disable 1998
namespace server.Hubs;
[Hub]
public interface IOscilloscopeHub
{
Task<bool> Initialize(OscilloscopeFullConfig config);
Task<bool> StartCapture();
Task<bool> StopCapture();
Task<OscilloscopeDataResponse?> GetData();
Task<bool> SetTrigger(byte level);
Task<bool> SetRisingEdge(bool risingEdge);
Task<bool> SetSampling(ushort decimationRate);
Task<bool> SetFrequency(int frequency);
}
[Receiver]
public interface IOscilloscopeReceiver
{
Task OnDataReceived(OscilloscopeDataResponse data);
}
[TranspilationSource]
public class OscilloscopeDataResponse
{
public uint AdFrequency { get; set; }
public byte AdVpp { get; set; }
public byte AdMax { get; set; }
public byte AdMin { get; set; }
public string WaveformData { get; set; } = "";
}
[TranspilationSource]
public class OscilloscopeFullConfig
{
public bool CaptureEnabled { get; set; }
public byte TriggerLevel { get; set; }
public bool TriggerRisingEdge { get; set; }
public ushort HorizontalShift { get; set; }
public ushort DecimationRate { get; set; }
public int CaptureFrequency { get; set; }
// public bool AutoRefreshRAM { get; set; }
public OscilloscopeConfig ToOscilloscopeConfig()
{
return new OscilloscopeConfig
{
CaptureEnabled = CaptureEnabled,
TriggerLevel = TriggerLevel,
TriggerRisingEdge = TriggerRisingEdge,
HorizontalShift = HorizontalShift,
DecimationRate = DecimationRate,
};
}
}
class OscilloscopeScanTaskInfo
{
public Task? ScanTask { get; set; }
public OscilloscopeCtrl Client { get; set; }
public CancellationTokenSource CTS { get; set; } = new CancellationTokenSource();
public int Frequency { get; set; } = 100;
public OscilloscopeScanTaskInfo(OscilloscopeCtrl client)
{
Client = client;
}
}
[Authorize]
[EnableCors("SignalR")]
public class OscilloscopeHub : Hub<IOscilloscopeReceiver>, IOscilloscopeHub
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IHubContext<OscilloscopeHub, IOscilloscopeReceiver> _hubContext;
private readonly Database.UserManager _userManager = new();
private static ConcurrentDictionary<string, OscilloscopeScanTaskInfo> _scanTasks = new();
public OscilloscopeHub(IHubContext<OscilloscopeHub, IOscilloscopeReceiver> hubContext)
{
_hubContext = hubContext;
}
private Optional<Database.Board> TryGetBoard()
{
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrEmpty(userName))
{
logger.Error("User name is null or empty");
return null;
}
var boardRet = _userManager.GetBoardByUserName(userName);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
{
logger.Error($"Board not found");
return null;
}
return boardRet.Value.Value;
}
private Optional<OscilloscopeCtrl> GetOscilloscope()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var client = new OscilloscopeCtrl(board.IpAddr, board.Port);
return client;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to get oscilloscope");
return null;
}
}
public async Task<bool> Initialize(OscilloscopeFullConfig config)
{
try
{
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
var result = await client.Init(config.ToOscilloscopeConfig());
if (!result.IsSuccessful)
{
logger.Error(result.Error, "Initialize failed");
return false;
}
return result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to initialize oscilloscope");
return false;
}
}
private Task ScanTask(OscilloscopeScanTaskInfo taskInfo, string clientId)
{
var token = taskInfo.CTS.Token;
return Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
var data = await GetCaptureData(taskInfo.Client);
if (data == null)
{
logger.Error("GetData failed");
continue;
}
await _hubContext.Clients.Client(clientId).OnDataReceived(data);
await Task.Delay(1000 / taskInfo.Frequency, token);
}
}, token).ContinueWith(t =>
{
if (t.IsFaulted)
logger.Error(t.Exception, "ScanTask failed");
});
}
public async Task<bool> StartCapture()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var key = board.ID.ToString();
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
if (_scanTasks.TryGetValue(key, out var existing))
return true;
var result = await client.SetCaptureEnable(true);
if (!result.IsSuccessful)
{
logger.Error(result.Error, "StartCapture failed");
return false;
}
var scanTaskInfo = new OscilloscopeScanTaskInfo(client);
scanTaskInfo.ScanTask = ScanTask(scanTaskInfo, Context.ConnectionId);
return _scanTasks.TryAdd(key, scanTaskInfo);
}
catch (Exception ex)
{
logger.Error(ex, "Failed to start capture");
return false;
}
}
public async Task<bool> StopCapture()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
var key = board.ID.ToString();
if (_scanTasks.TryRemove(key, out var taskInfo))
{
taskInfo.CTS.Cancel();
if (taskInfo.ScanTask != null) taskInfo.ScanTask.Wait();
var result = await client.SetCaptureEnable(false);
if (!result.IsSuccessful)
{
logger.Error(result.Error, "StopCapture failed");
return false;
}
return result.Value;
}
throw new Exception("Task not found");
}
catch (Exception ex)
{
logger.Error(ex, "Failed to stop capture");
return false;
}
}
private async Task<OscilloscopeDataResponse?> GetCaptureData(OscilloscopeCtrl oscilloscope)
{
try
{
var freqResult = await oscilloscope.GetADFrequency();
var vppResult = await oscilloscope.GetADVpp();
var maxResult = await oscilloscope.GetADMax();
var minResult = await oscilloscope.GetADMin();
var waveformResult = await oscilloscope.GetWaveformData();
if (!freqResult.IsSuccessful)
{
logger.Error($"获取AD采样频率失败: {freqResult.Error}");
throw new Exception($"获取AD采样频率失败: {freqResult.Error}");
}
if (!vppResult.IsSuccessful)
{
logger.Error($"获取AD采样幅度失败: {vppResult.Error}");
throw new Exception($"获取AD采样幅度失败: {vppResult.Error}");
}
if (!maxResult.IsSuccessful)
{
logger.Error($"获取AD采样最大值失败: {maxResult.Error}");
throw new Exception($"获取AD采样最大值失败: {maxResult.Error}");
}
if (!minResult.IsSuccessful)
{
logger.Error($"获取AD采样最小值失败: {minResult.Error}");
throw new Exception($"获取AD采样最小值失败: {minResult.Error}");
}
if (!waveformResult.IsSuccessful)
{
logger.Error($"获取波形数据失败: {waveformResult.Error}");
throw new Exception($"获取波形数据失败: {waveformResult.Error}");
}
var response = new OscilloscopeDataResponse
{
AdFrequency = freqResult.Value,
AdVpp = vppResult.Value,
AdMax = maxResult.Value,
AdMin = minResult.Value,
WaveformData = Convert.ToBase64String(waveformResult.Value)
};
return new OscilloscopeDataResponse
{
AdFrequency = freqResult.Value,
AdVpp = vppResult.Value,
AdMax = maxResult.Value,
AdMin = minResult.Value,
WaveformData = Convert.ToBase64String(waveformResult.Value)
};
}
catch (Exception ex)
{
logger.Error(ex, "获取示波器数据时发生异常");
return null;
}
}
public async Task<OscilloscopeDataResponse?> GetData()
{
try
{
var oscilloscope = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
var response = await GetCaptureData(oscilloscope);
return response;
}
catch (Exception ex)
{
logger.Error(ex, "获取示波器数据时发生异常");
return null;
}
}
public async Task<bool> SetTrigger(byte level)
{
try
{
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
var ret = await client.SetTriggerLevel(level);
if (!ret.IsSuccessful)
{
logger.Error(ret.Error, "UpdateTrigger failed");
return false;
}
return ret.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to update trigger");
return false;
}
}
public async Task<bool> SetRisingEdge(bool risingEdge)
{
try
{
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
var ret = await client.SetTriggerEdge(risingEdge);
if (!ret.IsSuccessful)
{
logger.Error(ret.Error, "Update Rising Edge failed");
return false;
}
return ret.Value;
}
catch (Exception ex)
{
logger.Error(ex, "SetRisingEdge failed");
return false;
}
}
public async Task<bool> SetSampling(ushort decimationRate)
{
try
{
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
var result = await client.SetDecimationRate(decimationRate);
if (!result.IsSuccessful)
{
logger.Error(result.Error, "UpdateSampling failed");
return false;
}
return result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to update sampling");
return false;
}
}
public async Task<bool> SetFrequency(int frequency)
{
try
{
if (frequency < 1 || frequency > 1000)
return false;
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var key = board.ID.ToString();
if (_scanTasks.TryGetValue(key, out var scanInfo))
{
scanInfo.Frequency = frequency;
return true;
}
else
{
logger.Warn($"SetFrequency called but no running scan for board {board.ID} and client {Context.ConnectionId}");
return false;
}
}
catch (Exception ex)
{
logger.Error(ex, "Failed to set frequency");
return false;
}
}
}

View File

@@ -0,0 +1,77 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Cors;
using TypedSignalR.Client;
using Tapper;
using server.Services;
#pragma warning disable 1998
namespace server.Hubs;
[Hub]
public interface IProgressHub
{
Task<bool> Join(string taskId);
Task<bool> Leave(string taskId);
Task<ProgressInfo?> GetProgress(string taskId);
}
[Receiver]
public interface IProgressReceiver
{
Task OnReceiveProgress(ProgressInfo message);
}
[TranspilationSource]
public enum ProgressStatus
{
Running,
Completed,
Canceled,
Failed
}
[TranspilationSource]
public class ProgressInfo
{
public required string TaskId { get; set; }
public required ProgressStatus Status { get; set; }
public required double ProgressPercent { get; set; }
public required string ErrorMessage { get; set; }
};
[Authorize]
[EnableCors("SignalR")]
public class ProgressHub : Hub<IProgressReceiver>, IProgressHub
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly ProgressTracker _progressTracker = MsgBus.ProgressTracker;
public async Task<bool> Join(string taskId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, taskId);
// 发送当前状态(如果存在)
var task = _progressTracker.GetTask(taskId);
if (task != null)
{
await Clients.Caller.OnReceiveProgress(task.Value.ToProgressInfo());
}
logger.Info($"Client {Context.ConnectionId} joined task {taskId}");
return true;
}
public async Task<bool> Leave(string taskId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, taskId);
logger.Info($"Client {Context.ConnectionId} left task {taskId}");
return true;
}
public async Task<ProgressInfo?> GetProgress(string taskId)
{
return _progressTracker.GetTask(taskId)?.ToProgressInfo();
}
}

View File

@@ -0,0 +1,268 @@
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Cors;
using TypedSignalR.Client;
using DotNext;
using Peripherals.RotaryEncoderClient;
using System.Collections.Concurrent;
#pragma warning disable 1998
namespace server.Hubs;
[Hub]
public interface IRotaryEncoderHub
{
Task<bool> SetEnable(bool enable);
Task<bool> RotateEncoderOnce(int num, RotaryEncoderDirection direction);
Task<bool> PressEncoderOnce(int num, RotaryEncoderPressStatus press);
Task<bool> EnableCycleRotateEncoder(int num, RotaryEncoderDirection direction, int freq);
Task<bool> DisableCycleRotateEncoder();
}
[Receiver]
public interface IRotaryEncoderReceiver
{
Task OnReceiveRotate(int num, RotaryEncoderDirection direction);
}
public class CycleTaskInfo
{
public Task? CycleTask { get; set; }
public RotaryEncoderCtrl EncoderClient { get; set; }
public CancellationTokenSource CTS { get; set; } = new();
public int Freq { get; set; }
public int Num { get; set; }
public RotaryEncoderDirection Direction { get; set; }
public CycleTaskInfo(
RotaryEncoderCtrl client,
int num, int freq,
RotaryEncoderDirection direction)
{
EncoderClient = client;
Num = num;
Direction = direction;
Freq = freq;
}
}
[Authorize]
[EnableCors("SignalR")]
public class RotaryEncoderHub : Hub<IRotaryEncoderReceiver>, IRotaryEncoderHub
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IHubContext<RotaryEncoderHub, IRotaryEncoderReceiver> _hubContext;
private readonly Database.UserManager _userManager = new();
private ConcurrentDictionary<(string, string), CycleTaskInfo> _cycleTasks = new();
public RotaryEncoderHub(IHubContext<RotaryEncoderHub, IRotaryEncoderReceiver> hubContext)
{
_hubContext = hubContext;
}
private Optional<Database.Board> TryGetBoard()
{
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrEmpty(userName))
{
logger.Error("User name is null or empty");
return null;
}
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
{
logger.Error($"User '{userName}' not found");
return null;
}
var user = userRet.Value.Value;
var boardRet = _userManager.GetBoardByID(user.BoardID);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
{
logger.Error($"Board not found");
return null;
}
return boardRet.Value.Value;
}
public async Task<bool> SetEnable(bool enable)
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var encoderCtrl = new RotaryEncoderCtrl(board.IpAddr, board.Port, 0);
var result = await encoderCtrl.SetEnable(enable);
if (!result.IsSuccessful)
{
logger.Error(result.Error, "SetEnable failed");
return false;
}
return result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to set enable");
return false;
}
}
public async Task<bool> RotateEncoderOnce(int num, RotaryEncoderDirection direction)
{
try
{
if (num <= 0 || num > 4)
throw new ArgumentException($"RotaryEncoder num should be 1~3, instead of {num}");
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var encoderCtrl = new RotaryEncoderCtrl(board.IpAddr, board.Port, 0);
var result = await encoderCtrl.RotateEncoderOnce(num, direction);
if (!result.IsSuccessful)
{
logger.Error(result.Error, $"RotateEncoderOnce({num}, {direction}) failed");
return false;
}
return result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to rotate encoder once");
return false;
}
}
public async Task<bool> PressEncoderOnce(int num, RotaryEncoderPressStatus press)
{
try
{
if (num <= 0 || num > 4)
throw new ArgumentException($"RotaryEncoder num should be 1~3, instead of {num}");
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var encoderCtrl = new RotaryEncoderCtrl(board.IpAddr, board.Port, 0);
var result = await encoderCtrl.PressEncoderOnce(num, press);
if (!result.IsSuccessful)
{
logger.Error(result.Error, $"RotateEncoderOnce({num}, {press}) failed");
return false;
}
return result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to rotate encoder once");
return false;
}
}
public async Task<bool> EnableCycleRotateEncoder(int num, RotaryEncoderDirection direction, int freq)
{
try
{
if (num <= 0 || num > 4) throw new ArgumentException(
$"RotaryEncoder num should be 1~3, instead of {num}");
if (freq <= 0 || freq > 1000) throw new ArgumentException(
$"Frequency should be between 1 and 1000, instead of {freq}");
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var key = (board.ID.ToString(), Context.ConnectionId);
if (_cycleTasks.TryGetValue(key, out var existing))
await DisableCycleRotateEncoder();
var cts = new CancellationTokenSource();
var encoderCtrl = new RotaryEncoderCtrl(board.IpAddr, board.Port, 0);
var cycleTaskInfo = new CycleTaskInfo(encoderCtrl, num, freq, direction);
cycleTaskInfo.CycleTask = CycleRotate(cycleTaskInfo, Context.ConnectionId, board.ID.ToString());
_cycleTasks[key] = cycleTaskInfo;
return true;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to enable cycle rotate encoder");
return false;
}
}
public async Task<bool> DisableCycleRotateEncoder()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var key = (board.ID.ToString(), Context.ConnectionId);
if (_cycleTasks.TryRemove(key, out var taskInfo))
{
taskInfo.CTS.Cancel();
if (taskInfo.CycleTask != null)
await taskInfo.CycleTask;
taskInfo.CTS.Dispose();
}
return true;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to disable cycle rotate encoder");
return false;
}
}
private Task CycleRotate(CycleTaskInfo taskInfo, string clientId, string boardId)
{
var ctrl = taskInfo.EncoderClient;
var token = taskInfo.CTS.Token;
return Task.Run(async () =>
{
var cntError = 0;
while (!token.IsCancellationRequested)
{
var ret = await ctrl.RotateEncoderOnce(taskInfo.Num, taskInfo.Direction);
if (!ret.IsSuccessful)
{
logger.Error(
$"Failed to rotate encoder {taskInfo.Num} on board {boardId}: {ret.Error}");
cntError++;
if (cntError >= 3)
{
logger.Error(
$"Too many errors occurred while rotating encoder {taskInfo.Num} on board {boardId}");
break;
}
}
if (!ret.Value)
{
logger.Warn(
$"Encoder {taskInfo.Num} on board {boardId} is not responding");
continue;
}
await _hubContext.Clients
.Client(clientId)
.OnReceiveRotate(taskInfo.Num, taskInfo.Direction);
await Task.Delay(1000 / taskInfo.Freq, token);
}
}, token)
.ContinueWith((task) =>
{
if (task.IsFaulted)
{
logger.Error($"Rotary encoder cycle operation failed: {task.Exception}");
}
else if (task.IsCanceled)
{
logger.Info($"Rotary encoder cycle operation cancelled for board {boardId}");
}
else
{
logger.Info($"Rotary encoder cycle completed for board {boardId}");
}
});
}
}

View File

@@ -0,0 +1,138 @@
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Cors;
using TypedSignalR.Client;
using Tapper;
using DotNext;
using Peripherals.WS2812Client;
using System.Collections.Concurrent;
#pragma warning disable 1998
namespace server.Hubs;
[Hub]
public interface IWS2812Hub
{
Task<RGBColor[]?> GetAllLedColors();
Task<RGBColor?> GetLedColor(int ledIndex);
}
[Receiver]
public interface IWS2812Receiver
{
Task OnReceive(RGBColor[] data);
}
[TranspilationSource]
public class WS2812TaskStatus
{
public bool IsRunning { get; set; } = false;
}
class WS2812ScanTaskInfo
{
public string BoardID { get; set; }
public string ClientID { get; set; }
public Task? ScanTask { get; set; }
public WS2812Client LedClient { get; set; }
public CancellationTokenSource CTS { get; set; } = new();
public bool IsRunning { get; set; } = false;
public WS2812ScanTaskInfo(string boardID, string clientID, WS2812Client client)
{
BoardID = boardID;
ClientID = clientID;
LedClient = client;
}
public WS2812TaskStatus ToWS2812TaskStatus()
{
return new WS2812TaskStatus
{
IsRunning = IsRunning
};
}
}
[Authorize]
[EnableCors("SignalR")]
public class WS2812Hub : Hub<IWS2812Receiver>, IWS2812Hub
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IHubContext<WS2812Hub, IWS2812Receiver> _hubContext;
private readonly Database.UserManager _userManager = new();
private ConcurrentDictionary<(string, string), WS2812ScanTaskInfo> _scanTasks = new();
public WS2812Hub(IHubContext<WS2812Hub, IWS2812Receiver> hubContext)
{
_hubContext = hubContext;
}
private Optional<Database.Board> TryGetBoard()
{
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
if (string.IsNullOrEmpty(userName))
{
logger.Error("User name is null or empty");
return null;
}
var userRet = _userManager.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
{
logger.Error($"User '{userName}' not found");
return null;
}
var user = userRet.Value.Value;
var boardRet = _userManager.GetBoardByID(user.BoardID);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
{
logger.Error($"Board not found");
return null;
}
return boardRet.Value.Value;
}
public async Task<RGBColor[]?> GetAllLedColors()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var client = new WS2812Client(board.IpAddr, board.Port, 0);
var result = await client.GetAllLedColors();
if (!result.IsSuccessful)
{
logger.Error($"GetAllLedColors failed: {result.Error}");
return null;
}
return result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to get all LED colors");
return null;
}
}
public async Task<RGBColor?> GetLedColor(int ledIndex)
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var client = new WS2812Client(board.IpAddr, board.Port, 0);
var result = await client.GetLedColor(ledIndex);
if (!result.IsSuccessful)
{
logger.Error($"GetLedColor failed: {result.Error}");
return null;
}
return result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to get LED color");
return null;
}
}
}

View File

@@ -1,27 +1,68 @@
using server.Services;
/// <summary>
/// 多线程通信总线
/// </summary>
public static class MsgBus
public sealed class MsgBus
{
private static readonly UDPServer udpServer = new UDPServer(1234);
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
// private static RtspStreamService _rtspStreamService = new RtspStreamService(new UsbCameraCapture());
private static readonly UDPServer udpServer = new UDPServer(1234, 12);
/// <summary>
/// 获取UDP服务器
/// </summary>
public static UDPServer UDPServer { get { return udpServer; } }
// 添加静态ProgressTracker引用
private static ProgressTracker? _progressTracker;
/// <summary>
/// 设置全局ProgressTracker实例
/// </summary>
public static void SetProgressTracker(ProgressTracker progressTracker)
{
_progressTracker = progressTracker;
}
public static ProgressTracker ProgressTracker
{
get
{
if (_progressTracker == null)
{
throw new InvalidOperationException("ProgressTracker is not set.");
}
return _progressTracker;
}
}
private static bool isRunning = false;
/// <summary>
/// 获取通信总线运行状态
/// </summary>
public static bool IsRunning { get { return isRunning; } }
private MsgBus() { }
static MsgBus() { }
/// <summary>
/// 通信总线初始化
/// </summary>
/// <returns>无</returns>
public static void Init()
public static async void Init()
{
if (!ArpClient.IsAdministrator())
{
logger.Error($"非管理员运行ARP无法更新请用管理员权限运行");
// throw new Exception($"非管理员运行ARP无法更新请用管理员权限运行");
}
udpServer.Start();
// _rtspStreamService.ConfigureVideo(1920, 1080, 30);
// await _rtspStreamService.StartAsync();
isRunning = true;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
# CommandID
示波器12
逻辑分析仪: 11
Jtag: 10
矩阵键盘1
HDMI9
Camera: 8
Debugger: 7
七段数码港6

View File

@@ -108,11 +108,11 @@ public class DDS
if (waveNum < 0 || waveNum > 3) return new(new ArgumentException(
$"Wave number should be 0 ~ 3 instead of {waveNum}", nameof(waveNum)));
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, 1);
logger.Trace("Clear udp data finished");
var ret = await UDPClientPool.WriteAddr(
this.ep, DDSAddr.Channel[channelNum].WaveSelect, (UInt32)waveNum, this.timeout);
this.ep, 1, DDSAddr.Channel[channelNum].WaveSelect, (UInt32)waveNum, this.timeout);
if (!ret.IsSuccessful)
return new(ret.Error);
return ret.Value;
@@ -132,11 +132,11 @@ public class DDS
if (waveNum < 0 || waveNum > 3) return new(new ArgumentException(
$"Wave number should be 0 ~ 3 instead of {waveNum}", nameof(waveNum)));
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, 1);
logger.Trace("Clear udp data finished");
var ret = await UDPClientPool.WriteAddr(
this.ep, DDSAddr.Channel[channelNum].FreqCtrl[waveNum], step, this.timeout);
this.ep, 1, DDSAddr.Channel[channelNum].FreqCtrl[waveNum], step, this.timeout);
if (!ret.IsSuccessful)
return new(ret.Error);
return ret.Value;
@@ -158,11 +158,11 @@ public class DDS
if (phase < 0 || phase > 4096) return new(new ArgumentException(
$"Phase should be 0 ~ 4096 instead of {phase}", nameof(phase)));
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, 1);
logger.Trace("Clear udp data finished");
var ret = await UDPClientPool.WriteAddr(
this.ep, DDSAddr.Channel[channelNum].PhaseCtrl[waveNum], (UInt32)phase, this.timeout);
this.ep, 1, DDSAddr.Channel[channelNum].PhaseCtrl[waveNum], (UInt32)phase, this.timeout);
if (!ret.IsSuccessful)
return new(ret.Error);
return ret.Value;

View File

@@ -0,0 +1,254 @@
using System.Net;
using DotNext;
namespace Peripherals.DebuggerClient;
/// <summary>
/// FPGA调试器的内存地址映射常量
/// </summary>
class DebuggerAddr
{
/// <summary>
/// 触发器启动地址
/// </summary>
public const UInt32 Start = 0x5100_0000;
/// <summary>
/// 刷新操作地址
/// </summary>
public const UInt32 Fresh = 0x5100_FFFF;
/// <summary>
/// 信号标志读取地址
/// </summary>
public const UInt32 Signal = 0x5000_0001;
/// <summary>
/// 数据读取基地址
/// </summary>
public const UInt32 Data = 0x5100_0000;
/// <summary>
/// 捕获模式设置地址
/// </summary>
public const UInt32 Mode = 0x5101_0000;
}
/// <summary>
/// FPGA调试器命令常量
/// </summary>
class DebuggerCmd
{
/// <summary>
/// 启动触发器命令
/// </summary>
public const UInt32 Start = 0xFFFF_FFFF;
/// <summary>
/// 刷新命令
/// </summary>
public const UInt32 Fresh = 0x0000_0000;
/// <summary>
/// 清除信号标志命令
/// </summary>
public const UInt32 ClearSignal = 0xFFFF_FFFF;
}
/// <summary>
/// 信号捕获模式枚举
/// </summary>
public enum CaptureMode : byte
{
/// <summary>
/// 无捕获模式
/// </summary>
None = 0,
/// <summary>
/// 低电平触发模式
/// </summary>
Logic0 = 1,
/// <summary>
/// 高电平触发模式
/// </summary>
Logic1 = 2,
/// <summary>
/// 上升沿触发模式
/// </summary>
Rise = 3,
/// <summary>
/// 下降沿触发模式
/// </summary>
Fall = 4,
}
/// <summary>
/// FPGA调试器客户端用于通过UDP协议与FPGA调试器进行通信
/// </summary>
public class DebuggerClient
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
private UInt32 captureDataAddr = 0x5100_0000;
/// <summary>
/// 初始化FPGA调试器客户端
/// </summary>
/// <param name="address">FPGA设备的IP地址</param>
/// <param name="port">通信端口号</param>
/// <param name="taskID">任务标识符</param>
/// <param name="timeout">通信超时时间毫秒默认2000ms</param>
/// <exception cref="ArgumentException">当timeout为负数时抛出</exception>
public DebuggerClient(string address, int port, int taskID, int timeout = 2000)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.taskID = taskID;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
/// <summary>
/// 设置信号捕获模式
/// </summary>
/// <param name="wireNum">要设置的线</param>
/// <param name="mode">要设置的捕获模式</param>
/// <returns>操作结果成功返回true失败返回错误信息</returns>
public async ValueTask<Result<bool>> SetMode(UInt32 wireNum, CaptureMode mode)
{
if (wireNum > 512)
{
return new(new ArgumentException($"Wire Num can't be over 512, but receive num: {wireNum}"));
}
UInt32 data = ((UInt32)mode);
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Mode + wireNum, data, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set mode: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to SetMode returned false");
return new(new Exception("Failed to set mode"));
}
return true;
}
/// <summary>
/// 启动信号触发器开始捕获
/// </summary>
/// <returns>操作结果成功返回true失败返回错误信息</returns>
public async ValueTask<Result<bool>> StartTrigger()
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Start, DebuggerCmd.Start, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to start trigger: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to StartTrigger returned false");
return new(new Exception("Failed to start trigger"));
}
return true;
}
/// <summary>
/// 读取触发器状态标志
/// </summary>
/// <returns>操作结果,成功返回状态标志字节,失败返回错误信息</returns>
public async ValueTask<Result<byte>> ReadFlag()
{
var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, DebuggerAddr.Signal, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read flag: {ret.Error}");
return new(ret.Error);
}
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 1)
{
logger.Error("ReadAddr returned invalid data for flag");
return new(new Exception("Failed to read flag"));
}
return ret.Value.Options.Data[3];
}
/// <summary>
/// 清除触发器状态标志
/// </summary>
/// <returns>操作结果成功返回true失败返回错误信息</returns>
public async ValueTask<Result<bool>> ClearFlag()
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Signal, DebuggerCmd.ClearSignal, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to clear flag: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to ClearFlag returned false");
return new(new Exception("Failed to clear flag"));
}
return true;
}
/// <summary>
/// 从指定偏移地址读取捕获的数据
/// </summary>
/// <param name="portNum">Port数量</param>
/// <returns>操作结果,成功返回捕获数据,失败返回错误信息</returns>
public async ValueTask<Result<byte[]>> ReadData(UInt32 portNum)
{
var captureData = new byte[1024 * 4 * portNum];
{
var ret = await UDPClientPool.ReadAddr4Bytes(this.ep, this.taskID, this.captureDataAddr, captureData.Length / 4, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read data: {ret.Error}");
return new(ret.Error);
}
if (ret.Value.Length != captureData.Length)
{
logger.Error($"Receive capture data length should be {captureData.Length} instead of {ret.Value.Length}");
return new(new Exception($"Receive capture data length should be {captureData.Length} instead of {ret.Value.Length}"));
}
Buffer.BlockCopy(ret.Value, 0, captureData, 0, captureData.Length);
}
return captureData;
}
/// <summary>
/// 刷新调试器状态,重置内部状态机
/// </summary>
/// <returns>操作结果成功返回true失败返回错误信息</returns>
public async ValueTask<Result<bool>> Refresh()
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Fresh, DebuggerCmd.Fresh, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to refresh: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to Refresh returned false");
return new(new Exception("Failed to refresh"));
}
return true;
}
}

View File

@@ -0,0 +1,295 @@
using System.Net;
using DotNext;
using WebProtocol;
namespace Peripherals.HdmiInClient;
static class HdmiInAddr
{
public const UInt32 BASE = 0xA000_0000;
public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0;
public const UInt32 START_WR_ADDR0 = BASE + 0x20;
public const UInt32 END_WR_ADDR0 = BASE + 0x21;
public const UInt32 HDMI_NOT_READY = BASE + 0x26;
public const UInt32 HDMI_HEIGHT_WIDTH = BASE + 0x27;
public const UInt32 CAPTURE_HEIGHT_WIDTH = BASE + 0x28;
public const UInt32 ADDR_HDMI_WD_START = 0x0400_0000;
}
public class HdmiIn
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 500;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
public int Width { get; private set; }
public int Height { get; private set; }
public int FrameLength => Width * Height / 2;
/// <summary>
/// 初始化HDMI输入客户端
/// </summary>
/// <param name="address">HDMI输入设备IP地址</param>
/// <param name="port">HDMI输入设备端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="timeout">超时时间(毫秒)</param>
public HdmiIn(string address, int port, int taskID, int timeout = 500)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.taskID = taskID;
this.timeout = timeout;
}
public async ValueTask<Result<bool>> Init(bool enable = true)
{
{
var ret = await CheckHdmiIsReady();
if (!ret.IsSuccessful)
{
logger.Error($"Failed to check HDMI ready: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("HDMI not ready");
return new(false);
}
}
int width = -1, height = -1;
{
var ret = await GetHdmiResolution();
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get HDMI resolution: {ret.Error}");
return new(ret.Error);
}
(width, height) = ret.Value;
}
{
var ret = await ConnectJpeg2Hdmi(width, height);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to connect JPEG to HDMI: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("Failed to connect JPEG to HDMI");
return false;
}
}
if (enable) return await SetTransEnable(true);
else return true;
}
public async ValueTask<Result<bool>> SetTransEnable(bool isEnable)
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, HdmiInAddr.CAPTURE_RD_CTRL, (isEnable ? 0x00000001u : 0x00000000u));
if (!ret.IsSuccessful)
{
logger.Error($"Failed to write HdmiIn_CTRL to HdmiIn at {this.address}:{this.port}, error: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"HdmiIn_CTRL write returned false for HdmiIn at {this.address}:{this.port}");
return false;
}
return true;
}
/// <summary>
/// 读取一帧图像数据
/// </summary>
/// <returns>包含图像数据的字节数组</returns>
public async ValueTask<Result<byte[]>> ReadFrame()
{
// 只在第一次或出错时清除UDP缓冲区避免每帧都清除造成延迟
// MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Reading frame from HdmiIn {this.address}");
// 使用UDPClientPool读取图像帧数据
var result = await UDPClientPool.ReadAddr4BytesAsync(
this.ep,
this.taskID, // taskID
HdmiInAddr.ADDR_HDMI_WD_START,
FrameLength, // 使用当前分辨率的动态大小
BurstType.ExtendBurst,
this.timeout);
if (!result.IsSuccessful)
{
logger.Error($"Failed to read frame from HdmiIn {this.address}:{this.port}, error: {result.Error}");
// 读取失败时清除缓冲区,为下次读取做准备
try
{
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
}
catch (Exception ex)
{
logger.Warn($"Failed to clear UDP data after read error: {ex.Message}");
}
return new(result.Error);
}
logger.Trace($"Successfully read frame from HdmiIn {this.address}:{this.port}, data length: {result.Value.Length} bytes");
return result.Value;
}
public async ValueTask<Optional<(byte[] header, byte[] data, byte[] footer)>> GetMJpegFrame()
{
// 从HDMI读取RGB24数据
var readStartTime = DateTime.UtcNow;
var frameResult = await ReadFrame();
var readEndTime = DateTime.UtcNow;
var readTime = (readEndTime - readStartTime).TotalMilliseconds;
if (!frameResult.IsSuccessful || frameResult.Value == null)
{
logger.Warn("HDMI帧读取失败或为空");
return Optional<(byte[] header, byte[] data, byte[] footer)>.None;
}
var rgb565Data = frameResult.Value;
// 验证数据长度是否正确 (RGB24为每像素2字节)
var expectedLength = Width * Height * 2;
if (rgb565Data.Length != expectedLength)
{
logger.Warn("HDMI数据长度不匹配期望: {Expected}, 实际: {Actual}",
expectedLength, rgb565Data.Length);
}
// 将RGB24转换为JPEG参考Camera版本的处理
var jpegResult = Common.Image.ConvertRGB565ToJpeg(rgb565Data, Width, Height, 80, false);
if (!jpegResult.IsSuccessful)
{
logger.Error("HDMI RGB565转JPEG失败: {Error}", jpegResult.Error);
return Optional<(byte[] header, byte[] data, byte[] footer)>.None;
}
var jpegData = jpegResult.Value;
var mjpegFrameHeader = Common.Image.CreateMjpegFrameHeader(jpegData.Length);
var mjpegFrameFooter = Common.Image.CreateMjpegFrameFooter();
return (mjpegFrameHeader, jpegData, mjpegFrameFooter);
}
public async ValueTask<Result<bool>> CheckHdmiIsReady()
{
var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, this.taskID, HdmiInAddr.HDMI_NOT_READY, 0b00, 0b01, 100, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to check HDMI status: {ret.Error}");
return new(ret.Error);
}
return ret.Value;
}
public async ValueTask<Result<(int, int)>> GetHdmiResolution()
{
var ret = await UDPClientPool.ReadAddrByte(
this.ep, this.taskID, HdmiInAddr.HDMI_HEIGHT_WIDTH, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get HDMI resolution: {ret.Error}");
return new(ret.Error);
}
var data = ret.Value.Options.Data;
if (data == null || data.Length != 4)
{
logger.Error($"Invalid HDMI resolution data length: {data?.Length ?? 0}");
return new(new Exception("Invalid HDMI resolution data length"));
}
var width = (data[3] | (data[2] << 8)) - 1 - (((data[3] | (data[2] << 8)) - 1)%2);
var height = (data[1] | (data[0] << 8)) - 1 - (((data[1] | (data[0] << 8)) - 1)%2);
this.Width = width;
this.Height = height;
logger.Info($"HDMI resolution: {width}x{height}");
return new((width, height));
}
public async ValueTask<Result<bool>> ConnectJpeg2Hdmi(int width, int height)
{
if (width <= 0 || height <= 0)
{
logger.Error($"Invalid HDMI resolution: {width}x{height}");
return new(new ArgumentException("Invalid HDMI resolution"));
}
var frameSize = (UInt32)(width * height) / 2;
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, HdmiInAddr.CAPTURE_HEIGHT_WIDTH, (uint)((height << 16) + width), this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set CAPTURE_HEIGHT_WIDTH: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set HDMI output start address");
return false;
}
}
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, HdmiInAddr.START_WR_ADDR0, HdmiInAddr.ADDR_HDMI_WD_START, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set HDMI output start address: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set HDMI output start address");
return false;
}
}
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, HdmiInAddr.END_WR_ADDR0,
HdmiInAddr.ADDR_HDMI_WD_START + frameSize - 1, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set HDMI output end address: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set HDMI output address");
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,315 @@
using System.Net;
using DotNext;
namespace Peripherals.I2cClient;
static class I2cAddr
{
const UInt32 Base = 0x6000_0000;
/// <summary>
/// 0x0000_0000:
/// [7:0] 本次传输的i2c地址(最高位总为0);
/// [8] 1为读0为写;
/// [16] 1为SCCB协议0为I2C协议;
/// [24] 1为开启本次传输自动置零
/// </summary>
public const UInt32 BaseConfig = Base + 0x0000_0000;
/// <summary>
/// 0x0000_0001:
/// [15:0] 本次传输的数据量以字节为单位0为传1个字节;
/// [31:16] 若本次传输为读的DUMMY数据量字节为单位0为传1个字节
/// </summary>
public const UInt32 TranConfig = Base + 0x0000_0001;
/// <summary>
/// 0x0000_0002: [0] cmd_done; [8] cmd_error;
/// </summary>
public const UInt32 Flag = Base + 0x0000_0002;
/// <summary>
/// 0x0000_0003: FIFO写入口仅低8位有效只写
/// </summary>
public const UInt32 Write = Base + 0x0000_0003;
/// <summary>
/// 0x0000_0004: FIFO读出口仅低8位有效只读
/// </summary>
public const UInt32 Read = Base + 0x0000_0004;
/// <summary>
/// 0x0000_0005: [0] FIFO写入口清空[8] FIFO读出口清空
/// </summary>
public const UInt32 Clear = Base + 0x0000_0005;
}
/// <summary>
/// [TODO:Enum]
/// </summary>
public enum I2cProtocol
{
/// <summary>
/// [TODO:Enum]
/// </summary>
I2c = 0,
/// <summary>
/// [TODO:Enum]
/// </summary>
SCCB = 1
}
/// <summary>
/// [TODO:description]
/// </summary>
public class I2c
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="taskID">[TODO:parameter]</param>
/// <param name="timeout">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public I2c(string address, int port, int taskID, int timeout = 2000)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.taskID = taskID;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
/// <summary>
/// 向指定I2C设备写入数据
/// </summary>
/// <param name="devAddr">I2C设备地址</param>
/// <param name="data">要写入的数据</param>
/// <param name="proto">I2C协议类型</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> WriteData(UInt32 devAddr, byte[] data, I2cProtocol proto)
{
if (data.Length > 0x0000_FFFF)
{
logger.Error($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF");
return new(new ArgumentException($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF"));
}
// 清除UDP服务器接收缓冲区
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Clear up udp server {this.address} receive data");
// 写入数据到I2C FIFO写入口
{
var i2cData = new byte[data.Length * 4];
int i = 0;
foreach (var item in data)
{
i2cData[i++] = 0x00;
i2cData[i++] = 0x00;
i2cData[i++] = 0x00;
i2cData[i++] = item;
}
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, I2cAddr.Write, i2cData);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to write data to I2C FIFO: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to I2C FIFO returned false");
return new(new Exception("Failed to write data to I2C FIFO"));
}
}
// 配置本次传输数据量
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, I2cAddr.TranConfig, ((uint)(data.Length - 1)));
if (!ret.IsSuccessful)
{
logger.Error($"Failed to configure transfer length: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to TranConfig returned false");
return new(new Exception("Failed to configure transfer length"));
}
}
// 配置I2C地址、协议及启动传输
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, I2cAddr.BaseConfig, (devAddr) | (((uint)proto) << 16) | (1 << 24));
if (!ret.IsSuccessful)
{
logger.Error($"Failed to configure I2C address/protocol/start: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to BaseConfig returned false");
return new(new Exception("Failed to configure I2C address/protocol/start"));
}
}
// 等待I2C命令完成
{
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, this.taskID, I2cAddr.Flag, 0x0000_0001, 0xFFFF_FFFF, 10);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to wait for I2C command completion: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("ReadAddrWithWait for I2C command completion returned false");
return new(new Exception("I2C command did not complete successfully"));
}
}
return true;
}
/// <summary>
/// 从指定I2C设备读取数据
/// </summary>
/// <param name="devAddr">I2C设备地址</param>
/// <param name="data">要写入的数据dummy数据</param>
/// <param name="dataReadLength">要读取的数据长度</param>
/// <param name="proto">I2C协议类型</param>
/// <returns>操作结果,成功返回读取到的数据,否则返回异常信息</returns>
public async ValueTask<Result<byte[]>> ReadData(UInt32 devAddr, byte[] data, int dataReadLength, I2cProtocol proto)
{
if (dataReadLength < 1 || dataReadLength > 0x0000_FFFF)
{
logger.Error($"Read length {dataReadLength} is invalid or exceeds maximum allowed 0x0000_FFFF");
return new(new ArgumentException($"Read length {dataReadLength} is invalid or exceeds maximum allowed 0x0000_FFFF"));
}
if (data.Length > 0x0000_FFFF)
{
logger.Error($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF");
return new(new ArgumentException($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF"));
}
// 清除UDP服务器接收缓冲区
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Clear up udp server {this.address} receive data");
// 配置写FIFO内容内容为data[]
{
var i2cData = new byte[data.Length * 4];
int i = 0;
foreach (var item in data)
{
i2cData[i++] = 0x00;
i2cData[i++] = 0x00;
i2cData[i++] = 0x00;
i2cData[i++] = item;
}
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, I2cAddr.Write, i2cData);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to write data to I2C FIFO: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to I2C FIFO returned false");
return new(new Exception("Failed to write data to I2C FIFO"));
}
}
// 配置本次传输数据量:[15:0]为读长度length-1[31:16]为dummy长度data.Length-1
{
uint tranConfig = ((uint)(dataReadLength - 1)) | (((uint)(data.Length - 1)) << 16);
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, I2cAddr.TranConfig, tranConfig);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to configure transfer length: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to TranConfig returned false");
return new(new Exception("Failed to configure transfer length"));
}
}
// 配置I2C地址、协议及启动传输读操作
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, I2cAddr.BaseConfig, (devAddr) | (1 << 8) | (((uint)proto) << 16) | (1 << 24));
if (!ret.IsSuccessful)
{
logger.Error($"Failed to configure I2C address/protocol/start: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to BaseConfig returned false");
return new(new Exception("Failed to configure I2C address/protocol/start"));
}
}
// 等待I2C命令完成
{
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, this.taskID, I2cAddr.Flag, 0x0000_0001, 0xFFFF_FFFF, 10);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to wait for I2C command completion: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("ReadAddrWithWait for I2C command completion returned false");
return new(new Exception("I2C command did not complete successfully"));
}
}
// 读取数据
{
var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, I2cAddr.Read);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read data from I2C FIFO: {ret.Error}");
return new(ret.Error);
}
if (ret.Value.Options.Data == null)
{
logger.Error($"ReadAddr returned unexpected data length: {ret.Value.Options.Data?.Length ?? 0}");
return new(new Exception("Failed to read expected amount of data from I2C FIFO"));
}
return ret.Value.Options.Data[3..]; // 返回读取到的数据跳过前3个字节
}
}
}

View File

@@ -0,0 +1,608 @@
using System.Net;
using DotNext;
using Common;
namespace Peripherals.JpegClient;
static class JpegAddr
{
const UInt32 BASE = 0xA000_0000;
public const UInt32 CAPTURE_RD_CTRL = BASE + 0x0;
public const UInt32 CAPTURE_WR_CTRL = BASE + 0x1;
public const UInt32 START_WR_ADDR0 = BASE + 0x2;
public const UInt32 END_WR_ADDR0 = BASE + 0x3;
public const UInt32 START_WR_ADDR1 = BASE + 0x4;
public const UInt32 END_WR_ADDR1 = BASE + 0x5;
public const UInt32 START_RD_ADDR0 = BASE + 0x6;
public const UInt32 END_RD_ADDR0 = BASE + 0x7;
public const UInt32 HDMI_NOT_READY = BASE + 0x8;
public const UInt32 HDMI_HEIGHT_WIDTH = BASE + 0x9;
public const UInt32 JPEG_HEIGHT_WIDTH = BASE + 0xA;
public const UInt32 JPEG_ADD_NEED_FRAME_NUM = BASE + 0xB;
public const UInt32 JPEG_FRAME_SAVE_NUM = BASE + 0xC;
public const UInt32 JPEG_FIFO_FRAME_INFO = BASE + 0xD;
public const UInt32 JPEG_QUANTIZATION_TABLE = BASE + 0x100;
public const UInt32 ADDR_HDMI_WD_START = 0x0400_0000;
public const UInt32 ADDR_JPEG_START = 0x0800_0000;
public const UInt32 ADDR_JPEG_END = 0x09FF_FFFF;
}
public class JpegInfo
{
public UInt32 Width { get; set; }
public UInt32 Height { get; set; }
public UInt32 Size { get; set; }
public JpegInfo(UInt32 width, UInt32 height, UInt32 size)
{
Width = width;
Height = height;
Size = size;
}
public JpegInfo(byte[] data)
{
if (data.Length < 8)
throw new ArgumentException("Invalid data length", nameof(data));
Width = ((UInt32)(data[5] << 8 + data[6] & 0xF0));
Height = ((UInt32)((data[6] & 0x0F) << 4 + data[7]));
Size = Number.BytesToUInt32(data, 0, 4).Value;
}
}
public enum JpegSampleRate : UInt32
{
RATE_1_1 = 0b1111_1111_1111_1111_1111_1111_1111_1111,
RATE_1_2 = 0b1010_1010_1010_1010_1010_1010_1010_1010,
RATE_1_4 = 0b1000_1000_1000_1000_1000_1000_1000_1000,
RATE_3_4 = 0b1110_1110_1110_1110_1110_1110_1110_1110,
RATE_1_8 = 0b1000_0000_1000_0000_1000_0000_1000_0000,
RATE_3_8 = 0b1001_0010_0100_1001_1001_0010_0100_1001,
RATE_7_8 = 0b1111_1110_1111_1110_1111_1110_1111_1110,
RATE_1_16 = 0b1000_0000_0000_0000_1000_0000_0000_0000,
RATE_3_16 = 0b1000_0100_0010_0000_1000_0100_0010_0000,
RATE_5_16 = 0b1001_0001_0010_0010_0100_0100_1000_1001,
RATE_15_16 = 0b1111_1111_1111_1110_1111_1111_1111_1110,
RATE_1_32 = 0b1000_0000_0000_0000_0000_0000_0000_0000,
RATE_31_32 = 0b1111_1111_1111_1111_1111_1111_1111_1110,
}
public class Jpeg
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
public int Width { get; set; }
public int Height { get; set; }
public Jpeg(string address, int port, int taskID, int timeout = 2000)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.taskID = taskID;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
public async ValueTask<Result<bool>> Init(bool enable = true)
{
{
var ret = await CheckHdmiIsReady();
if (!ret.IsSuccessful)
{
logger.Error($"Failed to check HDMI ready: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("HDMI not ready");
return new(false);
}
}
int width = -1, height = -1;
{
var ret = await GetHdmiResolution();
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get HDMI resolution: {ret.Error}");
return new(ret.Error);
}
(width, height) = ret.Value;
}
{
var ret = await ConnectJpeg2Hdmi(width, height);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to connect JPEG to HDMI: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("Failed to connect JPEG to HDMI");
return false;
}
}
if (enable)
return await SetEnable(true);
else return true;
}
public async ValueTask<Result<bool>> SetEnable(bool enable)
{
if (enable)
{
{
var ret = await UDPClientPool.WriteAddrSeq(
this.ep,
this.taskID,
[JpegAddr.CAPTURE_RD_CTRL, JpegAddr.CAPTURE_WR_CTRL],
[0b11, 0b01],
this.timeout
);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set JPEG enable: {ret.Error}");
return ret.Value;
}
}
{
var ret = await AddFrameNum2Process(1);
if (!ret)
{
logger.Error($"Failed to AddFrameNum2Process: {ret}");
return ret.Value;
}
}
return true;
}
else
{
var ret = await UDPClientPool.WriteAddrSeq(
this.ep,
this.taskID,
[JpegAddr.CAPTURE_RD_CTRL, JpegAddr.CAPTURE_WR_CTRL],
[0b00, 0b00],
this.timeout
);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set JPEG disable: {ret.Error}");
return false;
}
return ret.Value;
}
}
public async ValueTask<Result<bool>> CheckHdmiIsReady()
{
var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, this.taskID, JpegAddr.HDMI_NOT_READY, 0b00, 0b01, 100, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to check HDMI status: {ret.Error}");
return new(ret.Error);
}
return ret.Value;
}
public async ValueTask<Result<(int, int)>> GetHdmiResolution()
{
var ret = await UDPClientPool.ReadAddrByte(
this.ep, this.taskID, JpegAddr.HDMI_HEIGHT_WIDTH, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get HDMI resolution: {ret.Error}");
return new(ret.Error);
}
var data = ret.Value.Options.Data;
if (data == null || data.Length != 4)
{
logger.Error($"Invalid HDMI resolution data length: {data?.Length ?? 0}");
return new(new Exception("Invalid HDMI resolution data length"));
}
var width = data[3] | (data[2] << 8);
var height = data[1] | (data[0] << 8);
this.Width = width;
this.Height = height;
logger.Info($"HDMI resolution: {width}x{height}");
return new((width, height));
}
public async ValueTask<Result<bool>> ConnectJpeg2Hdmi(int width, int height)
{
if (width <= 0 || height <= 0)
{
logger.Error($"Invalid HDMI resolution: {width}x{height}");
return new(new ArgumentException("Invalid HDMI resolution"));
}
var frameSize = (UInt32)(width * height);
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, JpegAddr.JPEG_HEIGHT_WIDTH, (uint)((height << 16) + width), this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set HDMI output start address: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set HDMI output start address");
return false;
}
}
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, JpegAddr.START_WR_ADDR0, JpegAddr.ADDR_HDMI_WD_START, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set HDMI output start address: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set HDMI output start address");
return false;
}
}
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, JpegAddr.END_WR_ADDR0,
JpegAddr.ADDR_HDMI_WD_START + frameSize - 1, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set HDMI output end address: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set HDMI output address");
return false;
}
}
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, JpegAddr.START_RD_ADDR0, JpegAddr.ADDR_HDMI_WD_START, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set jpeg input start address: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set jpeg input address");
return false;
}
}
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, JpegAddr.END_RD_ADDR0,
JpegAddr.ADDR_HDMI_WD_START + frameSize - 1, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set jpeg input end address: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set jpeg input end address");
return false;
}
}
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, JpegAddr.START_WR_ADDR1, JpegAddr.ADDR_JPEG_START, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set jpeg output start address: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set jpeg output start address");
return false;
}
}
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, JpegAddr.END_WR_ADDR1, JpegAddr.ADDR_JPEG_END, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set jpeg output end address: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set jpeg output end address");
return false;
}
}
return true;
}
// public async ValueTask<bool> SetSampleRate(uint rate)
// {
// var ret = await UDPClientPool.WriteAddr(
// this.ep, this.taskID, JpegAddr.FRAME_SAMPLE_RATE, rate, this.timeout);
// if (!ret.IsSuccessful)
// {
// logger.Error($"Failed to set JPEG sample rate: {ret.Error}");
// return false;
// }
// return ret.Value;
// }
// public async ValueTask<bool> SetSampleRate(JpegSampleRate rate)
// {
// return await SetSampleRate((uint)rate);
// }
public async ValueTask<uint> GetFrameNumber()
{
const int maxAttempts = 10;
const int delayMs = 5;
for (int attempt = 0; attempt < maxAttempts; attempt++)
{
var ret = await UDPClientPool.ReadAddrByte(
this.ep, this.taskID, JpegAddr.JPEG_FRAME_SAVE_NUM, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get JPEG frame number on attempt {attempt + 1}: {ret.Error}");
if (attempt < maxAttempts - 1)
{
await Task.Delay(delayMs);
continue;
}
return 0;
}
var frameNumber = Number.BytesToUInt32(ret.Value.Options.Data ?? Array.Empty<byte>()).Value;
if (frameNumber != 0)
{
return frameNumber;
}
// 如果不是最后一次尝试等待5ms后重试
if (attempt < maxAttempts - 1)
{
await Task.Delay(delayMs);
}
}
// 所有尝试都失败或返回0
return 0;
}
public async ValueTask<Optional<List<JpegInfo>>> GetFrameInfo(int num)
{
var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, JpegAddr.JPEG_FIFO_FRAME_INFO, num, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get JPEG frame info: {ret.Error}");
return new(null);
}
var data = ret.Value.Options.Data;
if (data == null || data.Length == 0)
{
logger.Error($"Data is null or empty");
return new(null);
}
if (data.Length != num * 2)
{
logger.Error(
$"Data length should be {num * 2} bytes, instead of {data.Length} bytes");
return new(null);
}
var infos = new List<JpegInfo>();
for (int i = 0; i < num; i++)
{
infos.Add(new JpegInfo(data[i..(i + 1)]));
}
return new(infos);
}
public async ValueTask<Result<bool>> AddFrameNum2Process(uint cnt)
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, JpegAddr.JPEG_ADD_NEED_FRAME_NUM, cnt, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to update pointer: {ret.Error}");
return ret.Value;
}
return ret.Value;
}
public async ValueTask<Result<byte[]?>> GetFrame(uint offset, uint length)
{
if (!MsgBus.IsRunning)
{
logger.Error("Message bus is not running");
return new(new Exception("Message bus is not running"));
}
MsgBus.UDPServer.ClearUDPData(this.ep.Address.ToString(), this.ep.Port);
var firstReadLength = (int)(Math.Min(
length,
JpegAddr.ADDR_JPEG_END - JpegAddr.ADDR_JPEG_START - offset
));
var secondReadLength = (int)(length - firstReadLength);
var dataBytes = new byte[length];
{
var ret = await UDPClientPool.ReadAddr4Bytes(
this.ep, this.taskID, JpegAddr.ADDR_JPEG_START + offset, firstReadLength, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get JPEG frame data: {ret.Error}");
return null;
}
if (ret.Value.Length != firstReadLength)
{
logger.Error($"Data length should be {firstReadLength} bytes, instead of {ret.Value.Length} bytes");
return null;
}
Buffer.BlockCopy(ret.Value, 0, dataBytes, 0, firstReadLength);
}
if (secondReadLength > 0)
{
var ret = await UDPClientPool.ReadAddr4Bytes(
this.ep, this.taskID, JpegAddr.ADDR_JPEG_START, secondReadLength, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get JPEG frame data: {ret.Error}");
return null;
}
if (ret.Value.Length != secondReadLength)
{
logger.Error($"Data length should be {secondReadLength} bytes, instead of {ret.Value.Length} bytes");
return null;
}
Buffer.BlockCopy(ret.Value, 0, dataBytes, firstReadLength, secondReadLength);
}
return dataBytes;
}
public async ValueTask<List<byte[]>> GetMultiFrames(uint offset, uint[] sizes)
{
var frames = new List<byte[]>();
for (int i = 0; i < sizes.Length; i++)
{
var ret = await GetFrame(offset, sizes[i]);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get JPEG frame {i} data: {ret.Error}");
continue;
}
if (ret.Value == null)
{
logger.Error($"Frame {i} data is null");
continue;
}
if (ret.Value.Length != sizes[i])
{
logger.Error(
$"Frame {i} data length should be {sizes[i]} bytes, instead of {ret.Value.Length} bytes");
continue;
}
frames.Add(ret.Value);
offset += sizes[i];
}
{
var ret = await AddFrameNum2Process((uint)sizes.Length);
if (!ret) logger.Error($"Failed to update pointer");
}
return frames;
}
public async ValueTask<Result<List<byte[]>?>> GetMultiFrames(uint offset)
{
if (!MsgBus.IsRunning)
{
logger.Error("Message bus is not running");
return new(new Exception("Message bus is not running"));
}
MsgBus.UDPServer.ClearUDPData(this.ep.Address.ToString(), this.ep.Port);
var frameNum = await GetFrameNumber();
if (frameNum == 0) return null;
List<uint>? frameSizes = null;
{
var ret = await GetFrameInfo((int)frameNum);
if (!ret.HasValue || ret.Value.Count == 0)
{
logger.Error($"Failed to get frame info");
return null;
}
frameSizes = ret.Value.Select(x => x.Size).ToList();
}
var frames = await GetMultiFrames(offset, frameSizes.ToArray());
if (frames.Count == 0)
{
logger.Error($"Failed to get frames");
return null;
}
return frames;
}
public async ValueTask<Result<uint[]?>> GetQuantizationTable()
{
const int totalQuantValues = 8 * 8 * 3; // Y(64) + Cb(64) + Cr(64) = 192个量化值
const int bytesPerValue = 4; // 每个量化值32bit = 4字节
const int totalBytes = totalQuantValues * bytesPerValue; // 总共768字节
try
{
var ret = await UDPClientPool.ReadAddr4Bytes(
this.ep, this.taskID, JpegAddr.JPEG_QUANTIZATION_TABLE, totalBytes, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read JPEG quantization table: {ret.Error}");
return new(ret.Error);
}
var data = ret.Value;
if (data == null || data.Length != totalBytes)
{
logger.Error($"Invalid quantization table data length: expected {totalBytes}, got {data?.Length ?? 0}");
return new(new Exception("Invalid quantization table data length"));
}
var quantTable = new uint[totalQuantValues];
for (int i = 0; i < totalQuantValues; i++)
{
// 每32bit为一个量化值按小端序读取
var offset = i * bytesPerValue;
quantTable[i] = (uint)(data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24));
}
logger.Debug($"Successfully read JPEG quantization table with {totalQuantValues} values");
return quantTable;
}
catch (Exception ex)
{
logger.Error(ex, "Exception occurred while reading JPEG quantization table");
return new(ex);
}
}
}

View File

@@ -1,11 +1,11 @@
using System.Collections;
using System.Net;
using BsdlParser;
using DotNext;
using Newtonsoft.Json;
using server.Services;
using WebProtocol;
namespace JtagClient;
namespace Peripherals.JtagClient;
/// <summary>
/// Global Constant Jtag Address
@@ -380,15 +380,21 @@ public class JtagStatusReg
public class Jtag
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly ProgressTracker _progressTracker = MsgBus.ProgressTracker;
private const int CLOCK_FREQ = 50; // MHz
readonly int timeout;
readonly int taskID = 10;
readonly int port;
readonly string address;
/// <summary>
/// Jtag控制器IP地址
/// </summary>
public readonly string address;
private IPEndPoint ep;
/// <summary>
/// Jtag 构造函数
/// </summary>
@@ -406,15 +412,17 @@ public class Jtag
async ValueTask<Result<uint>> ReadFIFO(uint devAddr)
{
var ret = false;
var opts = new SendAddrPackOptions();
var opts = new SendAddrPackOptions()
{
BurstType = BurstType.FixedBurst,
BurstLength = 0,
CommandID = (byte)this.taskID,
Address = devAddr,
IsWrite = false,
};
opts.BurstType = BurstType.FixedBurst;
opts.BurstLength = 0;
opts.CommandID = 0;
opts.Address = devAddr;
// Read Jtag State Register
opts.IsWrite = false;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send Address Package Failed!"));
@@ -422,7 +430,7 @@ public class Jtag
if (!MsgBus.IsRunning)
return new(new Exception("Message Bus not Working!"));
var retPack = await MsgBus.UDPServer.WaitForDataAsync(address, port);
var retPack = await MsgBus.UDPServer.WaitForDataAsync(this.ep, this.taskID, this.timeout);
if (!retPack.IsSuccessful || !retPack.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
@@ -434,14 +442,15 @@ public class Jtag
if (retPackLen != 4)
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes"));
return Convert.ToUInt32(Common.Number.BytesToUInt64(retPackOpts.Data).Value);
return Convert.ToUInt32(Common.Number.BytesToUInt32(retPackOpts.Data).Value);
}
async ValueTask<Result<bool>> WriteFIFO
(UInt32 devAddr, UInt32 data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
async ValueTask<Result<bool>> WriteFIFO(
UInt32 devAddr, UInt32 data, UInt32 result,
UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, string progressId = "")
{
{
var ret = await UDPClientPool.WriteAddr(this.ep, devAddr, data, this.timeout);
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, devAddr, data, this.timeout, progressId);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write FIFO failed"));
}
@@ -450,17 +459,20 @@ public class Jtag
await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds));
{
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, JtagAddr.STATE, result, resultMask, this.timeout);
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, this.taskID, JtagAddr.STATE, result, resultMask, 0, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
_progressTracker?.AdvanceProgress(progressId, 10);
return ret.Value;
}
}
async ValueTask<Result<bool>> WriteFIFO
(UInt32 devAddr, byte[] data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
async ValueTask<Result<bool>> WriteFIFO(
UInt32 devAddr, byte[] data, UInt32 result,
UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, string progressId = "")
{
{
var ret = await UDPClientPool.WriteAddr(this.ep, devAddr, data, this.timeout);
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, devAddr, data, this.timeout, progressId);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write FIFO failed"));
}
@@ -469,8 +481,9 @@ public class Jtag
await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds));
{
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, JtagAddr.STATE, result, resultMask, this.timeout);
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, this.taskID, JtagAddr.STATE, result, resultMask, 0, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
_progressTracker.AdvanceProgress(progressId, 10);
return ret.Value;
}
}
@@ -554,7 +567,8 @@ public class Jtag
return await ClearWriteDataReg();
}
async ValueTask<Result<bool>> LoadDRCareInput(byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500)
async ValueTask<Result<bool>> LoadDRCareInput(
byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500, string progressId = "")
{
var bytesLen = ((uint)(bytesArray.Length * 8));
if (bytesLen > Math.Pow(2, 28)) return new(new Exception("Length is over 2^(28 - 3)"));
@@ -569,11 +583,16 @@ public class Jtag
else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed"));
}
_progressTracker.AdvanceProgress(progressId, 10);
{
var ret = await WriteFIFO(
JtagAddr.WRITE_DATA,
bytesArray, 0x01_00_00_00,
JtagState.CMD_EXEC_FINISH);
JtagState.CMD_EXEC_FINISH,
0,
progressId
);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
@@ -607,13 +626,10 @@ public class Jtag
if (ret.Value)
{
var array = new UInt32[UInt32Num];
for (int i = 0; i < UInt32Num; i++)
{
var retData = await ReadFIFO(JtagAddr.READ_DATA);
if (!retData.IsSuccessful)
return new(new Exception("Read FIFO failed when Load DR"));
array[i] = retData.Value;
}
var retData = await UDPClientPool.ReadAddr4Bytes(ep, this.taskID, JtagAddr.READ_DATA, (int)UInt32Num);
if (!retData.IsSuccessful)
return new(new Exception("Read FIFO failed when Load DR"));
Buffer.BlockCopy(retData.Value, 0, array, 0, (int)UInt32Num * 4);
return array;
}
else
@@ -627,9 +643,9 @@ public class Jtag
public async ValueTask<Result<uint>> ReadIDCode()
{
// Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Clear up udp server {this.address} receive data");
logger.Trace($"Clear up udp server {this.address} {this.taskID} receive data");
Result<bool> ret;
@@ -665,9 +681,9 @@ public class Jtag
public async ValueTask<Result<uint>> ReadStatusReg()
{
// Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Clear up udp server {this.address} receive data");
logger.Trace($"Clear up udp server {this.address} {this.taskID} receive data");
Result<bool> ret;
@@ -698,45 +714,55 @@ public class Jtag
/// 下载比特流到 JTAG 设备
/// </summary>
/// <param name="bitstream">比特流数据</param>
/// <param name="progressId">进度ID</param>
/// <returns>指示下载是否成功的异步结果</returns>
public async ValueTask<Result<bool>> DownloadBitstream(byte[] bitstream)
public async ValueTask<Result<bool>> DownloadBitstream(
byte[] bitstream, string progressId = "")
{
// Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Clear up udp server {this.address} receive data");
logger.Trace($"Clear up udp server {this.address} {this.taskID} receive data");
_progressTracker.AdvanceProgress(progressId, 10);
Result<bool> ret;
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
_progressTracker.AdvanceProgress(progressId, 10);
ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
_progressTracker.AdvanceProgress(progressId, 10);
logger.Trace("Jtag initialize");
ret = await ExecRDCmd(JtagCmd.JTAG_DR_JRST);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JRST Failed"));
_progressTracker.AdvanceProgress(progressId, 10);
ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
_progressTracker.AdvanceProgress(progressId, 10);
ret = await ExecRDCmd(JtagCmd.JTAG_DR_CFGI);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_CFGI Failed"));
_progressTracker.AdvanceProgress(progressId, 10);
logger.Trace("Jtag ready to write bitstream");
ret = await IdleDelay(100000);
ret = await IdleDelay(1000);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
_progressTracker.AdvanceProgress(progressId, 10);
ret = await LoadDRCareInput(bitstream);
ret = await LoadDRCareInput(bitstream, progressId: progressId);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Load Data Failed"));
@@ -745,32 +771,40 @@ public class Jtag
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
_progressTracker.AdvanceProgress(progressId, 10);
ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
_progressTracker.AdvanceProgress(progressId, 10);
ret = await ExecRDCmd(JtagCmd.JTAG_DR_JWAKEUP);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JWAKEUP Failed"));
_progressTracker.AdvanceProgress(progressId, 10);
logger.Trace("Jtag reset device");
ret = await IdleDelay(10000);
ret = await IdleDelay(1000);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
_progressTracker.AdvanceProgress(progressId, 10);
var retCode = await ReadStatusReg();
if (!retCode.IsSuccessful) return new(retCode.Error);
var jtagStatus = new JtagStatusReg(retCode.Value);
if (!(jtagStatus.done && jtagStatus.wakeup_over && jtagStatus.init_complete))
return new(new Exception("Jtag download bitstream failed"));
_progressTracker.AdvanceProgress(progressId, 10);
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
logger.Trace("Jtag download bitstream successfully");
_progressTracker.AdvanceProgress(progressId, 10);
// Finish
_progressTracker.AdvanceProgress(progressId, 10);
return true;
}
@@ -783,12 +817,12 @@ public class Jtag
{
var paser = new BsdlParser.Parser();
var portNum = paser.GetBoundaryRegsNum().Value;
logger.Debug($"Get boundar scan registers number: {portNum}");
logger.Debug($"Get boundary scan registers number: {portNum}");
// Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Clear up udp server {this.address} receive data");
logger.Trace($"Clear up udp server {this.address} {this.taskID} receive data");
Result<bool> ret;
@@ -853,9 +887,9 @@ public class Jtag
public async ValueTask<Result<bool>> SetSpeed(UInt32 speed)
{
// Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Clear up udp server {this.address} receive data");
logger.Trace($"Clear up udp server {this.address} {this.taskID} receive data");
var ret = await WriteFIFO(
JtagAddr.SPEED_CTRL, (speed << 16) | speed,

View File

@@ -0,0 +1,571 @@
using System.Collections;
using System.Net;
using Common;
using DotNext;
using WebProtocol;
namespace Peripherals.LogicAnalyzerClient;
static class AnalyzerAddr
{
const UInt32 BASE = 0x9000_0000;
const UInt32 DMA_BASE = 0xA000_0000;
const UInt32 DDR_BASE = 0x0000_0000;
/// <summary>
/// 0x0000_0000 R/W [ 0] capture on: 置1开始等待捕获0停止捕获。捕获到信号后该位自动清零。 <br/>
/// [ 8] capture force: 置1则强制捕获信号自动置0。 <br/>
/// [16] capture busy: 1为逻辑分析仪正在捕获信号。 <br/>
/// [24] capture done: 1为逻辑分析仪内存完整存储了此次捕获的信号。 <br/>
/// 配置顺序:若[0]为0则将其置1随后不断获取[0]若其变为0则表示触发成功。随后不断获取[24]若其为1则表示捕获完成。 <br/>
/// </summary>
public const UInt32 CAPTURE_MODE = BASE + 0x0000_0000;
/// <summary>
/// 0x0000_0001 R/W [1:0] global trig mode: 00: 全局与 (&amp;) <br/>
/// 01: 全局或 (&#124;) <br/>
/// 10: 全局非与(~&amp;) <br/>
/// 11: 全局非或(~&#124;) <br/>
/// </summary>
public const UInt32 GLOBAL_TRIG_MODE = BASE + 0x0000_0001;
/// <summary>
/// 0x0000_0010 - 0x0000_0017 R/W [5:0] 信号M的触发操作符共8路 <br/>
/// [5:3] M's Operator: 000 == <br/>
/// 001 != <br/>
/// 010 &lt; <br/>
/// 011 &lt;= <br/>
/// 100 &gt; <br/>
/// 101 &gt;= <br/>
/// [2:0] M's Value: 000 LOGIC 0 <br/>
/// 001 LOGIC 1 <br/>
/// 010 X(not care) <br/>
/// 011 RISE <br/>
/// 100 FALL <br/>
/// 101 RISE OR FALL <br/>
/// 110 NOCHANGE <br/>
/// 111 SOME NUMBER <br/>
/// </summary>
public static readonly UInt32[] SIGNAL_TRIG_MODE = {
BASE + 0x0000_0010, BASE + 0x0000_0011,
BASE + 0x0000_0012, BASE + 0x0000_0013,
BASE + 0x0000_0014, BASE + 0x0000_0015,
BASE + 0x0000_0016, BASE + 0x0000_0017,
BASE + 0x0000_0018, BASE + 0x0000_0019,
BASE + 0x0000_001A, BASE + 0x0000_001B,
BASE + 0x0000_001C, BASE + 0x0000_001D,
BASE + 0x0000_001E, BASE + 0x0000_001F,
BASE + 0x0000_0020, BASE + 0x0000_0021,
BASE + 0x0000_0022, BASE + 0x0000_0023,
BASE + 0x0000_0024, BASE + 0x0000_0025,
BASE + 0x0000_0026, BASE + 0x0000_0027,
BASE + 0x0000_0028, BASE + 0x0000_0029,
BASE + 0x0000_002A, BASE + 0x0000_002B,
BASE + 0x0000_002C, BASE + 0x0000_002D,
BASE + 0x0000_002E, BASE + 0x0000_002F
};
public const UInt32 LOAD_NUM_ADDR = BASE + 0x0000_0002;
public const UInt32 PRE_LOAD_NUM_ADDR = BASE + 0x0000_0003;
public const UInt32 CAHNNEL_DIV_ADDR = BASE + 0x0000_0004;
public const UInt32 CLOCK_DIV_ADDR = BASE + 0x0000_0005;
public const UInt32 DMA_CAPTURE_RD_CTRL1 = DMA_BASE + 0x1;
public const UInt32 DMA_START_WRITE_ADDR1 = DMA_BASE + 0x22;
public const UInt32 DMA_END_WRITE_ADDR1 = DMA_BASE + 0x23;
public const UInt32 STORE_OFFSET_ADDR = DDR_BASE + 0x0100_0000;
/// <summary>
/// 0x0100_0000 - 0x0100_03FF 只读 32位波形存储得到的32位数据中低八位最先捕获高八位最后捕获。<br/>
/// 共1024个地址每个地址存储4组深度为4096。<br/>
/// </summary>
public const Int32 CAPTURE_DATA_LENGTH = 1024;
public const Int32 CAPTURE_DATA_PRELOAD = 512;
}
/// <summary>
/// 逻辑分析仪运行状态枚举
/// </summary>
[Flags]
public enum CaptureStatus
{
/// <summary>
/// 无状态标志
/// </summary>
None = 0,
/// <summary>
/// 捕获使能位置1开始等待捕获0停止捕获。捕获到信号后该位自动清零
/// </summary>
CaptureOn = 1 << 0, // [0] 捕获使能
/// <summary>
/// 强制捕获位置1则强制捕获信号自动置0
/// </summary>
CaptureForce = 1 << 8, // [8] 强制捕获
/// <summary>
/// 捕获忙碌位1为逻辑分析仪正在捕获信号
/// </summary>
CaptureBusy = 1 << 16, // [16] 捕获进行中
/// <summary>
/// 捕获完成位1为逻辑分析仪内存完整存储了此次捕获的信号
/// </summary>
CaptureDone = 1 << 24 // [24] 捕获完成
}
/// <summary>
/// 全局触发模式枚举,定义多路信号触发条件的逻辑组合方式
/// </summary>
public enum GlobalCaptureMode
{
/// <summary>
/// 全局与模式,所有触发条件都必须满足
/// </summary>
AND = 0b00,
/// <summary>
/// 全局或模式,任一触发条件满足即可
/// </summary>
OR = 0b01,
/// <summary>
/// 全局非与模式,不是所有触发条件都满足
/// </summary>
NAND = 0b10,
/// <summary>
/// 全局非或模式,所有触发条件都不满足
/// </summary>
NOR = 0b11
}
/// <summary>
/// 逻辑分析仪采样时钟分频系数
/// </summary>
public enum AnalyzerClockDiv
{
/// <summary>
/// 1分频
/// </summary>
DIV1 = 0x0000_0000,
/// <summary>
/// 2分频
/// </summary>
DIV2 = 0x0000_0001,
/// <summary>
/// 4分频
/// </summary>
DIV4 = 0x0000_0002,
/// <summary>
/// 8分频
/// </summary>
DIV8 = 0x0000_0003,
/// <summary>
/// 16分频
/// </summary>
DIV16 = 0x0000_0004,
/// <summary>
/// 32分频
/// </summary>
DIV32 = 0x0000_0005,
/// <summary>
/// 64分频
/// </summary>
DIV64 = 0x0000_0006,
/// <summary>
/// 128分频
/// </summary>
DIV128 = 0x0000_0007
}
/// <summary>
/// 信号M的操作符枚举
/// </summary>
public enum SignalOperator : byte
{
/// <summary>
/// 等于操作符
/// </summary>
Equal = 0b000, // ==
/// <summary>
/// 不等于操作符
/// </summary>
NotEqual = 0b001, // !=
/// <summary>
/// 小于操作符
/// </summary>
LessThan = 0b010, // <
/// <summary>
/// 小于等于操作符
/// </summary>
LessThanOrEqual = 0b011, // <=
/// <summary>
/// 大于操作符
/// </summary>
GreaterThan = 0b100, // >
/// <summary>
/// 大于等于操作符
/// </summary>
GreaterThanOrEqual = 0b101 // >=
}
/// <summary>
/// 信号M的值枚举
/// </summary>
public enum SignalValue : byte
{
/// <summary>
/// 逻辑0电平
/// </summary>
Logic0 = 0b000, // LOGIC 0
/// <summary>
/// 逻辑1电平
/// </summary>
Logic1 = 0b001, // LOGIC 1
/// <summary>
/// 不关心该信号状态
/// </summary>
NotCare = 0b010, // X(not care)
/// <summary>
/// 上升沿触发
/// </summary>
Rise = 0b011, // RISE
/// <summary>
/// 下降沿触发
/// </summary>
Fall = 0b100, // FALL
/// <summary>
/// 上升沿或下降沿触发
/// </summary>
RiseOrFall = 0b101, // RISE OR FALL
/// <summary>
/// 信号无变化
/// </summary>
NoChange = 0b110, // NOCHANGE
/// <summary>
/// 特定数值
/// </summary>
SomeNumber = 0b111 // SOME NUMBER
}
/// <summary>
/// 逻辑分析仪有效通道数
/// </summary>
public enum AnalyzerChannelDiv
{
/// <summary>
/// 1路
/// </summary>
ONE = 0x0000_0000,
/// <summary>
/// 2路
/// </summary>
TWO = 0x0000_0001,
/// <summary>
/// 4路
/// </summary>
FOUR = 0x0000_0002,
/// <summary>
/// 8路
/// </summary>
EIGHT = 0x0000_0003,
/// <summary>
/// 16路
/// </summary>
XVI = 0x0000_0004,
/// <summary>
/// 32路
/// </summary>
XXXII = 0x0000_0005
}
/// <summary>
/// FPGA逻辑分析仪客户端用于控制FPGA上的逻辑分析仪模块进行信号捕获和分析
/// </summary>
public class Analyzer
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
/// <summary>
/// 初始化逻辑分析仪客户端
/// </summary>
/// <param name="address">FPGA设备的IP地址</param>
/// <param name="port">通信端口号</param>
/// <param name="taskID">任务标识符</param>
/// <param name="timeout">通信超时时间毫秒默认2000ms</param>
/// <exception cref="ArgumentException">当timeout为负数时抛出</exception>
public Analyzer(string address, int port, int taskID, int timeout = 2000)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.taskID = taskID;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
/// <summary>
/// 控制逻辑分析仪的捕获模式
/// </summary>
/// <param name="captureOn">是否开始捕获</param>
/// <param name="force">是否强制捕获</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetCaptureMode(bool captureOn, bool force)
{
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA_CAPTURE_RD_CTRL1, 0x00000000u, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set DMA_CAPTURE_RD_CTRL to 0: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DMA_CAPTURE_RD_CTRL returned false");
return new(new Exception("Failed to set DMA_CAPTURE_RD_CTRL"));
}
}
await Task.Delay(5);
// 构造寄存器值
UInt32 value = 0;
if (captureOn) value |= 1 << 0;
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA_CAPTURE_RD_CTRL1, value, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set DMA_CAPTURE_RD_CTRL: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DMA_CAPTURE_RD_CTRL returned false");
return new(new Exception("Failed to set DMA_CAPTURE_RD_CTRL"));
}
}
if (force) value |= 1 << 8;
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, value, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set capture mode: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to CAPTURE_MODE returned false");
return new(new Exception("Failed to set capture mode"));
}
}
return true;
}
/// <summary>
/// 读取逻辑分析仪捕获运行状态
/// </summary>
/// <returns>操作结果,成功返回寄存器值,否则返回异常信息</returns>
public async ValueTask<Result<CaptureStatus>> ReadCaptureStatus()
{
var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read capture status: {ret.Error}");
return new(ret.Error);
}
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 4)
{
logger.Error("ReadAddr returned invalid data for capture status");
return new(new Exception("Failed to read capture status"));
}
UInt32 status = Number.BytesToUInt32(ret.Value.Options.Data).Value;
return (CaptureStatus)status;
}
/// <summary>
/// 设置全局触发模式
/// </summary>
/// <param name="mode">全局触发模式0:与, 1:或, 2:非与, 3:非或)</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetGlobalTrigMode(GlobalCaptureMode mode)
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, AnalyzerAddr.GLOBAL_TRIG_MODE, (byte)mode, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set global trigger mode: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to GLOBAL_TRIG_MODE returned false");
return new(new Exception("Failed to set global trigger mode"));
}
return true;
}
/// <summary>
/// 设置指定信号通道的触发模式
/// </summary>
/// <param name="signalIndex">信号通道索引0-7</param>
/// <param name="op">触发操作符</param>
/// <param name="val">触发信号值</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetSignalTrigMode(int signalIndex, SignalOperator op, SignalValue val)
{
if (signalIndex < 0 || signalIndex >= AnalyzerAddr.SIGNAL_TRIG_MODE.Length)
return new(new ArgumentException($"Signal index must be 0~{AnalyzerAddr.SIGNAL_TRIG_MODE.Length}"));
// 计算模式值: [2:0] 信号值, [5:3] 操作符
UInt32 mode = ((UInt32)op << 3) | (UInt32)val;
var addr = AnalyzerAddr.SIGNAL_TRIG_MODE[signalIndex];
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, addr, mode, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set signal trigger mode: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to SIGNAL_TRIG_MODE returned false");
return new(new Exception("Failed to set signal trigger mode"));
}
return true;
}
/// <summary>
/// 设置逻辑分析仪的深度、预采样深度、有效通道、分频系数
/// </summary>
/// <param name="capture_length">深度</param>
/// <param name="pre_capture_length">预采样深度</param>
/// <param name="channel_div">有效通道(0-[1],1-[2],2-[4],3-[8],4-[16],5-[32])</param>
/// <param name="clock_div">采样时钟分频系数</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetCaptureParams(int capture_length, int pre_capture_length, AnalyzerChannelDiv channel_div, AnalyzerClockDiv clock_div)
{
if (capture_length == 0) capture_length = 1;
if (pre_capture_length == 0) pre_capture_length = 1;
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.LOAD_NUM_ADDR, (UInt32)(capture_length - 1), this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set LOAD_NUM_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to LOAD_NUM_ADDR returned false");
return new(new Exception("Failed to set LOAD_NUM_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.PRE_LOAD_NUM_ADDR, (UInt32)(pre_capture_length - 1), this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set PRE_LOAD_NUM_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to PRE_LOAD_NUM_ADDR returned false");
return new(new Exception("Failed to set PRE_LOAD_NUM_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA_START_WRITE_ADDR1, AnalyzerAddr.STORE_OFFSET_ADDR, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set DMA_START_WRITE_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DMA_START_WRITE_ADDR returned false");
return new(new Exception("Failed to set DMA_START_WRITE_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA_END_WRITE_ADDR1, AnalyzerAddr.STORE_OFFSET_ADDR + (UInt32)(capture_length - 1), this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set DMA_END_WRITE_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DMA_END_WRITE_ADDR returned false");
return new(new Exception("Failed to set DMA_END_WRITE_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.CAHNNEL_DIV_ADDR, (UInt32)channel_div, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set CAHNNEL_DIV_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to CAHNNEL_DIV_ADDR returned false");
return new(new Exception("Failed to set CAHNNEL_DIV_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.CLOCK_DIV_ADDR, (UInt32)clock_div, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set CLOCK_DIV_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to CLOCK_DIV_ADDR returned false");
return new(new Exception("Failed to set CLOCK_DIV_ADDR"));
}
}
return true;
}
/// <summary>
/// 读取捕获的波形数据
/// </summary>
/// <returns>操作结果成功返回byte[],否则返回异常信息</returns>
public async ValueTask<Result<byte[]>> ReadCaptureData(int capture_length = 2048 * 32)
{
var ret = await UDPClientPool.ReadAddr4BytesAsync(
this.ep,
this.taskID,
AnalyzerAddr.STORE_OFFSET_ADDR,
capture_length,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout
);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read capture data: {ret.Error}");
return new(ret.Error);
}
var data = ret.Value;
if (data == null || data.Length != capture_length * 4)
{
logger.Error($"Capture data length mismatch: {data?.Length}");
return new(new Exception("Capture data length mismatch"));
}
var reversed = Common.Number.ReverseBytes(data, 4).Value;
return reversed;
}
}

View File

@@ -44,10 +44,10 @@ public class MatrixKey
public async ValueTask<Result<bool>> EnableControl()
{
if (MsgBus.IsRunning)
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, 1);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(this.ep, MatrixKeyAddr.KEY_ENABLE, 1, this.timeout);
var ret = await UDPClientPool.WriteAddr(this.ep, 1, MatrixKeyAddr.KEY_ENABLE, 1, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
@@ -59,10 +59,10 @@ public class MatrixKey
public async ValueTask<Result<bool>> DisableControl()
{
if (MsgBus.IsRunning)
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, 1);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(this.ep, MatrixKeyAddr.KEY_ENABLE, 0, this.timeout);
var ret = await UDPClientPool.WriteAddr(this.ep, 1, MatrixKeyAddr.KEY_ENABLE, 0, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
@@ -75,14 +75,14 @@ public class MatrixKey
public async ValueTask<Result<bool>> ControlKey(BitArray keyStates)
{
if (MsgBus.IsRunning)
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, 1);
else return new(new Exception("Message Bus not work!"));
if (keyStates.Length != 16) return new(new ArgumentException(
$"The number of key should be 16 instead of {keyStates.Length}", nameof(keyStates)));
var ret = await UDPClientPool.WriteAddr(
this.ep, MatrixKeyAddr.KEY_CTRL, Common.Number.BitsToNumber(keyStates).Value, this.timeout);
this.ep, 1, MatrixKeyAddr.KEY_CTRL, Common.Number.BitsToNumber(keyStates).Value, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}

View File

@@ -0,0 +1,392 @@
using System.Net;
using DotNext;
namespace Peripherals.NetConfigClient;
static class NetConfigAddr
{
const UInt32 BASE = 0x30A7_0000;
public static readonly UInt32[] HOST_IP = { BASE + 0, BASE + 1, BASE + 2, BASE + 3 };
public static readonly UInt32[] BOARD_IP = { BASE + 4, BASE + 5, BASE + 6, BASE + 7 };
public static readonly UInt32[] HOST_MAC = { BASE + 8, BASE + 9, BASE + 10, BASE + 11, BASE + 12, BASE + 13 };
public static readonly UInt32[] BOARD_MAC = { BASE + 14, BASE + 15, BASE + 16, BASE + 17, BASE + 18, BASE + 19 };
}
/// <summary>
/// Network configuration client for FPGA board communication
/// </summary>
public class NetConfig
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
/// <summary>
/// Initialize NetConfig client
/// </summary>
/// <param name="address">Target board address</param>
/// <param name="port">Target board port</param>
/// <param name="taskID">Task identifier</param>
/// <param name="timeout">Timeout in milliseconds</param>
public NetConfig(string address, int port, int taskID, int timeout = 2000)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.taskID = taskID;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
/// <summary>
/// Set host IP address
/// </summary>
/// <param name="ip">IP address to set</param>
/// <returns>Result indicating success or failure</returns>
public async ValueTask<Result<bool>> SetHostIP(IPAddress ip)
{
// 清除UDP服务器接收缓冲区
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
// 刷新ARP
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
// if (!refrshRet)
// {
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
// }
var ipBytes = ip.GetAddressBytes();
var ret = await UDPClientPool.WriteAddrSeq(this.ep, this.taskID, NetConfigAddr.HOST_IP, ipBytes, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set host IP: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set host IP: operation returned false");
return false;
}
// 验证设置结果
var verifyResult = await GetHostIP();
if (!verifyResult.IsSuccessful)
{
logger.Error($"Failed to verify host IP after setting: {verifyResult.Error}");
return new(verifyResult.Error);
}
var expectedIP = ip.ToString();
if (verifyResult.Value != expectedIP)
{
logger.Error($"Host IP verification failed: expected {expectedIP}, got {verifyResult.Value}");
return false;
}
logger.Info($"Successfully set and verified host IP: {expectedIP}");
return true;
}
/// <summary>
/// Set board IP address
/// </summary>
/// <param name="ip">IP address to set</param>
/// <returns>Result indicating success or failure</returns>
public async ValueTask<Result<bool>> SetBoardIP(IPAddress ip)
{
// 清除UDP服务器接收缓冲区
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
// 刷新ARP
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
// if (!refrshRet)
// {
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
// }
var ipBytes = ip.GetAddressBytes();
var ret = await UDPClientPool.WriteAddrSeq(this.ep, this.taskID, NetConfigAddr.BOARD_IP, ipBytes, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set board IP: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set board IP: operation returned false");
return false;
}
// 验证设置结果
var verifyResult = await GetBoardIP();
if (!verifyResult.IsSuccessful)
{
logger.Error($"Failed to verify board IP after setting: {verifyResult.Error}");
return new(verifyResult.Error);
}
var expectedIP = ip.ToString();
if (verifyResult.Value != expectedIP)
{
logger.Error($"Board IP verification failed: expected {expectedIP}, got {verifyResult.Value}");
return false;
}
logger.Info($"Successfully set and verified board IP: {expectedIP}");
return true;
}
/// <summary>
/// Set host MAC address
/// </summary>
/// <param name="macAddress">MAC address bytes (6 bytes)</param>
/// <returns>Result indicating success or failure</returns>
public async ValueTask<Result<bool>> SetHostMAC(byte[] macAddress)
{
if (macAddress == null)
throw new ArgumentNullException(nameof(macAddress));
if (macAddress.Length != 6)
throw new ArgumentException("MAC address must be 6 bytes", nameof(macAddress));
// 清除UDP服务器接收缓冲区
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
// 刷新ARP
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
// if (!refrshRet)
// {
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
// }
var ret = await UDPClientPool.WriteAddrSeq(this.ep, this.taskID, NetConfigAddr.HOST_MAC, macAddress, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set host MAC address: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set host MAC address: operation returned false");
return false;
}
// 验证设置结果
var verifyResult = await GetHostMAC();
if (!verifyResult.IsSuccessful)
{
logger.Error($"Failed to verify host MAC after setting: {verifyResult.Error}");
return new(verifyResult.Error);
}
var expectedMAC = string.Join(":", macAddress.Select(b => $"{b:X2}"));
if (verifyResult.Value != expectedMAC)
{
logger.Error($"Host MAC verification failed: expected {expectedMAC}, got {verifyResult.Value}");
return false;
}
logger.Info($"Successfully set and verified host MAC: {expectedMAC}");
return true;
}
/// <summary>
/// Set board MAC address
/// </summary>
/// <param name="macAddress">MAC address bytes (6 bytes)</param>
/// <returns>Result indicating success or failure</returns>
public async ValueTask<Result<bool>> SetBoardMAC(byte[] macAddress)
{
if (macAddress == null)
throw new ArgumentNullException(nameof(macAddress));
if (macAddress.Length != 6)
throw new ArgumentException("MAC address must be 6 bytes", nameof(macAddress));
// 清除UDP服务器接收缓冲区
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
// 刷新ARP
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
// if (!refrshRet)
// {
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
// }
var ret = await UDPClientPool.WriteAddrSeq(this.ep, this.taskID, NetConfigAddr.BOARD_MAC, macAddress, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set board MAC address: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"Failed to set board MAC address: operation returned false");
return false;
}
// 验证设置结果
var verifyResult = await GetBoardMAC();
if (!verifyResult.IsSuccessful)
{
logger.Error($"Failed to verify board MAC after setting: {verifyResult.Error}");
return new(verifyResult.Error);
}
var expectedMAC = string.Join(":", macAddress.Select(b => $"{b:X2}"));
if (verifyResult.Value != expectedMAC)
{
logger.Error($"Board MAC verification failed: expected {expectedMAC}, got {verifyResult.Value}");
return false;
}
logger.Info($"Successfully set and verified board MAC: {expectedMAC}");
return true;
}
/// <summary>
/// Get host IP address
/// </summary>
/// <returns>Host IP address as string</returns>
public async ValueTask<Result<string>> GetHostIP()
{
// 清除UDP服务器接收缓冲区
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
// 刷新ARP
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
// if (!refrshRet)
// {
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
// }
var ret = await UDPClientPool.ReadAddrSeq(this.ep, this.taskID, NetConfigAddr.HOST_IP, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get host IP: {ret.Error}");
return new(ret.Error);
}
var ip = "";
for (int i = 0; i < NetConfigAddr.HOST_IP.Length; i++)
{
ip += $"{ret.Value[i * 4 + 3]}";
if (i != NetConfigAddr.HOST_IP.Length - 1)
ip += ".";
}
return ip;
}
/// <summary>
/// Get board IP address
/// </summary>
/// <returns>Board IP address as string</returns>
public async ValueTask<Result<string>> GetBoardIP()
{
// 清除UDP服务器接收缓冲区
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
// 刷新ARP
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
// if (!refrshRet)
// {
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
// }
var ret = await UDPClientPool.ReadAddrSeq(this.ep, this.taskID, NetConfigAddr.BOARD_IP, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get board IP: {ret.Error}");
return new(ret.Error);
}
var ip = "";
for (int i = 0; i < NetConfigAddr.BOARD_IP.Length; i++)
{
ip += $"{ret.Value[i * 4 + 3]}";
if (i != NetConfigAddr.BOARD_IP.Length - 1)
ip += ".";
}
return ip;
}
/// <summary>
/// Get host MAC address
/// </summary>
/// <returns>Host MAC address as formatted string (XX:XX:XX:XX:XX:XX)</returns>
public async ValueTask<Result<string>> GetHostMAC()
{
// 清除UDP服务器接收缓冲区
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
// 刷新ARP
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
// if (!refrshRet)
// {
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
// }
var ret = await UDPClientPool.ReadAddrSeq(this.ep, this.taskID, NetConfigAddr.HOST_MAC, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get host MAC address: {ret.Error}");
return new(ret.Error);
}
var mac = "";
for (int i = 0; i < NetConfigAddr.HOST_MAC.Length; i++)
{
mac += $"{ret.Value[i * 4 + 3]:X2}";
if (i != NetConfigAddr.HOST_MAC.Length - 1)
mac += ":";
}
return mac;
}
/// <summary>
/// Get board MAC address
/// </summary>
/// <returns>Board MAC address as formatted string (XX:XX:XX:XX:XX:XX)</returns>
public async ValueTask<Result<string>> GetBoardMAC()
{
// 清除UDP服务器接收缓冲区
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
// 刷新ARP
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
// if (!refrshRet)
// {
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
// }
var ret = await UDPClientPool.ReadAddrSeq(this.ep, this.taskID, NetConfigAddr.BOARD_MAC, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to get board MAC address: {ret.Error}");
return new(ret.Error);
}
var mac = "";
for (int i = 0; i < NetConfigAddr.BOARD_MAC.Length; i++)
{
mac += $"{ret.Value[i * 4 + 3]:X2}";
if (i != NetConfigAddr.BOARD_MAC.Length - 1)
mac += ":";
}
return mac;
}
}

View File

@@ -0,0 +1,448 @@
using System.Net;
using Common;
using DotNext;
using WebProtocol;
using Tapper;
namespace Peripherals.OscilloscopeClient;
public class OscilloscopeConfig
{
public bool CaptureEnabled { get; set; }
public byte TriggerLevel { get; set; }
public bool TriggerRisingEdge { get; set; }
public ushort HorizontalShift { get; set; }
public ushort DecimationRate { get; set; }
// public bool AutoRefreshRAM { get; set; }
}
static class OscilloscopeAddr
{
const UInt32 BASE = 0x8000_0000;
/// <summary>
/// 0x0000_0000:R/W[0] wave_run 启动捕获/关闭
/// </summary>
public const UInt32 START_CAPTURE = BASE + 0x0000_0000;
/// <summary>
/// 0x0000_0001: R/W[7:0] trig_level 触发电平
/// </summary>
public const UInt32 TRIG_LEVEL = BASE + 0x0000_0001;
/// <summary>
/// 0x0000_0002:R/W[0] trig_edge 触发边沿0-下降沿1-上升沿
/// </summary>
public const UInt32 TRIG_EDGE = BASE + 0x0000_0002;
/// <summary>
/// 0x0000_0004: R/W[9:0] deci rate 抽样率0—1023
/// </summary>
public const UInt32 DECI_RATE = BASE + 0x0000_0003;
/// <summary>
/// 0x0000_0005:R/W[0] ram refresh RAM刷新
/// </summary>
public const UInt32 RAM_FRESH = BASE + 0x0000_0004;
/// <summary>
/// 0x0000_0005:R/W[0] wave ready 波形数据就绪
/// </summary>
public const UInt32 WAVE_READY = BASE + 0x0000_0005;
/// <summary>
/// 0x0000_0005:R/W[0] trig postion 触发地址
/// </summary>
public const UInt32 TRIG_POSIION = BASE + 0x0000_0006;
/// <summary>
/// 0x0000 0006:R[19: 0] ad_freq AD采样频率
/// </summary>
public const UInt32 AD_FREQ = BASE + 0x0000_0007;
/// <summary>
/// Ox0000_0007: R[7:0] ad_vpp AD采样幅度
/// </summary>
public const UInt32 AD_VPP = BASE + 0x0000_0008;
/// <summary>
/// 0x0000_0008: R[7:0] ad max AD采样最大值
/// </summary>
public const UInt32 AD_MAX = BASE + 0x0000_0009;
/// <summary>
/// 0x0000_0009: R[7:0] ad_min AD采样最小值
/// </summary>
public const UInt32 AD_MIN = BASE + 0x0000_000A;
/// <summary>
/// 0x0000_1000-0x0000_13FF:R[7:0] wave_rd_data 共1024个字节
/// </summary>
public const UInt32 RD_DATA_ADDR = BASE + 0x0000_1000;
public const UInt32 RD_DATA_LENGTH = 0x0000_0400;
}
class OscilloscopeCtrl
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000;
readonly int taskID = 12;
readonly int port;
readonly string address;
private IPEndPoint ep;
/// <summary>
/// 初始化示波器客户端
/// </summary>
/// <param name="address">示波器设备IP地址</param>
/// <param name="port">示波器设备端口</param>
/// <param name="timeout">超时时间(毫秒)</param>
public OscilloscopeCtrl(string address, int port, int timeout = 2000)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
/// <summary>
/// 一次性初始化/配置示波器
/// </summary>
/// <param name="config">完整配置</param>
/// <returns>操作结果全部成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> Init(OscilloscopeConfig config)
{
// 1. 捕获使能
var ret = await SetCaptureEnable(config.CaptureEnabled);
if (!ret.IsSuccessful || !ret.Value)
return new(ret.Error ?? new Exception("Failed to set capture enable"));
// 2. 触发电平
ret = await SetTriggerLevel(config.TriggerLevel);
if (!ret.IsSuccessful || !ret.Value)
return new(ret.Error ?? new Exception("Failed to set trigger level"));
// 3. 触发边沿
ret = await SetTriggerEdge(config.TriggerRisingEdge);
if (!ret.IsSuccessful || !ret.Value)
return new(ret.Error ?? new Exception("Failed to set trigger edge"));
// 4. 水平偏移
ret = await SetHorizontalShift(config.HorizontalShift);
if (!ret.IsSuccessful || !ret.Value)
return new(ret.Error ?? new Exception("Failed to set horizontal shift"));
// 5. 抽样率
ret = await SetDecimationRate(config.DecimationRate);
if (!ret.IsSuccessful || !ret.Value)
return new(ret.Error ?? new Exception("Failed to set decimation rate"));
// 6. RAM刷新如果需要
// if (config.AutoRefreshRAM)
// {
// ret = await RefreshRAM();
// if (!ret.IsSuccessful || !ret.Value)
// return new(ret.Error ?? new Exception("Failed to refresh RAM"));
// }
return true;
}
/// <summary>
/// 控制示波器的捕获开关
/// </summary>
/// <param name="enable">是否启动捕获</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetCaptureEnable(bool enable)
{
UInt32 value = enable ? 1u : 0u;
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, OscilloscopeAddr.START_CAPTURE, value, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set capture enable: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to START_CAPTURE returned false");
return new(new Exception("Failed to set capture enable"));
}
return true;
}
/// <summary>
/// 设置触发电平
/// </summary>
/// <param name="level">触发电平值0-255</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetTriggerLevel(byte level)
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, OscilloscopeAddr.TRIG_LEVEL, level, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set trigger level: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to TRIG_LEVEL returned false");
return new(new Exception("Failed to set trigger level"));
}
return true;
}
/// <summary>
/// 设置触发边沿
/// </summary>
/// <param name="risingEdge">true为上升沿false为下降沿</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetTriggerEdge(bool risingEdge)
{
UInt32 value = risingEdge ? 1u : 0u;
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, OscilloscopeAddr.TRIG_EDGE, value, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set trigger edge: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to TRIG_EDGE returned false");
return new(new Exception("Failed to set trigger edge"));
}
return true;
}
/// <summary>
/// 设置水平偏移量
/// </summary>
/// <param name="shift">水平偏移量值0-1023</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetHorizontalShift(UInt16 shift)
{
return true;
}
/// <summary>
/// 设置抽样率
/// </summary>
/// <param name="rate">抽样率值0-1023</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetDecimationRate(UInt16 rate)
{
if (rate > 1023)
return new(new ArgumentException("Decimation rate must be 0-1023", nameof(rate)));
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, OscilloscopeAddr.DECI_RATE, rate, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set decimation rate: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DECI_RATE returned false");
return new(new Exception("Failed to set decimation rate"));
}
return true;
}
/// <summary>
/// 刷新RAM
/// </summary>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> RefreshRAM()
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, OscilloscopeAddr.RAM_FRESH, 1u, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to refresh RAM: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to RAM_FRESH returned false");
return new(new Exception("Failed to refresh RAM"));
}
return true;
}
/// <summary>
/// 获取AD采样频率
/// </summary>
/// <returns>操作结果,成功返回采样频率值,否则返回异常信息</returns>
public async ValueTask<Result<UInt32>> GetADFrequency()
{
var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_FREQ, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read AD frequency: {ret.Error}");
return new(ret.Error);
}
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 4)
{
logger.Error("ReadAddr returned invalid data for AD frequency");
return new(new Exception("Failed to read AD frequency"));
}
UInt32 freq = Number.BytesToUInt32(ret.Value.Options.Data).Value;
// 取低20位 [19:0]
freq &= 0xFFFFF;
return freq;
}
/// <summary>
/// 获取AD采样幅度
/// </summary>
/// <returns>操作结果,成功返回采样幅度值,否则返回异常信息</returns>
public async ValueTask<Result<byte>> GetADVpp()
{
var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_VPP, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read AD VPP: {ret.Error}");
return new(ret.Error);
}
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 1)
{
logger.Error("ReadAddr returned invalid data for AD VPP");
return new(new Exception("Failed to read AD VPP"));
}
return ret.Value.Options.Data[3];
}
/// <summary>
/// 获取AD采样最大值
/// </summary>
/// <returns>操作结果,成功返回采样最大值,否则返回异常信息</returns>
public async ValueTask<Result<byte>> GetADMax()
{
var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_MAX, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read AD max: {ret.Error}");
return new(ret.Error);
}
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 1)
{
logger.Error("ReadAddr returned invalid data for AD max");
return new(new Exception("Failed to read AD max"));
}
return ret.Value.Options.Data[3];
}
/// <summary>
/// 获取AD采样最小值
/// </summary>
/// <returns>操作结果,成功返回采样最小值,否则返回异常信息</returns>
public async ValueTask<Result<byte>> GetADMin()
{
var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.AD_MIN, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read AD min: {ret.Error}");
return new(ret.Error);
}
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 1)
{
logger.Error("ReadAddr returned invalid data for AD min");
return new(new Exception("Failed to read AD min"));
}
return ret.Value.Options.Data[3];
}
/// <summary>
/// 获取波形采样数据
/// </summary>
/// <returns>操作结果,成功返回采样数据数组,否则返回异常信息</returns>
public async ValueTask<Result<byte[]>> GetWaveformData()
{
// 等待WAVE_READY[0]位为1最多等待50ms5次x10ms间隔
var readyResult = await UDPClientPool.ReadAddrWithWait(
this.ep, this.taskID, OscilloscopeAddr.WAVE_READY, 0b01, 0x01, 10, 50);
if (!readyResult.IsSuccessful)
{
logger.Error($"Failed to wait for wave ready: {readyResult.Error}");
return new(readyResult.Error);
}
// 无论准备好与否都继续读取数据readyResult.Value表示是否在超时前准备好
if (!readyResult.Value)
{
logger.Warn("Wave data may not be ready, but continuing to read");
}
// 无论准备好与否,都继续读取数据
var ret = await UDPClientPool.ReadAddr4BytesAsync(
this.ep,
this.taskID,
OscilloscopeAddr.RD_DATA_ADDR,
(int)OscilloscopeAddr.RD_DATA_LENGTH / 32,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout
);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read waveform data: {ret.Error}");
return new(ret.Error);
}
var data = ret.Value;
if (data == null || data.Length != OscilloscopeAddr.RD_DATA_LENGTH / 8)
{
logger.Error($"Waveform data length mismatch: {data?.Length}");
return new(new Exception("Waveform data length mismatch"));
}
// 处理波形数据从每4个字节中提取第4个字节索引3作为有效数据
// 数据格式:低八位有效,即[4*i + 3]才是有效数据
int sampleCount = data.Length / 4;
byte[] waveformData = new byte[sampleCount];
for (int i = 0; i < sampleCount; i++)
{
waveformData[i] = data[4 * i + 3];
}
// 获取触发地址用作数据偏移量
var trigPosResult = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, OscilloscopeAddr.TRIG_POSIION, this.timeout);
if (!trigPosResult.IsSuccessful)
{
logger.Error($"Failed to read trigger position: {trigPosResult.Error}");
return new(trigPosResult.Error);
}
if (trigPosResult.Value.Options.Data == null || trigPosResult.Value.Options.Data.Length < 4)
{
logger.Error("ReadAddr returned invalid data for trigger position");
return new(new Exception("Failed to read trigger position"));
}
UInt32 trigAddr = Number.BytesToUInt32(trigPosResult.Value.Options.Data).Value;
// 根据触发地址对数据进行偏移,使触发点位于数据中间
int targetPos = sampleCount / 2; // 目标位置:数据中间
int actualTrigPos = (int)(trigAddr % (UInt32)sampleCount); // 实际触发位置
int shiftAmount = targetPos - actualTrigPos;
// 创建偏移后的数据数组
byte[] offsetData = new byte[sampleCount];
for (int i = 0; i < sampleCount; i++)
{
int sourceIndex = (i - shiftAmount + sampleCount) % sampleCount;
offsetData[i] = waveformData[sourceIndex];
}
// 刷新RAM
var refreshResult = await RefreshRAM();
if (!refreshResult.IsSuccessful)
{
logger.Error($"Failed to refresh RAM after reading waveform data: {refreshResult.Error}");
return new(refreshResult.Error);
}
return offsetData;
}
}

View File

@@ -0,0 +1,56 @@
using System.Net;
using DotNext;
namespace Peripherals.PowerClient;
class PowerAddr
{
public const UInt32 Base = 0x10_00_00_00;
public const UInt32 PowerCtrl = Base + 7;
}
/// <summary>
/// [TODO:description]
/// </summary>
public class Power
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout;
readonly int port;
readonly string address;
private IPEndPoint ep;
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="timeout">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public Power(string address, int port, int timeout = 1000)
{
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="enable">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<bool>> SetPowerOnOff(bool enable)
{
if (MsgBus.IsRunning)
MsgBus.UDPServer.ClearUDPData(this.address, 1);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(this.ep, 1, PowerAddr.PowerCtrl, Convert.ToUInt32(enable), this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
}

View File

@@ -1,25 +1,26 @@
using System.Net;
using DotNext;
namespace RemoteUpdateClient;
namespace Peripherals.RemoteUpdateClient;
static class RemoteUpdaterAddr
{
public const UInt32 Base = 0x20_00_00_00;
/// <summary>
/// <summary>
/// ADDR: 0X00: 写Flash-读写地址——控制位 <br/>
/// [31:16]: wr_sector_num <br/>
/// [15: 0]: {flash_wr_en,-,-,-, start_wr_sector} <br/>
/// </summary>
public const UInt32 WriteCtrl = Base + 0x00;
/// <summary>
/// <summary>
/// ADDR: 0X01: 写Flash-只写地址——FIFO入口 <br/>
/// [31:0]: 写比特流数据入口 <br/>
/// </summary>
public const UInt32 WriteFIFO = Base + 0x01;
/// <summary>
/// <summary>
/// ADDR: 0X02: 写Flash-只读地址——标志位 <br/>
/// [31:24]: {-, -, -, -, -, -, -, wr_fifo_full} <br/>
/// [23:16]: {-, -, -, -, -, -, -, wr_fifo_empty} <br/>
@@ -28,14 +29,14 @@ static class RemoteUpdaterAddr
/// </summary>
public const UInt32 WriteSign = Base + 0x02;
/// <summary>
/// <summary>
/// ADDR: 0X03: 读Flash-读写地址——控制位1 <br/>
/// [31:16]: rd_sector_num <br/>
/// [15: 0]: {flash_rd_en,-,-,-, start_rd_sub_sector} <br/>
/// </summary>
public const UInt32 ReadCtrl1 = Base + 0x03;
/// <summary>
/// <summary>
/// ADDR: 0X04: 读Flash-读写地址——控制位2 <br/>
/// [31:24]: { } <br/>
/// [23:16]: {-, -, -, -, -, -,{ bs_crc32_ok }} <br/>
@@ -44,19 +45,19 @@ static class RemoteUpdaterAddr
/// </summary>
public const UInt32 ReadCtrl2 = Base + 0x04;
/// <summary>
/// <summary>
/// ADDR: 0X05: 读Flash-只读地址——FIFO出口 <br/>
/// [31:0]: 读比特流数据出口 <br/>
/// </summary>
public const UInt32 ReadFIFO = Base + 0x05;
/// <summary>
/// <summary>
/// ADDR: 0X06: 读Flash-只读地址——CRC校验值 <br/>
/// [31:0]: CRC校验值 bs_readback_crc <br/>
/// </summary>
public const UInt32 ReadCRC = Base + 0x06;
/// <summary>
/// <summary>
/// ADDR: 0X07: 读Flash-只读地址——标志位 <br/>
/// [31:24]: {-, -, -, -, -, -, -, rd_fifo_afull} <br/>
/// [23:16]: {-, -, -, -, -, -, -, rd_fifo_empty} <br/>
@@ -65,14 +66,14 @@ static class RemoteUpdaterAddr
/// </summary>
public const UInt32 ReadSign = Base + 0x07;
/// <summary>
/// <summary>
/// ADDR: 0X08: 热启动开关-读写地址——控制位 <br/>
/// [31: 8]: hotreset_addr <br/>
/// [ 7: 0]: {-, -, -, -, -, -, -, hotreset_en} <br/>
/// </summary>
public const UInt32 HotResetCtrl = Base + 0x08;
/// <summary>
/// <summary>
/// ADDR: 0X09: 只读地址 版本号 <br/>
/// [31: 0]: FPGA_VERSION[31:0] <br/>
/// </summary>
@@ -98,7 +99,7 @@ public class RemoteUpdater
const int FLASH_SECTOR_LENGTH = 4 * 1024;
readonly int timeout = 2000;
readonly int timeoutForWait = 60 * 1000;
readonly int timeoutForWait = 20 * 1000;
readonly int port;
readonly string address;
@@ -142,7 +143,7 @@ public class RemoteUpdater
{
var ret = await UDPClientPool.WriteAddr(
this.ep, RemoteUpdaterAddr.WriteCtrl,
this.ep, 0, RemoteUpdaterAddr.WriteCtrl,
Convert.ToUInt32((writeSectorNum << 16) | (1 << 15) | Convert.ToInt32(flashAddr / 4096)), this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Enable write flash failed"));
@@ -150,23 +151,23 @@ public class RemoteUpdater
{
var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdaterAddr.WriteSign,
0x00_00_00_01, 0x00_00_00_01, this.timeoutForWait);
this.ep, 0, RemoteUpdaterAddr.WriteSign,
0x00_00_00_01, 0x00_00_00_01, 100, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception(
$"Flash clear failed after {this.timeoutForWait} milliseconds"));
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdaterAddr.WriteFIFO, bytesData, this.timeout);
var ret = await UDPClientPool.WriteAddr(this.ep, 0, RemoteUpdaterAddr.WriteFIFO, bytesData, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Send data to flash failed"));
}
{
var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdaterAddr.WriteSign,
0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
this.ep, 0, RemoteUpdaterAddr.WriteSign,
0x00_00_01_00, 0x00_00_01_00, 100, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
@@ -314,14 +315,14 @@ public class RemoteUpdater
private async ValueTask<Result<bool>> CheckBitstreamCRC(int bitstreamNum, int bitstreamLen, UInt32 checkSum)
{
{
var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdaterAddr.ReadCtrl2, 0x00_00_00_00, this.timeout);
var ret = await UDPClientPool.WriteAddr(this.ep, 0, RemoteUpdaterAddr.ReadCtrl2, 0x00_00_00_00, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write read control 2 failed"));
}
{
var ret = await UDPClientPool.WriteAddr(
this.ep, RemoteUpdaterAddr.ReadCtrl1,
this.ep, 0, RemoteUpdaterAddr.ReadCtrl1,
Convert.ToUInt32((bitstreamLen << 16) | (1 << 15) | Convert.ToInt32(FlashAddr.Bitstream[bitstreamNum] / 4096)),
this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
@@ -330,15 +331,15 @@ public class RemoteUpdater
{
var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdaterAddr.ReadSign,
0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
this.ep, 0, RemoteUpdaterAddr.ReadSign,
0x00_00_01_00, 0x00_00_01_00, 10, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception(
$"Read bitstream failed after {this.timeoutForWait} milliseconds"));
}
{
var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdaterAddr.ReadCRC, this.timeout);
var ret = await UDPClientPool.ReadAddrByte(this.ep, 0, RemoteUpdaterAddr.ReadCRC, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
var bytes = ret.Value.Options.Data;
@@ -368,7 +369,7 @@ public class RemoteUpdater
$"Bitsteam num should be 0 ~ 3 for HotRest, but given {bitstreamNum}", nameof(bitstreamNum)));
var ret = await UDPClientPool.WriteAddr(
this.ep, RemoteUpdaterAddr.HotResetCtrl,
this.ep, 0, RemoteUpdaterAddr.HotResetCtrl,
((FlashAddr.Bitstream[bitstreamNum] << 8) | 1), this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
@@ -381,7 +382,7 @@ public class RemoteUpdater
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<bool>> HotResetBitstream(int bitstreamNum)
{
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, 0);
logger.Trace("Clear udp data finished");
{
@@ -411,7 +412,7 @@ public class RemoteUpdater
byte[]? bitstream2,
byte[]? bitstream3)
{
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, 0);
logger.Trace("Clear udp data finished");
for (int bitstreamNum = 0; bitstreamNum < 4; bitstreamNum++)
@@ -462,7 +463,7 @@ public class RemoteUpdater
$"The length of data should be divided by 4096, bug given {bytesData.Length}", nameof(bytesData)));
var bitstreamBlockNum = bytesData.Length / (4 * 1024);
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, 0);
logger.Trace("Clear udp data finished");
{
@@ -538,11 +539,11 @@ public class RemoteUpdater
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<UInt32>> GetVersion()
{
await MsgBus.UDPServer.ClearUDPData(this.address);
MsgBus.UDPServer.ClearUDPData(this.address, 0);
logger.Trace("Clear udp data finished");
{
var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdaterAddr.Version, this.timeout);
var ret = await UDPClientPool.ReadAddrByte(this.ep, 0, RemoteUpdaterAddr.Version, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
var retData = ret.Value.Options.Data;

View File

@@ -0,0 +1,106 @@
using System.Net;
using DotNext;
using Tapper;
namespace Peripherals.RotaryEncoderClient;
class RotaryEncoderCtrlAddr
{
public const UInt32 BASE = 0xB0_00_00_30;
public const UInt32 PRESS_BASE = 0xB0_00_00_40;
public const UInt32 ENABLE = BASE;
public const UInt32 PRESS_ENABLE = PRESS_BASE;
}
[TranspilationSource]
public enum RotaryEncoderDirection : uint
{
CounterClockwise = 0,
Clockwise = 1,
}
[TranspilationSource]
public enum RotaryEncoderPressStatus : uint
{
Press = 0,
Release = 1,
}
public class RotaryEncoderCtrl
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 500;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
public RotaryEncoderCtrl(string address, int port, int taskID, int timeout = 500)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.taskID = taskID;
this.timeout = timeout;
}
public async ValueTask<Result<bool>> SetEnable(bool enable)
{
if (MsgBus.IsRunning)
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
else return new(new Exception("Message Bus not work!"));
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, RotaryEncoderCtrlAddr.ENABLE, enable ? 0x1U : 0x0U, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value)
{
logger.Error($"Set Rotary Encoder Enable failed: {ret.Error}");
return false;
}
}
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, RotaryEncoderCtrlAddr.PRESS_ENABLE, enable ? 0x1U : 0x0U, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
}
public async ValueTask<Result<bool>> RotateEncoderOnce(int num, RotaryEncoderDirection direction)
{
if (MsgBus.IsRunning)
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, RotaryEncoderCtrlAddr.BASE + (UInt32)num, (UInt32)direction, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Set Rotary Encoder Rotate {num} {direction.ToString()} failed: {ret.Error}");
return new(ret.Error);
}
return ret.Value;
}
public async ValueTask<Result<bool>> PressEncoderOnce(int num, RotaryEncoderPressStatus press)
{
if (MsgBus.IsRunning)
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, RotaryEncoderCtrlAddr.PRESS_BASE + (UInt32)num, (UInt32)press, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Set Rotary Encoder Set {num} {press.ToString()} failed: {ret.Error}");
return new(ret.Error);
}
return ret.Value;
}
}

View File

@@ -0,0 +1,79 @@
using System.Net;
using DotNext;
using Common;
namespace Peripherals.SevenDigitalTubesClient;
static class SevenDigitalTubesAddr
{
public const UInt32 BASE = 0xB000_0000;
}
public class SevenDigitalTubesCtrl
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 500;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
/// <summary>
/// 初始化七段数码管控制器
/// </summary>
/// <param name="address">七段数码管控制器IP地址</param>
/// <param name="port">七段数码管控制器端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="timeout">超时时间(毫秒)</param>
public SevenDigitalTubesCtrl(string address, int port, int taskID, int timeout = 500)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.taskID = taskID;
this.timeout = timeout;
}
public async ValueTask<Result<byte>> ReadTube(int num)
{
if (num < 0 || num > 31)
throw new ArgumentOutOfRangeException(nameof(num), "Tube number must be between 0 and 31");
var ret = await UDPClientPool.ReadAddrByte(
this.ep, this.taskID, SevenDigitalTubesAddr.BASE + (UInt32)num, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Read tubes failed: {ret.Error}");
return new(ret.Error);
}
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 4)
return new(new Exception("Data length is too short"));
var data = Number.BytesToUInt32(ret.Value.Options.Data, 0, 4).Value;
if ((data >> 8) != num)
{
logger.Error($"Read wrong tube number: {num} != {data >> 8}");
return new(new Exception($"Read wrong tube number: {num} != {data >> 8}"));
}
return (byte)(data & 0xFF);
}
public async ValueTask<Result<byte[]>> ScanAllTubes()
{
var tubes = new byte[32];
for (int i = 0; i < 32; i++)
{
var ret = await ReadTube(i);
if (!ret.IsSuccessful)
return new(ret.Error);
tubes[i] = ret.Value;
}
return tubes;
}
}

View File

@@ -0,0 +1,61 @@
using System.Net;
using DotNext;
namespace Peripherals.SwitchClient;
class SwitchCtrlAddr
{
public const UInt32 BASE = 0xB0_00_00_20;
public const UInt32 ENABLE = BASE;
}
public class SwitchCtrl
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 500;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
public SwitchCtrl(string address, int port, int taskID, int timeout = 500)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.taskID = taskID;
this.timeout = timeout;
}
public async ValueTask<Result<bool>> SetEnable(bool enable)
{
if (MsgBus.IsRunning)
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, SwitchCtrlAddr.ENABLE, enable ? 0x1U : 0x0U, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
public async ValueTask<Result<bool>> SetSwitchOnOff(int num, bool onOff)
{
if (MsgBus.IsRunning)
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, SwitchCtrlAddr.BASE + (UInt32)num, onOff ? 0x1U : 0x0U, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Set Switch {onOff} failed: {ret.Error}");
return new(ret.Error);
}
return ret.Value;
}
}

View File

@@ -0,0 +1,170 @@
using System.Net;
using DotNext;
using Tapper;
namespace Peripherals.WS2812Client;
class WS2812Addr
{
public const UInt32 BASE = 0xB0_00_01_00;
public const int LED_COUNT = 128;
}
/// <summary>
/// RGB颜色结构体包含红、绿、蓝三个颜色分量
/// </summary>
[TranspilationSource]
public class RGBColor
{
public byte Red { get; set; }
public byte Green { get; set; }
public byte Blue { get; set; }
public RGBColor(byte red, byte green, byte blue)
{
Red = red;
Green = green;
Blue = blue;
}
/// <summary>
/// 从32位数据的低24位提取RGB颜色
/// </summary>
/// <param name="data">32位数据</param>
/// <returns>RGB颜色</returns>
public static RGBColor FromUInt32(UInt32 data)
{
return new RGBColor(
(byte)((data >> 16) & 0xFF), // Red
(byte)((data >> 8) & 0xFF), // Green
(byte)(data & 0xFF) // Blue
);
}
/// <summary>
/// 转换为32位数据格式
/// </summary>
/// <returns>32位数据</returns>
public UInt32 ToUInt32()
{
return ((UInt32)Red << 16) | ((UInt32)Green << 8) | (UInt32)Blue;
}
public override string ToString()
{
return $"RGB({Red}, {Green}, {Blue})";
}
}
public class WS2812Client
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 500;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
public WS2812Client(string address, int port, int taskID, int timeout = 500)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.taskID = taskID;
this.timeout = timeout;
}
/// <summary>
/// 获取指定灯珠的RGB颜色
/// </summary>
/// <param name="ledIndex">灯珠索引范围0-127</param>
/// <returns>RGB颜色结果</returns>
public async ValueTask<Result<RGBColor>> GetLedColor(int ledIndex)
{
if (ledIndex < 0 || ledIndex >= WS2812Addr.LED_COUNT)
{
return new(new ArgumentOutOfRangeException(nameof(ledIndex),
$"LED index must be between 0 and {WS2812Addr.LED_COUNT - 1}"));
}
if (MsgBus.IsRunning)
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
else
return new(new Exception("Message Bus not work!"));
var addr = WS2812Addr.BASE + (UInt32)(ledIndex * 4); // 每个地址32位步长为4字节
var ret = await UDPClientPool.ReadAddrByte(this.ep, this.taskID, addr, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Get LED {ledIndex} color failed: {ret.Error}");
return new(ret.Error);
}
var retData = ret.Value.Options.Data;
if (retData is null)
return new(new Exception($"Device {address} receive none"));
if (retData.Length < 4)
{
var error = new Exception($"Invalid data length: expected 4 bytes, got {retData.Length}");
logger.Error($"Get LED {ledIndex} color failed: {error}");
return new(error);
}
var colorData = Convert.ToUInt32(Common.Number.BytesToUInt64(retData).Value);
var color = RGBColor.FromUInt32(colorData);
return new(color);
}
/// <summary>
/// 获取所有灯珠的RGB颜色
/// </summary>
/// <returns>包含所有灯珠颜色的数组</returns>
public async ValueTask<Result<RGBColor[]>> GetAllLedColors()
{
if (MsgBus.IsRunning)
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
else
return new(new Exception("Message Bus not work!"));
try
{
// 一次性读取所有LED数据每个LED占用4字节总共128*4=512字节
var ret = await UDPClientPool.ReadAddr4Bytes(this.ep, this.taskID, WS2812Addr.BASE, WS2812Addr.LED_COUNT, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Get all LED colors failed: {ret.Error}");
return new(ret.Error);
}
var data = ret.Value;
var expectedLength = WS2812Addr.LED_COUNT * 4; // 128 * 4 = 512 bytes
if (data.Length < expectedLength)
{
var error = new Exception($"Invalid data length: expected {expectedLength} bytes, got {data.Length}");
logger.Error(error.Message);
return new(error);
}
var colors = new RGBColor[WS2812Addr.LED_COUNT];
for (int i = 0; i < WS2812Addr.LED_COUNT; i++)
{
var offset = i * 4;
// 将4字节数据转换为UInt32
var colorData = BitConverter.ToUInt32(data, offset);
colors[i] = RGBColor.FromUInt32(colorData);
}
return new(colors);
}
catch (Exception ex)
{
logger.Error($"Get all LED colors failed: {ex}");
return new(ex);
}
}
}

View File

@@ -0,0 +1,496 @@
using System.Net;
using System.Collections.Concurrent;
using Peripherals.HdmiInClient;
using Peripherals.JpegClient;
namespace server.Services;
public class HdmiVideoStreamEndpoint
{
public string BoardId { get; set; } = "";
public string MjpegUrl { get; set; } = "";
public string VideoUrl { get; set; } = "";
public string SnapshotUrl { get; set; } = "";
}
public class HdmiVideoStreamClient
{
public required HdmiIn HdmiInClient { get; set; }
// public required Jpeg JpegClient { get; set; }
public required CancellationTokenSource CTS { get; set; }
public required int Offset { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
public class HttpHdmiVideoStreamService : BackgroundService
{
private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private HttpListener? _httpListener;
private readonly int _serverPort = 4322;
private readonly ConcurrentDictionary<string, HdmiVideoStreamClient> _clientDict = new();
public override async Task StartAsync(CancellationToken cancellationToken)
{
_httpListener = new HttpListener();
_httpListener.Prefixes.Add($"http://{Global.LocalHost}:{_serverPort}/");
_httpListener.Start();
logger.Info($"HDMI Video Stream Service started on port {_serverPort}");
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (_httpListener == null) continue;
try
{
logger.Debug("Waiting for HTTP request...");
var contextTask = _httpListener.GetContextAsync();
var completedTask = await Task.WhenAny(contextTask, Task.Delay(-1, stoppingToken));
if (completedTask == contextTask)
{
var context = contextTask.Result;
logger.Debug($"Received request: {context.Request.Url?.AbsolutePath}");
if (context != null)
_ = HandleRequestAsync(context, stoppingToken);
}
else
{
break;
}
}
catch (Exception ex)
{
logger.Error(ex, "Error in GetContextAsync");
break;
}
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
logger.Info("Stopping HDMI Video Stream Service...");
// 禁用所有活跃的HDMI传输
var disableTasks = new List<Task>();
foreach (var hdmiKey in _clientDict.Keys)
{
disableTasks.Add(DisableHdmiTransmissionAsync(hdmiKey));
}
// 等待所有禁用操作完成
await Task.WhenAll(disableTasks);
// 清空字典
_clientDict.Clear();
await base.StopAsync(cancellationToken);
}
public async Task DisableHdmiTransmissionAsync(string key)
{
try
{
var client = _clientDict[key];
client.CTS.Cancel();
// var disableResult = await client.JpegClient.SetEnable(false);
var disableResult = await client.HdmiInClient.SetTransEnable(false);
if (disableResult)
{
logger.Info("Successfully disabled HDMI transmission");
}
else
{
logger.Error($"Failed to disable HDMI transmission");
}
client.CTS = new CancellationTokenSource();
}
catch (Exception ex)
{
logger.Error(ex, "Exception occurred while disabling HDMI transmission");
}
}
private async Task<HdmiVideoStreamClient?> GetOrCreateClientAsync(string boardId)
{
if (!_clientDict.TryGetValue(boardId, out var client))
{
var userManager = new Database.UserManager();
var boardRet = userManager.GetBoardByID(Guid.Parse(boardId));
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
{
logger.Error($"Failed to get board with ID {boardId}");
return null;
}
var board = boardRet.Value.Value;
client = new HdmiVideoStreamClient()
{
HdmiInClient = new HdmiIn(board.IpAddr, board.Port, 9),
// JpegClient = new Jpeg(board.IpAddr, board.Port, 1),
CTS = new CancellationTokenSource(),
Offset = 0
};
}
// 启用HDMI传输
try
{
var hdmiEnableRet = await client.HdmiInClient.Init(true);
if (!hdmiEnableRet.IsSuccessful)
{
logger.Error($"Failed to enable HDMI transmission for board {boardId}: {hdmiEnableRet.Error}");
return null;
}
logger.Info($"Successfully enabled HDMI transmission for board {boardId}");
// var jpegEnableRet = await client.JpegClient.Init(true);
// if (!jpegEnableRet.IsSuccessful)
// {
// logger.Error($"Failed to enable JPEG transmission for board {boardId}: {jpegEnableRet.Error}");
// return null;
// }
// logger.Info($"Successfully enabled JPEG transmission for board {boardId}");
client.Width = client.HdmiInClient.Width;
client.Height = client.HdmiInClient.Height;
// client.Width = client.JpegClient.Width;
// client.Height = client.JpegClient.Height;
}
catch (Exception ex)
{
logger.Error(ex, $"Exception occurred while enabling HDMI transmission for board {boardId}");
return null;
}
_clientDict[boardId] = client;
return client;
}
private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken cancellationToken)
{
var path = context.Request.Url?.AbsolutePath ?? "/";
var boardId = context.Request.QueryString["boardId"];
if (string.IsNullOrEmpty(boardId))
{
await SendErrorAsync(context.Response, "Missing boardId");
return;
}
var client = await GetOrCreateClientAsync(boardId);
if (client == null)
{
await SendErrorAsync(context.Response, "Invalid boardId or board not available");
return;
}
var token = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, client.CTS.Token).Token;
if (path == "/snapshot")
{
await HandleSnapshotRequestAsync(context.Response, client, token);
}
else if (path == "/mjpeg")
{
await HandleMjpegStreamAsync(context.Response, client, token);
}
else if (path == "/video")
{
await SendVideoHtmlPageAsync(context.Response, boardId);
}
else
{
await SendIndexHtmlPageAsync(context.Response, boardId);
}
}
private async Task HandleSnapshotRequestAsync(
HttpListenerResponse response, HdmiVideoStreamClient client, CancellationToken cancellationToken)
{
try
{
logger.Debug("处理HDMI快照请求");
// 从HDMI读取RGB565数据
// var frameResult = await client.JpegClient.GetMultiFrames((uint)client.Offset);
// if (!frameResult.IsSuccessful || frameResult.Value == null || frameResult.Value.Count == 0)
// {
// logger.Error("HDMI快照获取失败");
// response.StatusCode = 500;
// var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get HDMI snapshot");
// await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
// response.Close();
// return;
// }
// var jpegData = frameResult.Value[0];
// var quantTableResult = await client.JpegClient.GetQuantizationTable();
// if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
// {
// logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
// response.StatusCode = 500;
// var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table");
// await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
// response.Close();
// return;
// }
// var jpegImage = Common.Image.CompleteJpegData(jpegData, client.Width, client.Height, quantTableResult.Value);
// if (!jpegImage.IsSuccessful)
// {
// logger.Error("JPEG数据补全失败");
// response.StatusCode = 500;
// var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to complete JPEG data");
// await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
// response.Close();
// return;
// }
var jpegImage = await client.HdmiInClient.GetMJpegFrame();
if (!jpegImage.HasValue)
{
logger.Error("获取HDMI MJPEG帧失败");
response.StatusCode = 500;
var errorBytes = System.Text.Encoding.UTF8.GetBytes("Failed to get HDMI MJPEG frame");
await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length, cancellationToken);
response.Close();
return;
}
// 设置响应头参考Camera版本
response.ContentType = "image/jpeg";
response.ContentLength64 = jpegImage.Value.data.Length;
response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
await response.OutputStream.WriteAsync(jpegImage.Value.data, 0, jpegImage.Value.data.Length, cancellationToken);
await response.OutputStream.FlushAsync(cancellationToken);
logger.Debug("已发送HDMI快照图像大小{Size} 字节", jpegImage.Value.data.Length);
}
catch (Exception ex)
{
logger.Error(ex, "处理HDMI快照请求时出错");
response.StatusCode = 500;
}
finally
{
response.StatusCode = 200;
response.Close();
}
}
private async Task HandleMjpegStreamAsync(
HttpListenerResponse response, HdmiVideoStreamClient client, CancellationToken cancellationToken)
{
try
{
// 设置MJPEG流的响应头参考Camera版本
response.ContentType = "multipart/x-mixed-replace; boundary=--boundary";
response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
response.Headers.Add("Pragma", "no-cache");
response.Headers.Add("Expires", "0");
logger.Debug("开始HDMI MJPEG流传输");
// var quantTableResult = await client.JpegClient.GetQuantizationTable();
// if (!quantTableResult.IsSuccessful || quantTableResult.Value == null)
// {
// logger.Error("获取JPEG量化表失败: {Error}", quantTableResult.Error);
// response.StatusCode = 500;
// await response.OutputStream.WriteAsync(
// System.Text.Encoding.UTF8.GetBytes("Failed to get quantization table"), 0, 0, cancellationToken);
// response.Close();
// return;
// }
// var quantTable = quantTableResult.Value;
int frameCounter = 0;
while (!cancellationToken.IsCancellationRequested)
{
var frameStartTime = DateTime.UtcNow;
var frameRet = await client.HdmiInClient.GetMJpegFrame();
if (!frameRet.HasValue)
{
logger.Error("获取HDMI帧失败");
continue;
}
var frame = frameRet.Value;
await response.OutputStream.WriteAsync(frame.header, 0, frame.header.Length, cancellationToken);
await response.OutputStream.WriteAsync(frame.data, 0, frame.data.Length, cancellationToken);
await response.OutputStream.WriteAsync(frame.footer, 0, frame.footer.Length, cancellationToken);
await response.OutputStream.FlushAsync(cancellationToken);
frameCounter++;
var totalTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds;
// 性能统计日志每30帧记录一次
if (frameCounter % 30 == 0)
{
logger.Debug("HDMI帧 {FrameNumber} 性能统计 - 总计: {TotalTime:F1}ms, JPEG大小: {JpegSize} 字节",
frameCounter, totalTime, frame.data.Length);
}
// var frameResult =
// await client.JpegClient.GetMultiFrames((uint)client.Offset);
// if (!frameResult.IsSuccessful || frameResult.Value == null || frameResult.Value.Count == 0)
// {
// logger.Error("获取HDMI帧失败");
// await Task.Delay(100, cancellationToken);
// continue;
// }
// foreach (var framebytes in frameResult.Value)
// {
// var jpegImage = Common.Image.CompleteJpegData(framebytes, client.Width, client.Height, quantTable);
// if (!jpegImage.IsSuccessful)
// {
// logger.Error("JPEG数据不完整");
// await Task.Delay(100, cancellationToken);
// continue;
// }
// var frameRet = Common.Image.CreateMjpegFrameFromJpeg(jpegImage.Value);
// if (!frameRet.IsSuccessful)
// {
// logger.Error("创建MJPEG帧失败");
// await Task.Delay(100, cancellationToken);
// continue;
// }
// var frame = frameRet.Value;
// await response.OutputStream.WriteAsync(frame.header, 0, frame.header.Length, cancellationToken); // await response.OutputStream.WriteAsync(frame.data, 0, frame.data.Length, cancellationToken);
// await response.OutputStream.WriteAsync(frame.footer, 0, frame.footer.Length, cancellationToken);
// await response.OutputStream.FlushAsync(cancellationToken);
// frameCounter++;
// var totalTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds;
// // 性能统计日志每30帧记录一次
// if (frameCounter % 30 == 0)
// {
// logger.Debug("HDMI帧 {FrameNumber} 性能统计 - 总计: {TotalTime:F1}ms, JPEG大小: {JpegSize} 字节",
// frameCounter, totalTime, frame.data.Length);
// }
// }
}
}
catch (Exception ex)
{
logger.Error(ex, "HDMI MJPEG流处理异常");
}
finally
{
try
{
// 停止传输时禁用HDMI传输
await client.HdmiInClient.SetTransEnable(false);
logger.Info("已禁用HDMI传输");
}
catch (Exception ex)
{
logger.Error(ex, "禁用HDMI传输时出错");
}
try
{
response.Close();
}
catch
{
// 忽略关闭时的错误
}
logger.Debug("HDMI MJPEG流连接已关闭");
}
}
private async Task SendVideoHtmlPageAsync(HttpListenerResponse response, string boardId)
{
string html = $@"<html><body>
<h1>HDMI Video Stream for Board {boardId}</h1>
<img src='/mjpeg?boardId={boardId}' />
</body></html>";
response.ContentType = "text/html";
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(html));
response.Close();
}
private async Task SendIndexHtmlPageAsync(HttpListenerResponse response, string boardId)
{
string html = $@"<html><body>
<h1>Welcome to HDMI Video Stream Service</h1>
<a href='/video?boardId={boardId}'>View Video Stream for Board {boardId}</a>
</body></html>";
response.ContentType = "text/html";
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(html));
response.Close();
}
private async Task SendErrorAsync(HttpListenerResponse response, string message)
{
response.StatusCode = 400;
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message));
response.Close();
}
/// <summary>
/// 获取所有可用的HDMI视频流终端点
/// </summary>
/// <returns>返回所有可用的HDMI视频流终端点列表</returns>
public List<HdmiVideoStreamEndpoint>? GetAllVideoEndpoints()
{
var userManager = new Database.UserManager();
var boards = userManager.GetAllBoard();
if (boards == null)
return null;
var endpoints = new List<HdmiVideoStreamEndpoint>();
foreach (var board in boards)
{
endpoints.Add(new HdmiVideoStreamEndpoint
{
BoardId = board.ID.ToString(),
MjpegUrl = $"http://{Global.LocalHost}:{_serverPort}/mjpeg?boardId={board.ID}",
VideoUrl = $"http://{Global.LocalHost}:{_serverPort}/video?boardId={board.ID}",
SnapshotUrl = $"http://{Global.LocalHost}:{_serverPort}/snapshot?boardId={board.ID}"
});
}
return endpoints;
}
/// <summary>
/// 获取指定板卡ID的HDMI视频流终端点
/// </summary>
/// <param name="boardId">板卡ID</param>
/// <returns>返回指定板卡的HDMI视频流终端点</returns>
public HdmiVideoStreamEndpoint GetVideoEndpoint(string boardId)
{
return new HdmiVideoStreamEndpoint
{
BoardId = boardId,
MjpegUrl = $"http://{Global.LocalHost}:{_serverPort}/mjpeg?boardId={boardId}",
VideoUrl = $"http://{Global.LocalHost}:{_serverPort}/video?boardId={boardId}",
SnapshotUrl = $"http://{Global.LocalHost}:{_serverPort}/snapshot?boardId={boardId}"
};
}
}

View File

@@ -0,0 +1,826 @@
using System.Net;
using System.Text;
using System.Collections.Concurrent;
using DotNext;
using DotNext.Threading;
namespace server.Services;
public class VideoStreamClient
{
public string? ClientId { get; set; } = string.Empty;
public bool IsEnabled { get; set; } = true;
public int FrameWidth { get; set; }
public int FrameHeight { get; set; }
public int FrameRate { get; set; }
public AsyncLazy<Peripherals.CameraClient.Camera> Camera { get; set; }
public CancellationTokenSource CTS { get; set; }
public readonly AsyncReaderWriterLock Lock = new();
public VideoStreamClient(
string clientId, int width, int height, AsyncLazy<Peripherals.CameraClient.Camera> camera)
{
ClientId = clientId;
FrameWidth = width;
FrameHeight = height;
FrameRate = 30;
Camera = camera;
CTS = new CancellationTokenSource();
}
}
/// <summary>
/// 表示摄像头连接状态信息
/// </summary>
public class VideoStreamEndpoint
{
public required string BoardId { get; set; } = "";
public required string MjpegUrl { get; set; } = "";
public required string VideoUrl { get; set; } = "";
public required string SnapshotUrl { get; set; } = "";
public required string HtmlUrl { get; set; } = "";
public required string UsbCameraUrl { get; set; } = "";
public required bool IsEnabled { get; set; }
/// <summary>
/// 视频流的帧率FPS
/// </summary>
public required int FrameRate { get; set; }
public int FrameWidth { get; set; }
public int FrameHeight { get; set; }
/// <summary>
/// 视频分辨率(如 640x480
/// </summary>
public string Resolution => $"{FrameWidth}x{FrameHeight}";
}
/// <summary>
/// 表示视频流服务的运行状态
/// </summary>
public class VideoStreamServiceStatus
{
/// <summary>
/// 服务是否正在运行
/// </summary>
public bool IsRunning { get; set; }
/// <summary>
/// 服务监听的端口号
/// </summary>
public int ServerPort { get; set; }
/// <summary>
/// 当前连接的客户端端点列表
/// </summary>
public List<VideoStreamEndpoint> ClientEndpoints { get; set; } = new();
/// <summary>
/// 当前连接的客户端数量
/// </summary>
public int ConnectedClientsNum => ClientEndpoints.Count;
}
/// <summary>
/// HTTP 视频流服务,用于从 FPGA 获取图像数据并推送到前端网页
/// 支持动态配置摄像头地址和端口
/// </summary>
public class HttpVideoStreamService : BackgroundService
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private HttpListener? _httpListener;
private readonly int _serverPort = 4321;
private readonly ConcurrentDictionary<string, VideoStreamClient> _clientDict = new();
// USB Camera 相关
private AsyncLazy<UsbCameraCapture> _usbCamera = new(async token => await InitializeUsbCamera(token));
private static async Task<UsbCameraCapture> InitializeUsbCamera(CancellationToken token)
{
try
{
var camera = new UsbCameraCapture();
var devices = camera.GetDevices();
for (int i = 0; i < devices.Count; i++)
logger.Info($"Device[{i}]: {devices[i].Name}");
await camera.StartAsync(1, 2592, 1994, 30);
return camera;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to start USB camera");
throw;
}
}
private Optional<VideoStreamClient> TryGetClient(string boardId)
{
return _clientDict.TryGetValue(boardId, out var client) ? client : null;
}
private Optional<VideoStreamClient> GetOrCreateClient(
string boardId, int initWidth, int initHeight)
{
if (_clientDict.TryGetValue(boardId, out var client))
{
// 可在此处做分辨率/Camera等配置更新
return client;
}
var userManager = new Database.UserManager();
var boardRet = userManager.GetBoardByID(Guid.Parse(boardId));
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
{
logger.Error($"Failed to get board with ID {boardId}");
return null;
}
var board = boardRet.Value.Value;
var camera = new AsyncLazy<Peripherals.CameraClient.Camera>(async (_) =>
{
var camera = new Peripherals.CameraClient.Camera(board.IpAddr, board.Port);
var ret = await camera.Init();
if (!ret.IsSuccessful || !ret.Value)
{
logger.Error("Camera Init Failed!");
throw new Exception("Camera Init Failed!");
}
return camera;
});
client = new VideoStreamClient(boardId, initWidth, initHeight, camera);
_clientDict[boardId] = client;
return client;
}
/// <summary>
/// 初始化 HttpVideoStreamService
/// </summary>
public override async Task StartAsync(CancellationToken cancellationToken)
{
_httpListener = new HttpListener();
_httpListener.Prefixes.Add($"http://{Global.LocalHost}:{_serverPort}/");
_httpListener.Start();
logger.Info($"Video Stream Service started on port {_serverPort}");
await base.StartAsync(cancellationToken);
}
/// <summary>
/// 停止 HTTP 视频流服务
/// </summary>
public override async Task StopAsync(CancellationToken cancellationToken)
{
foreach (var clientKey in _clientDict.Keys)
{
var client = _clientDict[clientKey];
client.CTS.Cancel();
if (!client.Camera.IsValueCreated) continue;
using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
var camera = await client.Camera.WithCancellation(cancellationToken);
await camera.EnableHardwareTrans(false);
}
}
_clientDict.Clear();
await base.StopAsync(cancellationToken);
}
/// <summary>
/// 执行 HTTP 视频流服务
/// </summary>
/// <param name="stoppingToken">取消令牌</param>
/// <returns>任务</returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (_httpListener == null) continue;
try
{
logger.Debug("Waiting for HTTP request...");
var contextTask = _httpListener.GetContextAsync();
var completedTask = await Task.WhenAny(contextTask, Task.Delay(-1, stoppingToken));
if (completedTask == contextTask)
{
var context = contextTask.Result;
logger.Debug($"Received request: {context.Request.Url?.AbsolutePath}");
if (context != null)
_ = HandleRequestAsync(context, stoppingToken);
}
else
{
break;
}
}
catch (Exception ex)
{
logger.Error(ex, "Error in GetContextAsync");
break;
}
}
}
private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken cancellationToken)
{
var path = context.Request.Url?.AbsolutePath ?? "/";
var boardId = context.Request.QueryString["boardId"];
if (string.IsNullOrEmpty(boardId))
{
await SendErrorAsync(context.Response, "Missing clientId");
return;
}
var width = int.TryParse(context.Request.QueryString["width"], out var w) ? w : 640;
var height = int.TryParse(context.Request.QueryString["height"], out var h) ? h : 480;
var clientOpt = GetOrCreateClient(boardId, width, height);
if (!clientOpt.HasValue)
{
await SendErrorAsync(context.Response, "Invalid clientId or camera not available");
return;
}
var client = clientOpt.Value;
var token = CancellationTokenSource.CreateLinkedTokenSource(
client.CTS.Token, cancellationToken).Token;
try
{
token.ThrowIfCancellationRequested();
logger.Info("新HTTP客户端连接: {RemoteEndPoint}", context.Request.RemoteEndPoint);
if (path == "/video")
{
// MJPEG 流请求FPGA
await HandleMjpegStreamAsync(context.Response, client, token);
}
else if (path == "/usbCamera")
{
// USB Camera MJPEG流请求
await HandleUsbCameraStreamAsync(context.Response, client, token);
}
else if (path == "/snapshot")
{
// 单帧图像请求
await HandleSnapshotRequestAsync(context.Response, client, token);
}
else if (path == "/html")
{
// HTML页面请求
await SendIndexHtmlPageAsync(context.Response);
}
else
{
// 默认返回简单的HTML页面提供链接到视频页面
await SendIndexHtmlPageAsync(context.Response);
}
}
catch (Exception ex)
{
logger.Error(ex, "接受HTTP客户端连接时发生错误");
}
}
private async Task SendErrorAsync(HttpListenerResponse response, string message)
{
response.StatusCode = 400;
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message));
response.Close();
}
// USB Camera MJPEG流处理
private async Task HandleUsbCameraStreamAsync(
HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
{
var camera = await _usbCamera.WithCancellation(cancellationToken);
Action<byte[]> frameHandler = async (jpegData) =>
{
try
{
var header = Encoding.ASCII.GetBytes("--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + jpegData.Length + "\r\n\r\n");
await response.OutputStream.WriteAsync(header, 0, header.Length, cancellationToken);
await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken);
await response.OutputStream.WriteAsync(new byte[] { 0x0D, 0x0A }, 0, 2, cancellationToken); // \r\n
await response.OutputStream.FlushAsync(cancellationToken);
}
catch
{
logger.Error("Error sending MJPEG frame");
}
};
try
{
if (!camera.IsCapturing)
{
logger.Error("USB Camera is not capturing");
response.StatusCode = 500;
await response.OutputStream.FlushAsync(cancellationToken);
response.Close();
return;
}
response.ContentType = "multipart/x-mixed-replace; boundary=--boundary";
response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
response.Headers.Add("Pragma", "no-cache");
response.Headers.Add("Expires", "0");
logger.Info("Start USB Camera MJPEG Stream");
camera.FrameReady += frameHandler;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(-1, cancellationToken);
}
}
catch (OperationCanceledException)
{
logger.Info("USB Camera MJPEG 串流取消");
}
catch (Exception ex)
{
logger.Error(ex, "USB Camera MJPEG流处理异常");
}
finally
{
camera.FrameReady -= frameHandler;
logger.Info("Usb Camera Stream Stopped");
try { response.Close(); } catch { }
}
}
private async Task HandleSnapshotRequestAsync(
HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
{
// 读取 Camera 快照,返回 JPEG
var camera = await client.Camera.WithCancellation(cancellationToken);
var frameResult = await camera.ReadFrame();
if (!frameResult.IsSuccessful || frameResult.Value == null)
{
response.StatusCode = 500;
await response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("Failed to get snapshot"));
response.Close();
return;
}
var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameResult.Value, client.FrameWidth, client.FrameHeight, 80);
if (!jpegResult.IsSuccessful)
{
response.StatusCode = 500;
await response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("JPEG conversion failed"));
response.Close();
return;
}
response.ContentType = "image/jpeg";
response.ContentLength64 = jpegResult.Value.Length;
await response.OutputStream.WriteAsync(jpegResult.Value, 0, jpegResult.Value.Length, cancellationToken);
response.Close();
}
private async Task HandleMjpegStreamAsync(
HttpListenerResponse response, VideoStreamClient client, CancellationToken cancellationToken)
{
response.ContentType = "multipart/x-mixed-replace; boundary=--boundary";
response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
response.Headers.Add("Pragma", "no-cache");
response.Headers.Add("Expires", "0");
var camera = await client.Camera.WithCancellation(cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
var frameResult = await camera.ReadFrame();
if (!frameResult.IsSuccessful || frameResult.Value == null) continue;
var jpegResult = Common.Image.ConvertRGB24ToJpeg(frameResult.Value, client.FrameWidth, client.FrameHeight, 80);
if (!jpegResult.IsSuccessful) continue;
var header = Encoding.ASCII.GetBytes("--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: " + jpegResult.Value.Length + "\r\n\r\n");
await response.OutputStream.WriteAsync(header, 0, header.Length, cancellationToken);
await response.OutputStream.WriteAsync(jpegResult.Value, 0, jpegResult.Value.Length, cancellationToken);
await response.OutputStream.WriteAsync(new byte[] { 0x0D, 0x0A }, 0, 2, cancellationToken);
await response.OutputStream.FlushAsync(cancellationToken);
await Task.Delay(1000 / client.FrameWidth, cancellationToken);
}
response.Close();
}
private async Task SendVideoHtmlPageAsync(HttpListenerResponse response)
{
string html = $@"
<!DOCTYPE html>
<html>
<head>
<title>FPGA 视频流</title>
<meta charset=""utf-8"">
<style>
body {{ font-family: Arial, sans-serif; text-align: center; margin: 20px; }}
h1 {{ color: #333; }}
.video-container {{ margin: 20px auto; max-width: 800px; }}
.controls {{ margin: 10px 0; }}
img {{ max-width: 100%; border: 1px solid #ddd; }}
button {{ padding: 8px 16px; margin: 0 5px; cursor: pointer; }}
</style>
</head>
<body>
<h1>FPGA 实时视频流</h1>
<div class=""video-container"">
<img id=""videoStream"" src=""/video-stream"" alt=""FPGA视频流"" />
</div>
<div class=""controls"">
<button onclick=""document.getElementById('videoStream').src='/snapshot?t=' + new Date().getTime()"">刷新快照</button>
<button onclick=""document.getElementById('videoStream').src='/video-stream'"">开始流媒体</button>
<span id=""status"">状态: 连接中...</span>
</div>
<script>
document.getElementById('videoStream').onload = function() {{
document.getElementById('status').textContent = '状态: 已连接';
}};
document.getElementById('videoStream').onerror = function() {{
document.getElementById('status').textContent = '状态: 连接错误';
}};
</script>
</body>
</html>
";
response.ContentType = "text/html";
response.ContentEncoding = Encoding.UTF8;
byte[] buffer = Encoding.UTF8.GetBytes(html);
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length);
response.Close();
}
private async Task SendIndexHtmlPageAsync(HttpListenerResponse response)
{
string html = $@"
<!DOCTYPE html>
<html>
<head>
<title>FPGA WebLab 视频服务</title>
<meta charset=""utf-8"">
<style>
body {{ font-family: Arial, sans-serif; text-align: center; margin: 20px; }}
h1 {{ color: #333; }}
.links {{ margin: 20px; }}
a {{ padding: 10px 15px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 4px; margin: 5px; display: inline-block; }}
a:hover {{ background-color: #45a049; }}
</style>
</head>
<body>
<h1>FPGA WebLab 视频服务</h1>
<div class=""links"">
<a href=""/video-feed.html"">观看实时视频</a>
<a href=""/snapshot"" target=""_blank"">获取当前快照</a>
</div>
<p>HTTP流媒体服务端口: {_serverPort}</p>
</body>
</html>
";
response.ContentType = "text/html";
response.ContentEncoding = Encoding.UTF8;
byte[] buffer = Encoding.UTF8.GetBytes(html);
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length);
response.Close();
}
/// <summary>
/// 从 FPGA 获取图像数据
/// 实际从摄像头读取 RGB565 格式数据并转换为 RGB24
/// </summary>
private async Task<byte[]?> GetFPGAImageData(
VideoStreamClient client, CancellationToken cancellationToken = default)
{
try
{
using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
// 从摄像头读取帧数据
var readStartTime = DateTime.UtcNow;
var camera = await client.Camera.WithCancellation(cancellationToken);
var result = await camera.ReadFrame();
var readEndTime = DateTime.UtcNow;
var readTime = (readEndTime - readStartTime).TotalMilliseconds;
if (!result.IsSuccessful)
{
logger.Error("读取摄像头帧数据失败: {Error}", result.Error);
return new byte[0];
}
var rgb565Data = result.Value;
// 验证数据长度是否正确
if (!Common.Image.ValidateImageDataLength(rgb565Data, client.FrameWidth, client.FrameHeight, 2))
{
logger.Warn("摄像头数据长度不匹配,期望: {Expected}, 实际: {Actual}",
client.FrameWidth * client.FrameHeight * 2, rgb565Data.Length);
}
// 将 RGB565 转换为 RGB24
var convertStartTime = DateTime.UtcNow;
var rgb24Result = Common.Image.ConvertRGB565ToRGB24(rgb565Data, client.FrameWidth, client.FrameHeight, isLittleEndian: false);
var convertEndTime = DateTime.UtcNow;
var convertTime = (convertEndTime - convertStartTime).TotalMilliseconds;
if (!rgb24Result.IsSuccessful)
{
logger.Error("RGB565转RGB24失败: {Error}", rgb24Result.Error);
return new byte[0];
}
return rgb24Result.Value;
}
}
catch (Exception ex)
{
logger.Error(ex, "获取FPGA图像数据时发生错误");
return new byte[0];
}
}
/// <summary>
/// 设置视频流分辨率
/// </summary>
/// <param name="boardId">板卡ID</param>
/// <param name="width">宽度</param>
/// <param name="height">高度</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>设置结果</returns>
public async Task<Result<bool>> SetResolutionAsync(
string boardId, int width, int height,
int timeout = 100, CancellationToken cancellationToken = default)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
using (await client.Lock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(timeout), cancellationToken))
{
var currentCamera = await client.Camera.WithCancellation(cancellationToken);
if (currentCamera == null)
{
var message = $"获取摄像头失败";
logger.Error(message);
return new(new Exception(message));
}
// 设置摄像头分辨率
var ret = await currentCamera.ChangeResolution(width, height);
if (!ret.IsSuccessful)
{
var message = $"设置摄像头分辨率失败: {ret.Error}";
logger.Error(message);
return new(new Exception(message));
}
if (!ret.Value)
{
logger.Warn($"设置摄像头分辨率失败");
return false;
}
// 更新HTTP服务的分辨率配置
client.FrameWidth = width;
client.FrameHeight = height;
logger.Info($"视频流分辨率已成功设置为 {width}x{height}");
return true;
}
}
catch (Exception ex)
{
var message = $"设置分辨率时发生错误: {ex.Message}";
logger.Error(ex, message);
return new(new Exception(message));
}
}
/// <summary>
/// 初始化摄像头自动对焦功能
/// </summary>
/// <returns>初始化结果</returns>
public async Task<bool> InitAutoFocusAsync(
string boardId, int timeout = 1000, CancellationToken cancellationToken = default)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
using (await client.Lock.AcquireWriteLockAsync(
TimeSpan.FromMilliseconds(timeout), cancellationToken))
{
var camera = await client.Camera.WithCancellation(cancellationToken);
var result = await camera.InitAutoFocus();
if (result.IsSuccessful && result.Value)
{
logger.Info($"Board{boardId}摄像头自动对焦功能初始化成功");
return true;
}
else
{
logger.Error($"Board{boardId}摄像头自动对焦功能初始化失败: {result.Error?.Message ?? ""}");
return false;
}
}
}
catch (Exception ex)
{
logger.Error(ex, $"Board{boardId}初始化摄像头自动对焦功能时发生异常");
return false;
}
}
/// <summary>
/// 执行摄像头自动对焦
/// </summary>
/// <returns>对焦结果</returns>
public async Task<bool> PerformAutoFocusAsync(
string boardId, int timeout = 1000, CancellationToken cancellationToken = default)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
logger.Info($"Board{boardId}开始执行摄像头自动对焦");
var camera = await client.Camera.WithCancellation(cancellationToken);
var result = await camera.PerformAutoFocus();
if (result.IsSuccessful && result.Value)
{
logger.Info($"Board{boardId}摄像头自动对焦成功");
return true;
}
else
{
logger.Error($"Board{boardId}摄像头自动对焦执行失败: {result.Error?.Message ?? ""}");
return false;
}
}
catch (Exception ex)
{
logger.Error(ex, $"Board{boardId}执行摄像头自动对焦时发生异常");
return false;
}
}
/// <summary>
/// 配置摄像头连接参数
/// </summary>
/// <param name="boardId">板卡ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>配置是否成功</returns>
public async Task<bool> ConfigureCameraAsync(string boardId, CancellationToken cancellationToken = default)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
var camera = await client.Camera.WithCancellation(cancellationToken);
using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
var ret = await camera.Init();
if (!ret.IsSuccessful)
{
logger.Error(ret.Error);
throw ret.Error;
}
if (!ret.Value)
{
logger.Error($"Camera Init Failed!");
throw new Exception($"Camera Init Failed!");
}
}
using (await client.Lock.AcquireWriteLockAsync(cancellationToken))
{
var ret = await camera.ChangeResolution(client.FrameWidth, client.FrameHeight);
if (!ret.IsSuccessful)
{
logger.Error(ret.Error);
throw ret.Error;
}
if (!ret.Value)
{
logger.Error($"Camera Resolution Change Failed!");
throw new Exception($"Camera Resolution Change Failed!");
}
}
return true;
}
catch (Exception ex)
{
logger.Error(ex, "配置摄像头连接时发生错误");
return false;
}
}
public async Task SetVideoStreamEnableAsync(string boardId, bool enable)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
if (client.IsEnabled == enable)
return;
using (await client.Lock.AcquireWriteLockAsync())
{
if (!enable || client.CTS.IsCancellationRequested)
{
client.CTS.Cancel();
client.CTS = new CancellationTokenSource();
}
if (!client.Camera.IsValueCreated) return;
var camera = await client.Camera.WithCancellation(client.CTS.Token);
var disableResult = await camera.EnableHardwareTrans(enable);
if (disableResult.IsSuccessful && disableResult.Value)
logger.Info($"Successfully disabled camera {boardId} hardware transmission");
else
logger.Error($"Failed to disable camera {boardId} hardware transmission: {disableResult.Error}");
}
}
catch (Exception ex)
{
logger.Error(ex, $"Exception occurred while disabling video transmission for {boardId}");
}
}
public async ValueTask<bool> TestCameraConnection(string boardId)
{
try
{
var client = TryGetClient(boardId).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
var imageData = await GetFPGAImageData(client);
if (imageData == null || imageData.Length == 0)
return false;
return true;
}
catch (Exception ex)
{
logger.Error(ex, $"Board{boardId}执行摄像头自动对焦时发生异常");
return false;
}
}
public VideoStreamEndpoint GetVideoEndpoint(string boardId)
{
var client = GetOrCreateClient(boardId, 640, 480).OrThrow(() => new Exception($"无法获取摄像头客户端: {boardId}"));
return new VideoStreamEndpoint
{
BoardId = boardId,
MjpegUrl = $"http://{Global.LocalHost}:{_serverPort}/mjpeg?boardId={boardId}",
VideoUrl = $"http://{Global.LocalHost}:{_serverPort}/video?boardId={boardId}",
SnapshotUrl = $"http://{Global.LocalHost}:{_serverPort}/snapshot?boardId={boardId}",
UsbCameraUrl = $"http://{Global.LocalHost}:{_serverPort}/usbCamera?boardId={boardId}",
HtmlUrl = $"http://{Global.LocalHost}:{_serverPort}/html?boardId={boardId}",
IsEnabled = client.IsEnabled,
FrameRate = client.FrameRate
};
}
public List<VideoStreamEndpoint> GetAllVideoEndpoints()
{
var endpoints = new List<VideoStreamEndpoint>();
foreach (var boardId in _clientDict.Keys)
endpoints.Add(GetVideoEndpoint(boardId));
return endpoints;
}
public VideoStreamServiceStatus GetServiceStatus()
{
return new VideoStreamServiceStatus
{
IsRunning = true,
ServerPort = _serverPort,
ClientEndpoints = GetAllVideoEndpoints()
};
}
}

View File

@@ -0,0 +1,147 @@
using System.Collections.Concurrent;
using Microsoft.AspNetCore.SignalR;
using server.Hubs;
namespace server.Services;
public enum TaskState { Running, Completed, Failed, Cancelled }
public readonly struct TaskProgress
{
public string Id { get; }
public int Current { get; }
public int Total { get; }
public TaskState State { get; }
public long Timestamp { get; }
public string? Error { get; }
public TaskProgress(string id, int current, int total, TaskState state, long timestamp, string? error = null)
{
Id = id;
Current = current;
Total = total;
State = state;
Timestamp = timestamp;
Error = error;
}
public TaskProgress WithUpdate(int? current = null, TaskState? state = null, string? error = null)
{
return new TaskProgress(
Id,
current ?? Current,
Total,
state ?? State,
DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
error ?? Error
);
}
public ProgressInfo ToProgressInfo()
{
return new ProgressInfo
{
TaskId = Id,
Status = State switch
{
TaskState.Running => ProgressStatus.Running,
TaskState.Completed => ProgressStatus.Completed,
TaskState.Failed => ProgressStatus.Failed,
TaskState.Cancelled => ProgressStatus.Canceled,
_ => ProgressStatus.Failed
},
ProgressPercent = Total > 0 ? ((double)Current * 100) / (double)Total : 0,
ErrorMessage = Error ?? string.Empty
};
}
}
public sealed class ProgressTracker
{
private readonly ConcurrentDictionary<string, TaskProgress> _tasks = new();
private readonly Timer _cleaner;
private readonly IHubContext<ProgressHub, IProgressReceiver> _hubContext;
// 构造器支持可选的Hub注入
public ProgressTracker(IHubContext<ProgressHub, IProgressReceiver> hubContext)
{
_hubContext = hubContext;
_cleaner = new Timer(CleanExpiredTasks, null,
TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
}
public void CleanExpiredTasks(object? obj)
{
var cutoff = DateTimeOffset.Now.AddMinutes(-3).ToUnixTimeSeconds();
var expired = _tasks.Where(kvp => kvp.Value.Timestamp < cutoff).Select(kvp => kvp.Key).ToList();
foreach (var id in expired)
{
_tasks.TryRemove(id, out _);
}
}
public string CreateTask(int total)
{
var id = Guid.NewGuid().ToString();
var task = new TaskProgress(id, 0, total, TaskState.Running, DateTimeOffset.UtcNow.ToUnixTimeSeconds());
_tasks[id] = task;
NotifyIfNeeded(task);
return id;
}
// 核心更新方法,现在包含自动通知
public bool UpdateTask(string id, Func<TaskProgress, TaskProgress> updater)
{
if (!_tasks.TryGetValue(id, out var current))
return false;
var updated = updater(current);
if (_tasks.TryUpdate(id, updated, current))
{
NotifyIfNeeded(updated);
return true;
}
return false;
}
// 自动通知逻辑 - 简单直接
private void NotifyIfNeeded(TaskProgress task)
{
_hubContext.Clients.Group(task.Id).OnReceiveProgress(task.ToProgressInfo());
}
public bool UpdateProgress(string id, int current)
{
return UpdateTask(id, p => p.WithUpdate(
current: Math.Min(current, p.Total)));
}
public bool AdvanceProgress(string id, int steps)
{
return UpdateTask(id, p => p.WithUpdate(
current: Math.Min(p.Current + steps, p.Total)));
}
public bool CancelProgress(string id)
{
return UpdateTask(id, p => p.WithUpdate(state: TaskState.Cancelled));
}
public bool CompleteProgress(string id)
{
return UpdateTask(id, p => p.WithUpdate(
current: p.Total, state: TaskState.Completed));
}
public bool FailProgress(string id, string? error)
{
return UpdateTask(id, p => p.WithUpdate(
state: TaskState.Failed, error: error));
}
public TaskProgress? GetTask(string id)
{
_tasks.TryGetValue(id, out var task);
return task.Id == null ? null : task;
}
}

View File

@@ -0,0 +1,576 @@
using System.Net;
using System.Net.Sockets;
using System.Collections.Concurrent;
using System.Text;
using Rtsp;
using Rtsp.Messages;
using Rtsp.Sdp;
using server.Services;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace server.Services;
/// <summary>
/// RTSP streaming service that integrates with UsbCameraCapture
/// Uses simplified RTSP server architecture with RTSPDispatcher
/// Provides Motion JPEG stream over RTP/RTSP
/// Compatible with Windows and Linux
/// </summary>
public class RtspStreamService : IDisposable
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly UsbCameraCapture _cameraCapture;
private readonly ConcurrentDictionary<string, RtspListener> _activeListeners = new();
// RTSP configuration
private readonly int _rtspPort;
private readonly string _streamPath;
private TcpListener? _rtspServerListener;
private ManualResetEvent? _stopping;
private Thread? _listenThread;
// Video encoding parameters
private int _videoWidth = 640;
private int _videoHeight = 480;
private int _frameRate = 30;
private int _jpegQuality = 75;
private bool _isStreaming;
private bool _disposed;
// Frame timing and RTP sequencing
private DateTime _lastFrameTime = DateTime.UtcNow;
private readonly TimeSpan _frameInterval;
private uint _rtpTimestamp = 0;
private ushort _sequenceNumber = 0;
private readonly uint _ssrc = (uint)Random.Shared.Next();
// Current frame data for broadcasting
private byte[]? _currentFrame;
private readonly object _frameLock = new object();
public event Action<Exception>? Error;
public event Action<string>? StatusChanged;
public bool IsStreaming => _isStreaming;
public int Port => _rtspPort;
public string StreamUrl => $"rtsp://localhost:{_rtspPort}/{_streamPath}";
public int ActiveSessions => _activeListeners.Count;
public RtspStreamService(UsbCameraCapture cameraCapture, int port = 8554, string streamPath = "camera")
{
_cameraCapture = cameraCapture ?? throw new ArgumentNullException(nameof(cameraCapture));
_rtspPort = port;
_streamPath = streamPath;
_frameInterval = TimeSpan.FromSeconds(1.0 / _frameRate);
// Register RTSP URI scheme
RtspUtils.RegisterUri();
// Subscribe to camera events
_cameraCapture.FrameReady += OnFrameReady;
_cameraCapture.Error += OnCameraError;
}
/// <summary>
/// Configure video encoding parameters
/// </summary>
public void ConfigureVideo(int width, int height, int frameRate, int jpegQuality = 75)
{
if (_isStreaming)
throw new InvalidOperationException("Cannot configure video while streaming");
_videoWidth = width;
_videoHeight = height;
_frameRate = frameRate;
_jpegQuality = jpegQuality;
logger.Info($"Video configured: {width}x{height} @ {frameRate}fps, JPEG quality {jpegQuality}%");
}
/// <summary>
/// Start RTSP server and begin streaming
/// </summary>
public async Task StartAsync()
{
if (_isStreaming)
return;
try
{
// Validate port range
if (_rtspPort < IPEndPoint.MinPort || _rtspPort > IPEndPoint.MaxPort)
throw new ArgumentOutOfRangeException(nameof(_rtspPort), _rtspPort, "Port number must be between System.Net.IPEndPoint.MinPort and System.Net.IPEndPoint.MaxPort");
// Initialize RTSP server
_rtspServerListener = new TcpListener(IPAddress.Any, _rtspPort);
_rtspServerListener.Start();
// Start listening for connections
_stopping = new ManualResetEvent(false);
_listenThread = new Thread(AcceptConnections)
{
Name = "RTSP-Listener",
IsBackground = true
};
_listenThread.Start();
// Start camera capture if not already running
if (!_cameraCapture.IsCapturing)
{
await _cameraCapture.StartAsync(1, _videoWidth, _videoHeight, _frameRate);
}
_isStreaming = true;
StatusChanged?.Invoke("Streaming started");
logger.Info($"RTSP stream started on {StreamUrl}");
}
catch (Exception ex)
{
await StopAsync();
Error?.Invoke(ex);
throw;
}
}
/// <summary>
/// Stop RTSP server and streaming
/// </summary>
public async Task StopAsync()
{
if (!_isStreaming)
return;
_isStreaming = false;
try
{
// Signal stop and wait for listen thread
_stopping?.Set();
if (_listenThread != null && _listenThread.IsAlive)
{
_listenThread.Join(TimeSpan.FromSeconds(5));
}
// Stop RTSP server
_rtspServerListener?.Stop();
// Clean up active listeners
foreach (var listener in _activeListeners.Values.ToArray())
{
try
{
listener.Stop();
}
catch (Exception ex)
{
logger.Warn(ex, "Error stopping RTSP listener");
}
}
_activeListeners.Clear();
StatusChanged?.Invoke("Streaming stopped");
logger.Info("RTSP stream stopped");
}
catch (Exception ex)
{
Error?.Invoke(ex);
}
await Task.CompletedTask;
}
/// <summary>
/// Get current stream statistics
/// </summary>
public StreamStats GetStats()
{
return new StreamStats
{
IsStreaming = _isStreaming,
ActiveSessions = _activeListeners.Count,
VideoWidth = _videoWidth,
VideoHeight = _videoHeight,
FrameRate = _frameRate,
StreamUrl = StreamUrl
};
}
/// <summary>
/// Accept incoming RTSP connections
/// </summary>
private void AcceptConnections()
{
try
{
while (!(_stopping?.WaitOne(0) ?? true))
{
TcpClient client = _rtspServerListener!.AcceptTcpClient();
var transport = new RtspTcpTransport(client);
var listener = new RtspListener(transport);
var listenerId = Guid.NewGuid().ToString();
_activeListeners[listenerId] = listener;
// Handle listener events
listener.MessageReceived += (sender, args) => HandleRtspMessage(listenerId, args);
// Store listener for later cleanup
// We'll rely on exception handling to detect disconnections
// Start the listener
listener.Start();
logger.Info($"New RTSP client connected: {listenerId} from {client.Client.RemoteEndPoint}");
}
}
catch (SocketException ex)
{
if (_isStreaming) // Only log if we're still supposed to be running
{
logger.Warn(ex, "Socket error while accepting connections (may be normal during shutdown)");
}
}
catch (Exception ex)
{
if (_isStreaming)
{
logger.Error(ex, "Error accepting RTSP connections");
Error?.Invoke(ex);
}
}
}
/// <summary>
/// Handle RTSP messages from clients
/// </summary>
private void HandleRtspMessage(string listenerId, RtspChunkEventArgs args)
{
try
{
if (args.Message is RtspRequest request)
{
HandleRtspRequest(listenerId, request);
}
}
catch (Exception ex)
{
logger.Error(ex, $"Error handling RTSP message for listener {listenerId}");
}
}
/// <summary>
/// Handle RTSP requests
/// </summary>
private void HandleRtspRequest(string listenerId, RtspRequest request)
{
if (!_activeListeners.TryGetValue(listenerId, out var listener))
return;
var response = new RtspResponse();
response.OriginalRequest = request;
// 1. 返回 CSeq 字段
if (request.Headers.TryGetValue("CSeq", out var cseq))
{
response.Headers["CSeq"] = cseq;
}
switch (request.RequestTyped)
{
case RtspRequest.RequestType.OPTIONS:
response.Headers["Public"] = "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE";
response.ReturnCode = 200;
break;
case RtspRequest.RequestType.DESCRIBE:
if (request.RtspUri?.AbsolutePath.TrimStart('/') == _streamPath)
{
var sdp = CreateSdp();
response.Headers["Content-Type"] = "application/sdp";
response.Data = Encoding.UTF8.GetBytes(sdp);
response.ReturnCode = 200;
}
else
{
response.ReturnCode = 404;
}
break;
case RtspRequest.RequestType.SETUP:
// 2. 解析客户端 Transport 字段
string clientTransport = request.Headers.TryGetValue("Transport", out var transport) ? transport : "";
string serverTransport;
if (clientTransport.Contains("TCP", StringComparison.OrdinalIgnoreCase) || clientTransport.Contains("interleaved"))
{
// 客户端要求TCP
serverTransport = "RTP/AVP/TCP;unicast;interleaved=0-1";
}
else if (clientTransport.Contains("UDP", StringComparison.OrdinalIgnoreCase) || clientTransport.Contains("client_port"))
{
// 客户端要求UDP
// 这里假设端口号格式为 client_port=xxxx-xxxx
var match = System.Text.RegularExpressions.Regex.Match(clientTransport, @"client_port=(\d+)-(\d+)");
if (match.Success)
{
var clientPort1 = match.Groups[1].Value;
var clientPort2 = match.Groups[2].Value;
// 你可以自定义 server_port
serverTransport = $"RTP/AVP;unicast;client_port={clientPort1}-{clientPort2};server_port=9000-9001";
}
else
{
// 默认UDP
serverTransport = "RTP/AVP;unicast;client_port=8000-8001;server_port=9000-9001";
}
}
else
{
// 默认TCP
serverTransport = "RTP/AVP/TCP;unicast;interleaved=0-1";
}
response.Headers["Transport"] = serverTransport;
response.Headers["Session"] = listenerId;
response.ReturnCode = 200;
break;
case RtspRequest.RequestType.PLAY:
response.Headers["Session"] = listenerId;
response.ReturnCode = 200;
// Start sending frames to this client
StartFrameBroadcastForListener(listenerId);
break;
case RtspRequest.RequestType.TEARDOWN:
response.ReturnCode = 200;
// Stop and remove the listener
Task.Run(() =>
{
listener.Stop();
_activeListeners.TryRemove(listenerId, out _);
});
break;
default:
response.ReturnCode = 501; // Not implemented
break;
}
// Send response
try
{
listener.SendMessage(response);
}
catch (Exception ex)
{
logger.Error(ex, $"Error sending RTSP response to listener {listenerId}");
}
}
/// <summary>
/// Create SDP description for the stream
/// </summary>
private string CreateSdp()
{
var sessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return $@"v=0
o=- {sessionId} {sessionId} IN IP4 127.0.0.1
s=FPGA WebLab Camera Stream
c=IN IP4 0.0.0.0
t=0 0
m=video 0 RTP/AVP 26
a=rtpmap:26 JPEG/90000
a=control:track1
a=framerate:{_frameRate}";
}
/// <summary>
/// Start broadcasting frames to a specific listener
/// </summary>
private void StartFrameBroadcastForListener(string listenerId)
{
// For now, we'll use a simple approach where we send the current frame
// In a full implementation, you'd want to manage RTP streaming per client
lock (_frameLock)
{
if (_currentFrame != null && _activeListeners.TryGetValue(listenerId, out var listener))
{
try
{
// Send current frame (simplified - in real implementation you'd send RTP packets)
// This is a placeholder for actual RTP packet creation and sending
logger.Debug($"Started frame broadcast for listener {listenerId}");
}
catch (Exception ex)
{
logger.Error(ex, $"Error starting frame broadcast for listener {listenerId}");
}
}
}
}
/// <summary>
/// Handle new frame from camera
/// </summary>
private void OnFrameReady(byte[] frameData)
{
if (!_isStreaming || frameData == null || _activeListeners.IsEmpty)
return;
try
{
// Throttle frame rate
var now = DateTime.UtcNow;
if (now - _lastFrameTime < _frameInterval)
return;
_lastFrameTime = now;
// Process and encode frame
var processedFrame = ProcessFrame(frameData);
if (processedFrame != null)
{
lock (_frameLock)
{
_currentFrame = processedFrame;
}
BroadcastFrame(processedFrame);
}
}
catch (Exception ex)
{
logger.Error(ex, "Error processing camera frame");
Error?.Invoke(ex);
}
}
/// <summary>
/// Process raw frame data
/// </summary>
private byte[]? ProcessFrame(byte[] frameData)
{
try
{
// Convert frame to JPEG for Motion JPEG streaming
using var image = Image.Load<Rgb24>(frameData);
// Resize if necessary
if (image.Width != _videoWidth || image.Height != _videoHeight)
{
image.Mutate(x => x.Resize(_videoWidth, _videoHeight));
}
// Encode as JPEG with specified quality
using var stream = new MemoryStream();
image.SaveAsJpeg(stream, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder
{
Quality = _jpegQuality
});
return stream.ToArray();
}
catch (Exception ex)
{
logger.Error(ex, "Error processing frame");
return null;
}
}
/// <summary>
/// Broadcast frame to all active listeners
/// </summary>
private void BroadcastFrame(byte[] frameData)
{
if (_activeListeners.IsEmpty)
return;
var timestamp = _rtpTimestamp;
_rtpTimestamp += (uint)(90000 / _frameRate); // 90kHz clock
var sequenceNumber = ++_sequenceNumber;
var listenersToRemove = new List<string>();
foreach (var kvp in _activeListeners)
{
try
{
var listener = kvp.Value;
// Try to send data to test if listener is still active
// In a full implementation, you would create and send RTP packets here
// For now, this is a placeholder that just checks if we can access the listener
try
{
var _ = listener.RemoteEndPoint; // Test if listener is still valid
// SendRtpFrame(listener, frameData, timestamp, sequenceNumber, _ssrc);
}
catch
{
listenersToRemove.Add(kvp.Key);
}
}
catch (Exception ex)
{
logger.Warn(ex, $"Error sending frame to listener {kvp.Key}");
listenersToRemove.Add(kvp.Key);
}
}
// Remove failed listeners
foreach (var listenerId in listenersToRemove)
{
if (_activeListeners.TryRemove(listenerId, out var listener))
{
try
{
listener.Stop();
}
catch (Exception ex)
{
logger.Warn(ex, $"Error stopping failed listener {listenerId}");
}
}
}
}
/// <summary>
/// Handle camera capture errors
/// </summary>
private void OnCameraError(Exception error)
{
logger.Error(error, "Camera capture error");
Error?.Invoke(error);
}
public void Dispose()
{
if (_disposed) return;
StopAsync().Wait();
_cameraCapture.FrameReady -= OnFrameReady;
_cameraCapture.Error -= OnCameraError;
_rtspServerListener?.Stop();
_stopping?.Dispose();
_disposed = true;
}
}
/// <summary>
/// Stream statistics data structure
/// </summary>
public class StreamStats
{
public bool IsStreaming { get; set; }
public int ActiveSessions { get; set; }
public int VideoWidth { get; set; }
public int VideoHeight { get; set; }
public int FrameRate { get; set; }
public string StreamUrl { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,202 @@
using FlashCap;
namespace server.Services;
/// <summary>
/// Simple USB camera capture service following Linus principles:
/// - Single responsibility: just capture frames
/// - No special cases: uniform error handling
/// - Good taste: clean data structures
/// </summary>
public class UsbCameraCapture : IDisposable
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly CaptureDevices _captureDevices;
private CaptureDevice? _device;
private CaptureDeviceDescriptor? _descriptor;
private VideoCharacteristics? _characteristics;
// Single source of truth for latest frame - no redundant buffering
private volatile byte[]? _latestFrame;
private volatile bool _isCapturing;
private bool _disposed;
public event Action<byte[]>? FrameReady;
public event Action<Exception>? Error;
public bool IsCapturing => _isCapturing;
public VideoCharacteristics? CurrentCharacteristics => _characteristics;
public CaptureDeviceDescriptor? CurrentDevice => _descriptor;
public UsbCameraCapture()
{
_captureDevices = new CaptureDevices();
}
/// <summary>
/// Get all available camera devices
/// </summary>
public IReadOnlyList<CaptureDeviceDescriptor> GetDevices()
{
return _captureDevices.EnumerateDescriptors().ToArray();
}
/// <summary>
/// Start capturing from specified device with best matching characteristics
/// </summary>
public async Task StartAsync(int deviceIndex, int width = 640, int height = 480, int frameRate = 30)
{
var devices = GetDevices();
if (deviceIndex >= devices.Count)
throw new ArgumentOutOfRangeException(nameof(deviceIndex));
var descriptor = devices[deviceIndex];
var characteristics = FindBestMatch(descriptor, width, height, frameRate);
await StartAsync(descriptor, characteristics);
}
/// <summary>
/// Start capturing with exact device and characteristics
/// </summary>
public async Task StartAsync(CaptureDeviceDescriptor descriptor, VideoCharacteristics characteristics)
{
if (_isCapturing)
await StopAsync();
try
{
_descriptor = descriptor;
_characteristics = characteristics;
_device = await descriptor.OpenAsync(
characteristics, TranscodeFormats.DoNotTranscode, true, 10, OnFrameCaptured);
await _device.StartAsync();
_isCapturing = true;
logger.Debug("Started capturing");
}
catch (Exception ex)
{
await CleanupAsync();
Error?.Invoke(ex);
throw;
}
}
/// <summary>
/// Stop capturing and cleanup
/// </summary>
public async Task StopAsync()
{
if (!_isCapturing)
return;
_isCapturing = false;
await CleanupAsync();
}
/// <summary>
/// Get the latest captured frame (returns copy for thread safety)
/// </summary>
public byte[]? GetLatestFrame()
{
return _latestFrame;
}
/// <summary>
/// Get supported video characteristics for current device
/// </summary>
public IReadOnlyList<VideoCharacteristics> GetSupportedCharacteristics()
{
return _descriptor?.Characteristics.ToArray() ?? Array.Empty<VideoCharacteristics>();
}
private VideoCharacteristics FindBestMatch(CaptureDeviceDescriptor descriptor, int width, int height, int frameRate)
{
var characteristics = descriptor.Characteristics;
// Exact match first
var exact = characteristics.FirstOrDefault(c =>
c.Width == width && c.Height == height && Math.Abs(c.FramesPerSecond - frameRate) < 1);
if (exact != null)
return exact;
// Resolution match with best framerate
var resolution = characteristics
.Where(c => c.Width == width && c.Height == height)
.OrderByDescending(c => c.FramesPerSecond)
.FirstOrDefault();
if (resolution != null)
return resolution;
// Closest resolution
try
{
var closest = characteristics
.OrderBy(c => Math.Abs(c.Width - width) + Math.Abs(c.Height - height))
.ThenByDescending(c => c.FramesPerSecond)
.First();
return closest;
}
catch
{
for (int i = 0; i < characteristics.Length; i++)
logger.Error($"Characteristics[{i}]: {characteristics[i].Width}x{characteristics[i].Height} @ {characteristics[i].FramesPerSecond}fps");
throw;
}
}
private void OnFrameCaptured(PixelBufferScope bufferScope)
{
if (!_isCapturing)
return;
try
{
// Simple: extract and store. No queues, no locks, no complexity.
var imageData = bufferScope.Buffer.CopyImage();
_latestFrame = imageData;
FrameReady?.Invoke(imageData);
// logger.Info("USB Camera frame captured");
}
catch (Exception ex)
{
Error?.Invoke(ex);
}
}
private async Task CleanupAsync()
{
try
{
if (_device != null)
{
await _device.StopAsync();
_device.Dispose();
_device = null;
}
}
catch (Exception ex)
{
Error?.Invoke(ex);
}
finally
{
_latestFrame = null;
_descriptor = null;
_characteristics = null;
}
}
public void Dispose()
{
if (_disposed) return;
if (_isCapturing) StopAsync().Wait();
_device?.Dispose();
_disposed = true;
}
}

View File

@@ -1,30 +0,0 @@
// 此接口提供获取例程目录服务
// GET /api/tutorials 返回所有可用的例程目录
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { Request, Response } from 'express';
// 获取当前文件的目录
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const publicDir = path.resolve(__dirname, '../public');
export function getTutorials(req: Request, res: Response) {
try {
const docDir = path.join(publicDir, 'doc');
// 读取doc目录下的所有文件夹
const entries = fs.readdirSync(docDir, { withFileTypes: true });
const dirs = entries
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
// 返回文件夹列表
res.json({ tutorials: dirs });
} catch (error) {
console.error('获取例程目录失败:', error);
res.status(500).json({ error: '无法读取例程目录' });
}
}

View File

@@ -3,15 +3,16 @@ using System.Net.Sockets;
using System.Text;
using DotNext;
using WebProtocol;
using server.Services;
/// <summary>
/// UDP客户端发送池
/// </summary>
public class UDPClientPool
public sealed class UDPClientPool
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private static IPAddress localhost = IPAddress.Parse("127.0.0.1");
private static ProgressTracker _progressTracker = MsgBus.ProgressTracker;
/// <summary>
/// 发送字符串
@@ -60,8 +61,8 @@ public class UDPClientPool
var sendLen = socket.SendTo(buf, endPoint);
socket.Close();
logger.Debug($"UDP socket send bytes to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
logger.Debug($" Original Data: {BitConverter.ToString(buf).Replace("-", " ")}");
// logger.Debug($"UDP socket send bytes to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
// logger.Debug($" Original Data: {BitConverter.ToString(buf).Replace("-", " ")}");
if (sendLen == buf.Length) { return true; }
else { return false; }
@@ -91,9 +92,9 @@ public class UDPClientPool
var sendLen = socket.SendTo(sendBytes, endPoint);
socket.Close();
logger.Debug($"UDP socket send address package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
logger.Debug($" Decoded Data: {pkg.ToString()}");
// logger.Debug($"UDP socket send address package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
// logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
// logger.Debug($" Decoded Data: {pkg.ToString()}");
if (sendLen == sendBytes.Length) { return true; }
else { return false; }
@@ -110,6 +111,46 @@ public class UDPClientPool
return await Task.Run(() => { return SendAddrPack(endPoint, pkg); });
}
/// <summary>
/// 发送多个地址包
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="pkgs">地址包集合(最多512 / 8)</param>
/// <returns>是否全部成功</returns>
public static bool SendMultiAddrPack(IPEndPoint endPoint, IEnumerable<WebProtocol.SendAddrPackage> pkgs)
{
const int maxPkgs = 512 / 8;
var pkgList = pkgs.Take(maxPkgs).ToList();
if (pkgList.Count == 0) return false;
// 合并所有包为一个buffer
int totalLen = pkgList.Sum(pkg => pkg.ToBytes().Length);
byte[] buffer = new byte[totalLen];
int offset = 0;
foreach (var pkg in pkgList)
{
var bytes = pkg.ToBytes();
Buffer.BlockCopy(bytes, 0, buffer, offset, bytes.Length);
offset += bytes.Length;
}
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
var sendLen = socket.SendTo(buffer.ToArray(), endPoint);
socket.Close();
return sendLen == buffer.Length;
}
/// <summary>
/// 异步发送多个地址包
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="pkgs">地址包集合(最多512 / 8)</param>
/// <returns>是否全部成功</returns>
public async static ValueTask<bool> SendMultiAddrPackAsync(IPEndPoint endPoint, IEnumerable<WebProtocol.SendAddrPackage> pkgs)
{
return await Task.Run(() => SendMultiAddrPack(endPoint, pkgs));
}
/// <summary>
/// 发送数据包
@@ -124,8 +165,8 @@ public class UDPClientPool
var sendLen = socket.SendTo(sendBytes, endPoint);
socket.Close();
logger.Debug($"UDP socket send data package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
// logger.Debug($"UDP socket send data package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
// logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
if (sendLen == sendBytes.Length) { return true; }
else { return false; }
@@ -143,59 +184,50 @@ public class UDPClientPool
}
/// <summary>
/// 发送字符串到本地
/// 发送重置信号
/// </summary>
/// <param name="port">端口</param>
/// <param name="stringArray">字符串数组</param>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <returns>是否成功</returns>
public static bool SendStringLocalHost(int port, string[] stringArray)
public async static ValueTask<bool> SendResetSignal(IPEndPoint endPoint)
{
return SendString(new IPEndPoint(localhost, port), stringArray);
}
/// <summary>
/// 循环发送字符串到本地
/// </summary>
/// <param name="times">发送总次数</param>
/// <param name="sleepMilliSeconds">间隔时间</param>
/// <param name="port">端口</param>
/// <param name="stringArray">字符串数组</param>
/// <returns>是否成功</returns>
public static bool CycleSendStringLocalHost(int times, int sleepMilliSeconds, int port, string[] stringArray)
{
var isSuccessful = true;
while (times-- >= 0)
return await Task.Run(async () =>
{
isSuccessful = SendStringLocalHost(port, stringArray);
if (!isSuccessful) break;
Thread.Sleep(sleepMilliSeconds);
}
return isSuccessful;
var ret = SendAddrPack(endPoint,
new WebProtocol.SendAddrPackage(BurstType.FixedBurst, 0, true, 0, 0xF0F0F0F0));
await Task.Delay(100);
return ret;
});
}
/// <summary>
/// [TODO:description]
/// 读取设备地址数据
/// </summary>
/// <param name="endPoint">[TODO:parameter]</param>
/// <param name="devAddr">[TODO:parameter]</param>
/// <param name="timeout">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param>
/// <param name="dataLength">数据长度(0~255)</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <returns>读取结果,包含接收到的数据包</returns>
public static async ValueTask<Result<RecvDataPackage>> ReadAddr(
IPEndPoint endPoint, uint devAddr, int timeout = 1000)
IPEndPoint endPoint, int taskID, uint devAddr, int dataLength, int timeout = 1000)
{
if (dataLength <= 0) return new(new ArgumentException(
$"Data length must be greater than 0, instead of {dataLength}"));
if (dataLength > 255) return new(new ArgumentException(
$"Data length must be less than or equal to 255, instead of {dataLength}"));
var ret = false;
var opts = new SendAddrPackOptions();
var opts = new SendAddrPackOptions()
{
BurstType = BurstType.FixedBurst,
BurstLength = ((byte)(dataLength - 1)),
CommandID = Convert.ToByte(taskID),
Address = devAddr,
IsWrite = false,
};
opts.BurstType = BurstType.FixedBurst;
opts.BurstLength = 0;
opts.CommandID = 0;
opts.Address = devAddr;
// Read Jtag State Register
opts.IsWrite = false;
// Read Register
ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send Address Package Failed!"));
@@ -203,8 +235,7 @@ public class UDPClientPool
if (!MsgBus.IsRunning)
return new(new Exception("Message Bus not Working!"));
var retPack = await MsgBus.UDPServer.WaitForDataAsync(
endPoint.Address.ToString(), endPoint.Port, timeout);
var retPack = await MsgBus.UDPServer.WaitForDataAsync(endPoint, taskID, timeout);
if (!retPack.IsSuccessful) return new(retPack.Error);
else if (!retPack.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
@@ -217,20 +248,35 @@ public class UDPClientPool
}
/// <summary>
/// [TODO:description]
/// 读取设备地址数据
/// </summary>
/// <param name="endPoint">[TODO:parameter]</param>
/// <param name="devAddr">[TODO:parameter]</param>
/// <param name="result">[TODO:parameter]</param>
/// <param name="resultMask">[TODO:parameter]</param>
/// <param name="timeout">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <returns>读取结果,包含接收到的数据包</returns>
public static async ValueTask<Result<RecvDataPackage>> ReadAddrByte(
IPEndPoint endPoint, int taskID, uint devAddr, int timeout = 1000)
{
return await ReadAddr(endPoint, taskID, devAddr, 1, timeout);
}
/// <summary>
/// 读取设备地址数据并校验结果
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param>
/// <param name="result">期望的结果值</param>
/// <param name="resultMask">结果掩码,用于位校验</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <returns>校验结果true表示数据匹配期望值</returns>
public static async ValueTask<Result<bool>> ReadAddr(
IPEndPoint endPoint, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000)
IPEndPoint endPoint, int taskID, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000)
{
var address = endPoint.Address.ToString();
var ret = await ReadAddr(endPoint, devAddr, timeout);
var ret = await ReadAddrByte(endPoint, taskID, devAddr, timeout);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value.IsSuccessful)
return new(new Exception($"Read device {address} address {devAddr} failed"));
@@ -255,16 +301,20 @@ public class UDPClientPool
}
/// <summary>
/// [TODO:description]
/// 读取设备地址数据并等待直到结果匹配或超时
/// </summary>
/// <param name="endPoint">[TODO:parameter]</param>
/// <param name="devAddr">[TODO:parameter]</param>
/// <param name="result">[TODO:parameter]</param>
/// <param name="resultMask">[TODO:parameter]</param>
/// <param name="timeout">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param>
/// <param name="result">期望的结果值</param>
/// <param name="resultMask">结果掩码,用于位校验</param>
/// <param name="waittime">等待间隔时间(毫秒)</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <returns>校验结果true表示在超时前数据匹配期望值</returns>
public static async ValueTask<Result<bool>> ReadAddrWithWait(
IPEndPoint endPoint, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000)
IPEndPoint endPoint, int taskID, uint devAddr,
UInt32 result, UInt32 resultMask,
int waittime = 100, int timeout = 1000)
{
var address = endPoint.Address.ToString();
@@ -274,10 +324,10 @@ public class UDPClientPool
var elapsed = DateTime.Now - startTime;
if (elapsed >= TimeSpan.FromMilliseconds(timeout)) break;
var timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
await Task.Delay(waittime);
try
{
var ret = await ReadAddr(endPoint, devAddr, Convert.ToInt32(timeleft.TotalMilliseconds));
var ret = await ReadAddrByte(endPoint, taskID, devAddr, Convert.ToInt32(timeleft.TotalMilliseconds));
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value.IsSuccessful)
return new(new Exception($"Read device {address} address {devAddr} failed"));
@@ -290,7 +340,7 @@ public class UDPClientPool
$"Device {address} receive data is {retData.Length} bytes instead of 4 bytes"));
// Check result
var retCode = Convert.ToUInt32(Common.Number.BytesToUInt64(retData).Value);
var retCode = Convert.ToUInt32(Common.Number.BytesToUInt32(retData).Value);
if (Common.Number.BitsCheck(retCode, result, resultMask)) return true;
}
catch (Exception error)
@@ -302,85 +352,330 @@ public class UDPClientPool
return false;
}
/// <summary>
/// [TODO:description]
/// 从设备地址读取字节数组数据(支持大数据量分段传输)
/// </summary>
/// <param name="endPoint">[TODO:parameter]</param>
/// <param name="devAddr">[TODO:parameter]</param>
/// <param name="data">[TODO:parameter]</param>
/// <param name="timeout">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public static async ValueTask<Result<bool>> WriteAddr(
IPEndPoint endPoint, UInt32 devAddr, UInt32 data, int timeout = 1000)
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param>
/// <param name="dataLength">要读取的数据长度4字节</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <returns>读取结果,包含接收到的字节数组</returns>
public static async ValueTask<Result<byte[]>> ReadAddr4Bytes(
IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, int timeout = 1000)
{
var ret = false;
var opts = new SendAddrPackOptions();
var opts = new SendAddrPackOptions()
{
BurstLength = 0,
Address = 0,
BurstType = BurstType.FixedBurst,
CommandID = Convert.ToByte(taskID),
IsWrite = false,
};
var resultData = new List<byte>();
opts.BurstType = BurstType.FixedBurst;
opts.BurstLength = 0;
opts.CommandID = 0;
opts.Address = devAddr;
// Write Jtag State Register
opts.IsWrite = true;
// Check Msg Bus
if (!MsgBus.IsRunning)
return new(new Exception("Message bus not working!"));
// Calculate read times and segments
var max4BytesPerRead = 0x80; // 512 bytes per read
var rest4Bytes = dataLength % max4BytesPerRead;
var readTimes = (rest4Bytes != 0) ?
(dataLength / max4BytesPerRead + 1) :
(dataLength / max4BytesPerRead);
for (var i = 0; i < readTimes; i++)
{
// Calculate current segment size
var isLastSegment = i == readTimes - 1;
var currentSegmentSize = (isLastSegment && rest4Bytes != 0) ? rest4Bytes : max4BytesPerRead;
// Set burst length (in 32-bit words)
opts.BurstLength = (byte)(currentSegmentSize - 1);
// Update address for current segment
opts.Address = devAddr + (uint)(i * max4BytesPerRead);
// Send read address package
ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
if (!ret) return new(new Exception($"Send address package failed at segment {i}!"));
// Wait for data response
var retPack = await MsgBus.UDPServer.WaitForDataAsync(endPoint, taskID, timeout);
if (!retPack.IsSuccessful) return new(retPack.Error);
if (!retPack.Value.IsSuccessful)
return new(new Exception($"Read address package failed at segment {i}"));
var retPackOpts = retPack.Value.Options;
if (retPackOpts.Data is null)
return new(new Exception($"Data is null at segment {i}, package: {retPackOpts.ToString()}"));
// Validate received data length
if (retPackOpts.Data.Length != currentSegmentSize * 4)
return new(new Exception($"Expected {currentSegmentSize * 4} bytes but received {retPackOpts.Data.Length} bytes at segment {i}"));
// Add received data to result
resultData.AddRange(retPackOpts.Data);
}
// Validate total data length
if (resultData.Count != dataLength * 4)
return new(new Exception($"Expected total {dataLength * 4} bytes but received {resultData.Count} bytes"));
return resultData.ToArray();
}
/// <summary>
/// 从设备地址读取字节数组数据(支持大数据量分段传输,先发送所有地址包再接收所有数据包)
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param>
/// <param name="burstType">突发类型</param>
/// <param name="dataLength">要读取的数据长度4字节</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <returns>读取结果,包含接收到的字节数组</returns>
public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync(
IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, BurstType burstType, int timeout = 1000)
{
var pkgList = new List<SendAddrPackage>();
var resultData = new List<byte>();
// Check Msg Bus
if (!MsgBus.IsRunning)
return new(new Exception("Message bus not working!"));
// Prepare packages for each segment
var max4BytesPerRead = 0x80; // 512 bytes per read
var rest4Bytes = dataLength % max4BytesPerRead;
var readTimes = (rest4Bytes != 0) ?
(dataLength / max4BytesPerRead + 1) :
(dataLength / max4BytesPerRead);
for (var i = 0; i < readTimes; i++)
{
var isLastSegment = i == readTimes - 1;
var currentSegmentSize = (isLastSegment && rest4Bytes != 0) ? rest4Bytes : max4BytesPerRead;
var opts = new SendAddrPackOptions
{
BurstType = burstType,
CommandID = Convert.ToByte(taskID),
IsWrite = false,
BurstLength = (byte)(currentSegmentSize - 1),
Address = (burstType == BurstType.ExtendBurst) ? (devAddr + (uint)(i * max4BytesPerRead)) : (devAddr),
// Address = devAddr + (uint)(i * max4BytesPerRead),
};
pkgList.Add(new SendAddrPackage(opts));
}
// Send address packages in batches of 128, control outstanding
int sentCount = 0;
var startTime = DateTime.Now;
const int batchSize = 32;
while (sentCount < pkgList.Count)
{
var elapsed = DateTime.Now - startTime;
if (elapsed >= TimeSpan.FromMilliseconds(timeout))
break;
var timeleft = timeout - (int)elapsed.TotalMilliseconds;
var found = await MsgBus.UDPServer.GetDataCountAsync(endPoint.Address.ToString(), taskID, timeleft);
int outstanding = sentCount - (found.HasValue ? found.Value : 0);
// If outstanding >= 512 - batchSize, wait for some data to be received
if (outstanding >= 128 - batchSize)
continue;
// Send next batch of address packages (up to 128)
int batchSend = Math.Min(batchSize, pkgList.Count - sentCount);
var batchPkgs = pkgList.Skip(sentCount).Take(batchSend);
var ret = await UDPClientPool.SendMultiAddrPackAsync(endPoint, batchPkgs);
if (!ret) return new(new Exception($"Send address package batch failed at segment {sentCount}!"));
sentCount += batchSend;
// Task.Delay(1).Wait();
}
// Wait until enough data is received or timeout
startTime = DateTime.Now;
var udpDatas = new List<UDPData>();
while (true)
{
var elapsed = DateTime.Now - startTime;
if (elapsed >= TimeSpan.FromMilliseconds(timeout)) break;
var timeleft = timeout - (int)elapsed.TotalMilliseconds;
var found = await MsgBus.UDPServer.GetDataCountAsync(endPoint.Address.ToString(), taskID, timeleft);
if (found.HasValue && found.Value >= readTimes)
{
var dataArr = await MsgBus.UDPServer.FindDataArrayAsync(endPoint.Address.ToString(), taskID, timeleft);
if (dataArr.HasValue)
{
udpDatas.AddRange(dataArr.Value);
break;
}
}
}
if (udpDatas.Count < readTimes)
return new(new Exception($"Expected {readTimes} UDP data packets but received {udpDatas.Count}"));
// Collect and validate all received data
for (var i = 0; i < udpDatas.Count; i++)
{
var bytes = udpDatas[i].Data;
var expectedLen = ((pkgList[i].Options.BurstLength + 1) * 4);
if ((bytes.Length - 8) != expectedLen)
return new(new Exception($"Expected {expectedLen} bytes but received {bytes.Length - 8} bytes at segment {i}"));
resultData.AddRange(bytes[8..]);
}
// Validate total data length
if (resultData.Count != dataLength * 4)
return new(new Exception($"Expected total {dataLength * 4} bytes but received {resultData.Count} bytes"));
return resultData.ToArray();
}
/// <summary>
/// 顺序读取多个地址的数据并合并BodyData后返回
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="addr">地址数组</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <returns>合并后的BodyData字节数组</returns>
public static async ValueTask<Result<byte[]>> ReadAddrSeq(IPEndPoint endPoint, int taskID, UInt32[] addr, int timeout = 1000)
{
var length = addr.Length;
var resultData = new List<byte>();
for (int i = 0; i < length; i++)
{
var ret = await ReadAddrByte(endPoint, taskID, addr[i], timeout);
if (!ret.IsSuccessful)
{
logger.Error($"ReadAddrSeq failed at index {i}: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value.IsSuccessful)
{
logger.Error($"ReadAddrSeq failed at index {i}: Read not successful");
return new(new Exception($"ReadAddrSeq failed at index {i}"));
}
var data = ret.Value.Options.Data;
if (data is null)
{
logger.Error($"ReadAddrSeq got null data at index {i}");
return new(new Exception($"ReadAddrSeq got null data at index {i}"));
}
resultData.AddRange(data);
}
return resultData.ToArray();
}
/// <summary>
/// 向设备地址写入32位数据
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param>
/// <param name="data">要写入的32位数据</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <param name="progressId">进度报告器</param>
/// <returns>写入结果true表示写入成功</returns>
public static async ValueTask<Result<bool>> WriteAddr(
IPEndPoint endPoint, int taskID, UInt32 devAddr,
UInt32 data, int timeout = 1000, string progressId = "")
{
var ret = false;
var opts = new SendAddrPackOptions()
{
BurstType = BurstType.FixedBurst,
BurstLength = 0,
CommandID = Convert.ToByte(taskID),
Address = devAddr,
IsWrite = true,
};
_progressTracker.AdvanceProgress(progressId, 10);
// Write Register
ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 1st address package failed!"));
_progressTracker.AdvanceProgress(progressId, 10);
// Send Data Package
ret = await UDPClientPool.SendDataPackAsync(endPoint,
new SendDataPackage(Common.Number.NumberToBytes(data, 4).Value));
if (!ret) return new(new Exception("Send data package failed!"));
_progressTracker.AdvanceProgress(progressId, 10);
// Check Msg Bus
if (!MsgBus.IsRunning)
return new(new Exception("Message bus not working!"));
// Wait for Write Ack
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(
endPoint.Address.ToString(), endPoint.Port, timeout);
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(endPoint, taskID, timeout);
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
_progressTracker.AdvanceProgress(progressId, 10);
return udpWriteAck.Value.IsSuccessful;
}
/// <summary>
/// [TODO:description]
/// 向设备地址写入字节数组数据(支持大数据量分段传输)
/// </summary>
/// <param name="endPoint">[TODO:parameter]</param>
/// <param name="devAddr">[TODO:parameter]</param>
/// <param name="dataArray">[TODO:parameter]</param>
/// <param name="timeout">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public static async ValueTask<Result<bool>> WriteAddr(IPEndPoint endPoint, UInt32 devAddr, byte[] dataArray, int timeout = 1000)
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param>
/// <param name="dataArray">要写入的字节数组</param>
/// <param name="timeout">超时时间(毫秒)</param>
/// <param name="progressId">进度报告器</param>
/// <returns>写入结果true表示写入成功</returns>
public static async ValueTask<Result<bool>> WriteAddr(
IPEndPoint endPoint, int taskID, UInt32 devAddr,
byte[] dataArray, int timeout = 1000, string progressId = "")
{
var ret = false;
var opts = new SendAddrPackOptions();
var opts = new SendAddrPackOptions()
{
BurstType = BurstType.FixedBurst,
CommandID = Convert.ToByte(taskID),
Address = devAddr,
BurstLength = 0,
IsWrite = true,
};
opts.BurstType = BurstType.FixedBurst;
opts.CommandID = 0;
opts.Address = devAddr;
var max4BytesPerRead = 128; // 1024 bytes per read
// Check Msg Bus
if (!MsgBus.IsRunning)
return new(new Exception("Message bus not working!"));
opts.IsWrite = true;
var hasRest = dataArray.Length % (256 * (32 / 8)) != 0;
var hasRest = dataArray.Length % (max4BytesPerRead * (32 / 8)) != 0;
var writeTimes = hasRest ?
dataArray.Length / (256 * (32 / 8)) + 1 :
dataArray.Length / (256 * (32 / 8));
dataArray.Length / (max4BytesPerRead * (32 / 8)) + 1 :
dataArray.Length / (max4BytesPerRead * (32 / 8));
for (var i = 0; i < writeTimes; i++)
{
// Sperate Data Array
var isLastData = i == writeTimes - 1;
var sendDataArray =
isLastData ?
dataArray[(i * (256 * (32 / 8)))..] :
dataArray[(i * (256 * (32 / 8)))..((i + 1) * (256 * (32 / 8)))];
var sendDataArray = isLastData ?
dataArray[(i * (max4BytesPerRead * (32 / 8)))..] :
dataArray[(i * (max4BytesPerRead * (32 / 8)))..((i + 1) * (max4BytesPerRead * (32 / 8)))];
// Calculate BurstLength
opts.BurstLength = ((byte)(
sendDataArray.Length % 4 == 0 ?
(sendDataArray.Length / 4 - 1) :
(sendDataArray.Length / 4)
));
// Write Jtag State Register
opts.BurstLength = ((byte)(sendDataArray.Length / 4 - 1));
ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 1st address package failed!"));
@@ -389,14 +684,51 @@ public class UDPClientPool
if (!ret) return new(new Exception("Send data package failed!"));
// Wait for Write Ack
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(endPoint.Address.ToString(), endPoint.Port, timeout);
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(endPoint, taskID, timeout);
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
if (!udpWriteAck.Value.IsSuccessful)
return false;
_progressTracker.AdvanceProgress(progressId, 1);
}
return true;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="endPoint">[TODO:parameter]</param>
/// <param name="taskID">[TODO:parameter]</param>
/// <param name="addr">[TODO:parameter]</param>
/// <param name="data">[TODO:parameter]</param>
/// <param name="timeout">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public static async ValueTask<Result<bool>> WriteAddrSeq(IPEndPoint endPoint, int taskID, UInt32[] addr, byte[] data, int timeout = 1000)
{
var length = addr.Length;
if (length != data.Length)
{
logger.Error($"TODO");
return new(new ArgumentException($"TODO"));
}
for (int i = 0; i < length; i++)
{
var ret = await WriteAddr(endPoint, taskID, addr[i], (UInt32)data[i], timeout);
if (!ret.IsSuccessful)
{
logger.Error($"TODO");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"TODO");
return false;
}
}
return true;
}
}

View File

@@ -1,7 +1,10 @@
using System.Collections.Concurrent;
using System.Net;
using System.Net.NetworkInformation; // 添加这个引用
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using Common;
using DotNext;
using DotNext.Threading;
using Newtonsoft.Json;
@@ -15,6 +18,10 @@ public class UDPData
/// </summary>
public required DateTime DateTime { get; set; }
/// <summary>
/// 数据包时间戳
/// </summary>
public required UInt32 Timestamp { get; set; }
/// <summary>
/// 发送来源的IP地址
/// </summary>
public required string Address { get; set; }
@@ -22,6 +29,11 @@ public class UDPData
/// 发送来源的端口号
/// </summary>
public required int Port { get; set; }
/// <summary>
/// 任务ID
/// </summary>
public required int TaskID { get; set; }
/// <summary>
/// 接受到的数据
/// </summary>
@@ -42,8 +54,10 @@ public class UDPData
return new UDPData()
{
DateTime = this.DateTime,
Timestamp = this.Timestamp,
Address = new string(this.Address),
Port = this.Port,
TaskID = this.TaskID,
Data = cloneData,
HasRead = this.HasRead
};
@@ -66,17 +80,22 @@ public class UDPServer
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private static Dictionary<string, Queue<UDPData>> udpData = new Dictionary<string, Queue<UDPData>>();
private ConcurrentDictionary<string, SortedList<UInt32, UDPData>> udpData
= new ConcurrentDictionary<string, SortedList<UInt32, UDPData>>();
private readonly AsyncReaderWriterLock udpDataLock = new AsyncReaderWriterLock();
private int listenPort;
private UdpClient listener;
private List<UdpClient> listeners = new List<UdpClient>();
private List<Task> tasks = new List<Task>();
private IPEndPoint groupEP;
private bool isRunning = false;
private CancellationTokenSource? cancellationTokenSource;
private bool disposed = false;
/// <summary>
/// 是否正在工作
/// </summary>
public bool IsRunning { get { return isRunning; } }
public bool IsRunning => cancellationTokenSource?.Token.IsCancellationRequested == false;
/// <summary> UDP 服务器的错误代码 </summary>
public enum ErrorCode
@@ -95,15 +114,27 @@ public class UDPServer
/// Construct a udp server with fixed port
/// </summary>
/// <param name="port"> Device UDP Port </param>
/// <param name="num"> UDP Client Num </param>
/// <returns> UDPServer class </returns>
public UDPServer(int port)
public UDPServer(int port, int num)
{
// Construction
listenPort = port;
this.listenPort = port;
try
{
listener = new UdpClient(listenPort);
groupEP = new IPEndPoint(IPAddress.Any, listenPort);
for (int i = 0; i < num; i++)
{
int currentPort = this.listenPort + i;
if (IsPortInUse(currentPort))
{
throw new ArgumentException(
$"端口{currentPort}已被占用无法启动UDP Server",
nameof(port)
);
}
listeners.Add(new UdpClient(currentPort));
}
this.groupEP = new IPEndPoint(IPAddress.Any, listenPort);
}
catch (Exception e)
{
@@ -115,10 +146,34 @@ public class UDPServer
}
}
private bool IsPortInUse(int port)
{
bool inUse = false;
try
{
var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
var udpListeners = ipGlobalProperties.GetActiveUdpListeners();
foreach (var ep in udpListeners)
{
if (ep.Port == port)
{
inUse = true;
break;
}
}
}
catch (Exception ex)
{
logger.Warn($"Failed to check port usage for port {port}: {ex.Message}");
}
return inUse;
}
/// <summary>
/// 异步寻找目标发送的内容
/// </summary>
/// <param name="ipAddr"> 目标IP地址 </param>
/// <param name="taskID">[TODO:parameter]</param>
/// <param name="timeout">超时时间</param>
/// <param name="cycle">延迟时间</param>
/// <param name="callerName">调用函数名称</param>
@@ -129,82 +184,89 @@ public class UDPServer
/// Optional 存在时,为最先收到的数据
/// </returns>
public async ValueTask<Optional<UDPData>> FindDataAsync(
string ipAddr, int timeout = 1000, int cycle = 0,
string ipAddr, int taskID, int timeout = 1000, int cycle = 0,
[CallerMemberName] string callerName = "",
[CallerLineNumber] int callerLineNum = 0)
[CallerLineNumber] int callerLineNum = 0
)
{
UDPData? data = null;
logger.Debug($"Caller \"{callerName}|{callerLineNum}\": Try to find {ipAddr} UDP Data");
var key = $"{ipAddr}-{taskID}";
var startTime = DateTime.Now;
var isTimeout = false;
var timeleft = TimeSpan.FromMilliseconds(timeout);
while (!isTimeout)
{
var elapsed = DateTime.Now - startTime;
isTimeout = elapsed >= TimeSpan.FromMilliseconds(timeout);
if (isTimeout) break;
timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
using (await udpData.AcquireWriteLockAsync(timeleft))
try
{
while (!isTimeout)
{
if (udpData.ContainsKey(ipAddr) &&
udpData.TryGetValue(ipAddr, out var dataQueue) &&
dataQueue.Count > 0)
var elapsed = DateTime.Now - startTime;
isTimeout = elapsed >= TimeSpan.FromMilliseconds(timeout);
if (isTimeout) break;
using (await udpDataLock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(timeout)))
{
data = dataQueue.Dequeue();
logger.Debug($"Find UDP Data: {data.ToString()}");
break;
if (udpData.TryGetValue(key, out var sortedList) && sortedList.Count > 0)
{
// 获取最早的数据(第一个元素)
var firstKey = sortedList.Keys[0];
data = sortedList[firstKey];
sortedList.RemoveAt(0);
break;
}
}
}
await Task.Delay(cycle);
if (data is null)
throw new TimeoutException("Get nothing even after time out");
else return new(data.DeepClone());
}
if (data is null)
catch
{
logger.Trace("Get nothing even after time out");
return new(null);
}
else
{
return new(data.DeepClone());
}
}
/// <summary>
/// 获取还未被读取的数据列表
/// 异步寻找目标发送的所有内容,并清空队列
/// </summary>
/// <param name="ipAddr">IP地址</param>
/// <param name="ipAddr">目标IP地址</param>
/// <param name="taskID">任务ID</param>
/// <param name="timeout">超时时间</param>
/// <param name="cycle">延迟时间</param>
/// <returns>数据列表</returns>
public async ValueTask<Optional<List<UDPData>>> GetDataArrayAsync(string ipAddr, int timeout = 1000, int cycle = 0)
/// <returns>异步Optional 数据包列表</returns>
public async ValueTask<Optional<List<UDPData>>> FindDataArrayAsync(string ipAddr, int taskID, int timeout = 1000)
{
List<UDPData>? data = null;
var key = $"{ipAddr}-{taskID}";
var startTime = DateTime.Now;
var isTimeout = false;
var timeleft = TimeSpan.FromMilliseconds(timeout);
while (!isTimeout)
{
var elapsed = DateTime.Now - startTime;
isTimeout = elapsed >= TimeSpan.FromMilliseconds(timeout);
if (isTimeout) break;
timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
using (await udpData.AcquireReadLockAsync(timeleft))
try
{
if (udpData.ContainsKey(ipAddr) &&
udpData.TryGetValue(ipAddr, out var dataQueue) &&
dataQueue.Count > 0)
using (await udpDataLock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(timeout)))
{
data = dataQueue.ToList();
logger.Debug($"Find UDP Data Array: {JsonConvert.SerializeObject(data)}");
break;
if (udpData.TryGetValue(key, out var sortedList) && sortedList.Count > 0)
{
data = new List<UDPData>(sortedList.Values);
// 输出数据
// PrintDataArray(data);
sortedList.Clear();
break;
}
}
}
catch
{
logger.Trace("Get nothing even after time out");
return new(null);
}
}
if (data is null)
@@ -218,19 +280,103 @@ public class UDPServer
}
}
/// <summary>
/// 获取还未被读取的数据列表
/// </summary>
/// <param name="ipAddr">IP地址</param>
/// <param name="taskID">任务ID</param>
/// <param name="timeout">超时时间</param>
/// <returns>数据列表</returns>
public async ValueTask<Optional<List<UDPData>>> GetDataArrayAsync(string ipAddr, int taskID, int timeout = 1000)
{
List<UDPData>? data = null;
try
{
using (await udpDataLock.AcquireReadLockAsync(TimeSpan.FromMilliseconds(timeout)))
{
var key = $"{ipAddr}-{taskID}";
if (udpData.TryGetValue(key, out var sortedList) && sortedList.Count > 0)
{
data = new List<UDPData>(sortedList.Values);
}
}
}
catch (TimeoutException)
{
logger.Trace("Failed to acquire read lock within timeout");
return new(null);
}
if (data is null)
{
logger.Trace("Get nothing even after time out");
return new(null);
}
else
{
return new(data);
}
}
/// <summary>
/// 异步获取指定IP和任务ID的数据队列长度
/// </summary>
/// <param name="ipAddr">IP地址</param>
/// <param name="taskID">任务ID</param>
/// <param name="timeout">超时时间</param>
/// <returns>数据队列长度</returns>
public async ValueTask<Optional<int>> GetDataCountAsync(string ipAddr, int taskID, int timeout = 1000)
{
int? count = null;
try
{
using (await udpDataLock.AcquireReadLockAsync(TimeSpan.FromMilliseconds(timeout)))
{
var key = $"{ipAddr}-{taskID}";
if (udpData.TryGetValue(key, out var sortedList))
{
count = sortedList.Count;
}
}
}
catch (TimeoutException)
{
logger.Trace("Failed to acquire read lock within timeout");
return Optional<int>.None;
}
if (count is null)
{
logger.Trace("Get nothing even after time out");
return Optional<int>.None;
}
else
{
return new(count.Value);
}
}
/// <summary>
/// 异步等待写响应
/// </summary>
/// <param name="address">IP地址</param>
/// <param name="port">UDP 端口</param>
/// <param name="endPoint">IP地址及端口</param>
/// <param name="taskID">[TODO:parameter]</param>
/// <param name="timeout">超时时间范围</param>
/// <returns>接收响应包</returns>
public async ValueTask<Result<WebProtocol.RecvRespPackage>> WaitForAckAsync
(string address, int port = -1, int timeout = 1000)
(IPEndPoint endPoint, int taskID, int timeout = 1000)
{
var data = await FindDataAsync(address, timeout);
var address = endPoint.Address.ToString();
var port = endPoint.Port;
var data = await FindDataAsync(address, taskID, timeout);
if (!data.HasValue)
{
await UDPClientPool.SendResetSignal(endPoint);
return new(new Exception("Get None even after time out!"));
}
var recvData = data.Value;
if (recvData.Address != address || (port > 0 && recvData.Port != port))
@@ -246,16 +392,22 @@ public class UDPServer
/// <summary>
/// 异步等待数据
/// </summary>
/// <param name="address">IP地址</param>
/// <param name="port">UDP 端口</param>
/// <param name="endPoint">IP地址</param>
/// <param name="taskID">任务ID</param>
/// <param name="timeout">超时时间范围</param>
/// <returns>接收数据包</returns>
public async ValueTask<Result<RecvDataPackage>> WaitForDataAsync
(string address, int port = -1, int timeout = 1000)
(IPEndPoint endPoint, int taskID, int timeout = 1000)
{
var data = await FindDataAsync(address, timeout);
var address = endPoint.Address.ToString();
var port = endPoint.Port;
var data = await FindDataAsync(address, taskID, timeout);
if (!data.HasValue)
{
await UDPClientPool.SendResetSignal(endPoint);
return new(new Exception("Get None even after time out!"));
}
var recvData = data.Value;
if (recvData.Address != address || (port >= 0 && recvData.Port != port))
@@ -268,75 +420,73 @@ public class UDPServer
return retPack.Value;
}
private void ReceiveHandler(IAsyncResult res)
private async Task ReceiveHandler(byte[] data, IPEndPoint endPoint, DateTime time)
{
logger.Trace("Enter handler");
var remoteEP = new IPEndPoint(IPAddress.Any, listenPort);
byte[] bytes = listener.EndReceive(res, ref remoteEP);
// Handle RemoteEP
if (remoteEP is null)
// 异步锁保护 udpData
await Task.Run(async () =>
{
logger.Debug($"Receive Data from Unknown at {DateTime.Now.ToString()}:");
logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
goto BEGIN_RECEIVE;
}
try
{
// Handle RemoteEP
if (endPoint is null)
{
logger.Debug($"Receive Data from Unknown at {DateTime.Now.ToString()}:");
logger.Debug($" Original Data : {BitConverter.ToString(data).Replace("-", " ")}");
return;
}
// Handle Package
var udpData = RecordUDPData(bytes, remoteEP);
PrintData(udpData);
BEGIN_RECEIVE:
listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
var udpDataObj = await RecordUDPData(data, endPoint, time, Convert.ToInt32(data[1 + 4]));
// PrintData(udpDataObj);
}
catch (Exception e)
{
logger.Error($"Got Error when handle receive:{e}");
}
});
}
private bool SendBytes(IPEndPoint endPoint, byte[] buf)
{
var sendLen = listener.Send(buf, endPoint);
if (sendLen == buf.Length) { return true; }
else { return false; }
}
private bool SendString(IPEndPoint endPoint, string text)
{
byte[] buf = Encoding.ASCII.GetBytes(text);
var sendLen = listener.Send(buf, endPoint);
if (sendLen == buf.Length) { return true; }
else { return false; }
}
private UDPData RecordUDPData(byte[] bytes, IPEndPoint remoteEP)
private async Task<UDPData> RecordUDPData(byte[] bytes, IPEndPoint remoteEP, DateTime time, int taskID)
{
var remoteAddress = remoteEP.Address.ToString();
var remotePort = remoteEP.Port;
var data = new UDPData()
{
DateTime = time,
Timestamp = Number.BytesToUInt32(bytes[..4]).Value,
Address = remoteAddress,
Port = remotePort,
TaskID = taskID,
Data = bytes,
DateTime = DateTime.Now,
HasRead = false,
};
using (udpData.AcquireWriteLock())
var key = $"{remoteAddress}-{taskID}";
try
{
// Record UDP Receive Data
if (udpData.ContainsKey(remoteAddress) && udpData.TryGetValue(remoteAddress, out var dataQueue))
using (await udpDataLock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(5000)))
{
dataQueue.Enqueue(data);
logger.Trace("Receive data from old client");
}
else
{
var queue = new Queue<UDPData>();
queue.Enqueue(data);
udpData.Add(remoteAddress, queue);
logger.Trace("Receive data from new client");
var sortedList = udpData.GetOrAdd(key, _ => new SortedList<UInt32, UDPData>());
// 处理相同时间戳的情况,添加微小的时间差
var uniqueTime = data.Timestamp;
while (sortedList.ContainsKey(uniqueTime))
{
logger.Warn(
$"Duplicate timestamp detected for {remoteAddress}:{remotePort} at {uniqueTime}.");
uniqueTime += 1;
}
sortedList.Add(uniqueTime, data);
// 输出单个数据
// PrintData(data);
}
}
catch (TimeoutException)
{
logger.Error($"Failed to acquire write lock for recording UDP data from {remoteAddress}:{remotePort}");
throw;
}
return data;
}
@@ -345,21 +495,12 @@ public class UDPServer
/// 输出UDP Data到log中
/// </summary>
/// <param name="data">UDP数据</param>
public void PrintData(UDPData data)
public string PrintData(UDPData data)
{
var bytes = data.Data;
var sign = bytes[0];
var sign = bytes[4];
string recvData = "";
if (sign == (byte)WebProtocol.PackSign.SendAddr)
{
var resData = WebProtocol.SendAddrPackage.FromBytes(bytes);
if (resData.IsSuccessful)
recvData = resData.Value.ToString();
else
recvData = resData.Error.ToString();
}
else if (sign == (byte)WebProtocol.PackSign.SendData) { }
else if (sign == (byte)WebProtocol.PackSign.RecvData)
if (sign == (byte)WebProtocol.PackSign.RecvData)
{
var resData = WebProtocol.RecvDataPackage.FromBytes(bytes);
if (resData.IsSuccessful)
@@ -380,49 +521,75 @@ public class UDPServer
recvData = Encoding.ASCII.GetString(bytes, 0, bytes.Length);
}
logger.Debug($"Receive Data from {data.Address}:{data.Port} at {data.DateTime.ToString()}:");
logger.Debug($"Receive Data from {data.Address}:{data.Port} at {data.DateTime.ToString()} - {data.Timestamp}:");
logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
if (recvData.Length != 0) logger.Debug($" Decoded Data : {recvData}");
return $@"
Receive Data from {data.Address}:{data.Port} at {data.DateTime.ToString()}:
Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}
Decoded Data : {recvData}
";
}
/// <summary>
/// 输出UDP Data数组到log中
/// </summary>
/// <param name="dataArray">UDP数据列表</param>
public void PrintDataArray(IEnumerable<UDPData> dataArray)
{
foreach (var data in dataArray)
{
logger.Debug(PrintData(data));
}
}
/// <summary>
/// 将所有数据输出到log中
/// </summary>
/// <returns> void </returns>
public void PrintAllData()
public async Task PrintAllDataAsync()
{
using (udpData.AcquireReadLock())
{
logger.Debug("Ready Data:");
logger.Debug("Ready Data:");
foreach (var ip in udpData)
try
{
using (await udpDataLock.AcquireReadLockAsync(TimeSpan.FromMilliseconds(5000)))
{
foreach (var data in ip.Value)
foreach (var kvp in udpData)
{
logger.Debug(data.ToString());
foreach (var data in kvp.Value.Values)
{
logger.Debug(PrintData(data));
}
}
}
}
catch (TimeoutException)
{
logger.Error("Failed to acquire read lock for printing all data");
}
}
/// <summary>
/// 清空指定IP地址的数据
/// </summary>
/// <param name="ipAddr">IP地址</param>
/// <param name="taskID">[TODO:parameter]</param>
/// <returns>无</returns>
public async Task ClearUDPData(string ipAddr)
public void ClearUDPData(string ipAddr, int taskID)
{
using (await udpData.AcquireWriteLockAsync())
var key = $"{ipAddr}-{taskID}";
using (udpDataLock.AcquireWriteLock())
{
if (udpData.ContainsKey(ipAddr) &&
udpData.TryGetValue(ipAddr, out var dataQueue) &&
dataQueue.Count > 0)
if (udpData.TryGetValue(key, out var sortedList))
{
dataQueue.Clear();
sortedList.Clear();
}
}
}
}
/// <summary>
/// Start UDP Server
@@ -430,17 +597,57 @@ public class UDPServer
/// <returns>None</returns>
public void Start()
{
if (cancellationTokenSource != null && !cancellationTokenSource.Token.IsCancellationRequested)
{
logger.Warn("UDP Server is already running");
return;
}
cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
try
{
this.listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
foreach (var client in listeners)
{
tasks.Add(Task.Run(async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
// 使用 CancellationToken 来取消接收操作
var result = await client.ReceiveAsync(cancellationToken);
_ = ReceiveHandler(result.Buffer, result.RemoteEndPoint, DateTime.Now);
}
catch (OperationCanceledException)
{
logger.Debug("UDP receive operation was cancelled");
break;
}
catch (ObjectDisposedException)
{
logger.Debug("UDP client was disposed");
break;
}
catch (Exception ex)
{
if (!cancellationToken.IsCancellationRequested)
{
logger.Error($"Error in UDP receive: {ex.Message}");
}
}
}
}, cancellationToken));
}
logger.Info("UDP Server started successfully");
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
finally
{
this.isRunning = true;
logger.Error($"Failed to start UDP server: {e}");
cancellationTokenSource?.Cancel();
throw;
}
}
@@ -450,8 +657,71 @@ public class UDPServer
/// <returns>None</returns>
public void Stop()
{
this.listener.Close();
this.isRunning = false;
if (cancellationTokenSource == null || cancellationTokenSource.Token.IsCancellationRequested)
{
logger.Warn("UDP Server is not running or already stopped");
return;
}
try
{
logger.Info("Stopping UDP Server...");
// 取消所有操作
cancellationTokenSource.Cancel();
// 等待所有任务完成,设置超时时间
var waitTasks = Task.WhenAll(tasks);
if (!waitTasks.Wait(TimeSpan.FromSeconds(5)))
{
logger.Warn("Some tasks did not complete within timeout period");
}
// 关闭所有UDP客户端
foreach (var client in listeners)
{
try
{
client.Close();
}
catch (Exception ex)
{
logger.Warn($"Error closing UDP client: {ex.Message}");
}
}
// 清理任务列表
tasks.Clear();
logger.Info("UDP Server stopped successfully");
}
catch (Exception ex)
{
logger.Error($"Error stopping UDP server: {ex.Message}");
}
finally
{
cancellationTokenSource?.Dispose();
cancellationTokenSource = null;
}
}
/// <summary>
/// 实现IDisposable接口确保资源正确释放
/// </summary>
public void Dispose()
{
if (!disposed)
{
Stop();
foreach (var client in listeners)
{
client?.Dispose();
}
udpDataLock?.Dispose();
disposed = true;
}
}
}

View File

@@ -1,3 +1,4 @@
using Common;
using DotNext;
using Newtonsoft.Json;
@@ -35,32 +36,32 @@ namespace WebProtocol
/// 突发类型
/// </summary>
/// <example>0</example>
public BurstType BurstType { get; set; }
public required BurstType BurstType { get; set; }
/// <summary>
/// 任务ID
/// </summary>
/// <example>1</example>
public byte CommandID { get; set; }
public required byte CommandID { get; set; }
/// <summary>
/// 标识写入还是读取
/// </summary>
/// <example>true</example>
public bool IsWrite { get; set; }
public required bool IsWrite { get; set; }
/// <summary>
/// 突发长度0是32bits255是32bits x 256
/// </summary>
/// <example>255</example>
public byte BurstLength { get; set; }
public required byte BurstLength { get; set; }
/// <summary>
/// 目标地址
/// </summary>
/// <example>0</example>
public UInt32 Address { get; set; }
public required UInt32 Address { get; set; }
/// <summary>
/// 转换为Json格式字符串
@@ -84,23 +85,29 @@ namespace WebProtocol
WriteResp
};
/// <summary>
/// 时间戳
/// </summary>
/// <example>1234567</example>
public required UInt32 Timestamp { get; set; }
/// <summary>
/// 数据包类型
/// </summary>
/// <example>0</example>
public PackType Type { get; set; }
public required PackType Type { get; set; }
/// <summary>
/// Task ID
/// </summary>
/// <example>0</example>
public byte CommandID { get; set; }
public required byte CommandID { get; set; }
/// <summary>
/// Whether is succeed to finish command
/// </summary>
/// <example>true</example>
public bool IsSuccess { get; set; }
public required bool IsSuccess { get; set; }
/// <summary>
/// Return Data
@@ -124,7 +131,7 @@ namespace WebProtocol
readonly byte sign = (byte)PackSign.SendAddr;
readonly byte commandType;
readonly byte burstLength;
readonly byte _reserved = 0;
readonly byte commandID;
readonly UInt32 address;
/// <summary>
@@ -133,10 +140,10 @@ namespace WebProtocol
/// <param name="opts"> 地址包选项 </param>
public SendAddrPackage(SendAddrPackOptions opts)
{
byte byteBurstType = Convert.ToByte((byte)opts.BurstType << 6);
byte byteCommandID = Convert.ToByte((opts.CommandID & 0x03) << 4);
byte byteBurstType = Convert.ToByte((byte)opts.BurstType << 4);
byte byteIsWrite = (opts.IsWrite ? (byte)0x01 : (byte)0x00);
this.commandType = Convert.ToByte(byteBurstType | byteCommandID | byteIsWrite);
this.commandType = Convert.ToByte(byteBurstType | byteIsWrite);
this.commandID = opts.CommandID;
this.burstLength = opts.BurstLength;
this.address = opts.Address;
}
@@ -151,10 +158,10 @@ namespace WebProtocol
/// <param name="address"> 设备地址 </param>
public SendAddrPackage(BurstType burstType, byte commandID, bool isWrite, byte burstLength, UInt32 address)
{
byte byteBurstType = Convert.ToByte((byte)burstType << 6);
byte byteCommandID = Convert.ToByte((commandID & 0x03) << 4);
byte byteBurstType = Convert.ToByte((byte)burstType << 4);
byte byteIsWrite = (isWrite ? (byte)0x01 : (byte)0x00);
this.commandType = Convert.ToByte(byteBurstType | byteCommandID | byteIsWrite);
this.commandType = Convert.ToByte(byteBurstType | byteIsWrite);
this.commandID = commandID;
this.burstLength = burstLength;
this.address = address;
}
@@ -165,9 +172,10 @@ namespace WebProtocol
/// <param name="commandType">二进制命令类型</param>
/// <param name="burstLength">突发长度</param>
/// <param name="address">写入或读取的地址</param>
public SendAddrPackage(byte commandType, byte burstLength, UInt32 address)
public SendAddrPackage(byte commandType, byte burstLength, byte commandID, UInt32 address)
{
this.commandType = commandType;
this.commandID = commandID;
this.burstLength = burstLength;
this.address = address;
}
@@ -183,8 +191,8 @@ namespace WebProtocol
{
Address = this.address,
BurstLength = this.burstLength,
BurstType = (BurstType)(this.commandType >> 6),
CommandID = Convert.ToByte((this.commandType >> 4) & 0b11),
BurstType = (BurstType)(this.commandType >> 4),
CommandID = this.commandID,
IsWrite = Convert.ToBoolean(this.commandType & 1)
};
}
@@ -200,7 +208,7 @@ namespace WebProtocol
arr[0] = sign;
arr[1] = commandType;
arr[2] = burstLength;
arr[3] = _reserved;
arr[3] = commandID;
var bytesAddr = Common.Number.NumberToBytes(address, 4).Value;
Array.Copy(bytesAddr, 0, arr, 4, bytesAddr.Length);
@@ -214,12 +222,14 @@ namespace WebProtocol
/// <returns> 字符串 </returns>
public override string ToString()
{
var opts = new SendAddrPackOptions();
opts.BurstType = (BurstType)(commandType >> 6);
opts.CommandID = Convert.ToByte((commandType >> 4) & 0b0011);
opts.IsWrite = Convert.ToBoolean(commandType & 0x01);
opts.BurstLength = burstLength;
opts.Address = address;
var opts = new SendAddrPackOptions()
{
BurstType = (BurstType)(commandType >> 4),
CommandID = this.commandID,
IsWrite = Convert.ToBoolean(commandType & 0x01),
BurstLength = burstLength,
Address = address,
};
return JsonConvert.SerializeObject(opts);
}
@@ -249,7 +259,7 @@ namespace WebProtocol
}
var address = Common.Number.BytesToUInt64(bytes[4..]).Value;
return new SendAddrPackage(bytes[1], bytes[2], Convert.ToUInt32(address));
return new SendAddrPackage(bytes[1], bytes[2], bytes[3], Convert.ToUInt32(address));
}
}
@@ -269,6 +279,9 @@ namespace WebProtocol
if (bodyData.Length > 256 * (32 / 8))
throw new Exception("The data of SendDataPackage can't over 256 * 32bits");
if (bodyData.Length % 4 != 0)
throw new Exception("The data of SendDataPackage should be divided by 4");
this.bodyData = bodyData;
_ = _reserved;
@@ -294,8 +307,9 @@ namespace WebProtocol
}
/// <summary> FPGA->Server 读响应包 </summary>
public struct RecvDataPackage
public class RecvDataPackage
{
readonly UInt32 timestamp;
readonly byte sign = (byte)PackSign.RecvData;
readonly byte commandID;
readonly byte resp;
@@ -303,14 +317,16 @@ namespace WebProtocol
readonly byte[] bodyData;
/// <summary>
/// FPGA->Server 读响应包
/// FPGA->Server 读响应包
/// 构造函数
/// </summary>
/// <param name="timestamp"> 时间戳 </param>
/// <param name="commandID"> 任务ID号 </param>
/// <param name="resp"> 读响应包响应 </param>
/// <param name="bodyData"> 数据 </param>
public RecvDataPackage(byte commandID, byte resp, byte[] bodyData)
public RecvDataPackage(UInt32 timestamp, byte commandID, byte resp, byte[] bodyData)
{
this.timestamp = timestamp;
this.commandID = commandID;
this.resp = resp;
this.bodyData = bodyData;
@@ -319,26 +335,13 @@ namespace WebProtocol
_ = this._reserved;
}
/// <summary>
/// FPGA->Server 读响应包
/// 构造函数
/// </summary>
/// <param name="commandID"> 任务ID号 </param>
/// <param name="isSuccess">是否读取成功</param>
/// <param name="bodyData"> 数据 </param>
public RecvDataPackage(byte commandID, bool isSuccess, byte[] bodyData)
{
this.commandID = commandID;
this.resp = Convert.ToByte(isSuccess);
this.bodyData = bodyData;
}
/// <summary>
/// 通过接受包选项构建读响应包
/// </summary>
/// <param name="opts">接收包(读响应包和写响应包)选项</param>
public RecvDataPackage(RecvPackOptions opts)
{
this.timestamp = opts.Timestamp;
this.commandID = opts.CommandID;
this.resp = Convert.ToByte(opts.IsSuccess ? 0b10 : 0b00);
this.bodyData = opts.Data ?? (byte[])[0, 0, 0, 0];
@@ -351,11 +354,14 @@ namespace WebProtocol
{
get
{
var opts = new RecvPackOptions();
opts.Type = RecvPackOptions.PackType.ReadResp;
opts.CommandID = commandID;
opts.IsSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? false : true);
opts.Data = bodyData;
var opts = new RecvPackOptions()
{
Timestamp = this.timestamp,
Type = RecvPackOptions.PackType.ReadResp,
CommandID = this.commandID,
IsSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? false : true),
Data = this.bodyData,
};
return opts;
}
@@ -366,7 +372,7 @@ namespace WebProtocol
/// </summary>
public bool IsSuccessful
{
get { return Convert.ToBoolean((resp >> 1) == 0b01 ? false : true); }
get { return Convert.ToBoolean((this.resp >> 1) == 0b01 ? false : true); }
}
/// <summary>
@@ -376,12 +382,26 @@ namespace WebProtocol
/// <returns>读响应包</returns>
public static Result<RecvDataPackage> FromBytes(byte[] bytes)
{
if (bytes[0] != (byte)PackSign.RecvData)
if (bytes[4] != (byte)PackSign.RecvData)
return new(new ArgumentException(
$"The sign of bytes is not RecvData Package, Sign: 0x{BitConverter.ToString([bytes[0]])}",
nameof(bytes)
));
return new RecvDataPackage(bytes[1], bytes[2], bytes[4..]);
return new RecvDataPackage(
Number.BytesToUInt32(bytes[..4]).Value,
bytes[5],
bytes[6],
bytes[8..]);
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="bytes">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public static bool IsRecvDataPackage(byte[] bytes)
{
return bytes[4] == (byte)PackSign.RecvData;
}
/// <summary>
@@ -391,13 +411,16 @@ namespace WebProtocol
public byte[] ToBytes()
{
var bodyDataLen = bodyData.Length;
var arr = new byte[4 + bodyDataLen];
var arr = new byte[8 + bodyDataLen];
arr[0] = this.sign;
arr[1] = this.commandID;
arr[2] = this.resp;
Buffer.BlockCopy(
Number.UInt32ArrayToBytes([this.timestamp]).Value, 0, arr, 0, 4);
arr[4] = this.sign;
arr[5] = this.commandID;
arr[6] = this.resp;
arr[7] = this.resp;
Array.Copy(bodyData, 0, arr, 4, bodyDataLen);
Array.Copy(bodyData, 0, arr, 8, bodyDataLen);
return arr;
}
@@ -405,8 +428,9 @@ namespace WebProtocol
}
/// <summary> 写响应包 </summary>
public struct RecvRespPackage
public class RecvRespPackage
{
readonly UInt32 timestamp;
readonly byte sign = (byte)PackSign.RecvResp;
readonly byte commandID;
readonly byte resp;
@@ -415,10 +439,12 @@ namespace WebProtocol
/// <summary>
/// 构建写响应包
/// </summary>
/// <param name="timestamp">时间戳</param>
/// <param name="commandID">任务ID</param>
/// <param name="resp">写响应</param>
public RecvRespPackage(byte commandID, byte resp)
public RecvRespPackage(UInt32 timestamp, byte commandID, byte resp)
{
this.timestamp = timestamp;
this.commandID = commandID;
this.resp = resp;
@@ -426,23 +452,13 @@ namespace WebProtocol
_ = this._reserved;
}
/// <summary>
/// 构建写响应包
/// </summary>
/// <param name="commandID">任务ID</param>
/// <param name="isSuccess">是否写成功</param>
public RecvRespPackage(byte commandID, bool isSuccess)
{
this.commandID = commandID;
this.resp = Convert.ToByte(isSuccess);
}
/// <summary>
/// 通过接受包选项构建写响应包
/// </summary>
/// <param name="opts">接收包(读响应包和写响应包)选项</param>
public RecvRespPackage(RecvPackOptions opts)
{
this.timestamp = opts.Timestamp;
this.commandID = opts.CommandID;
this.resp = Convert.ToByte(opts.IsSuccess ? 0b10 : 0b00);
}
@@ -454,11 +470,14 @@ namespace WebProtocol
{
get
{
var opts = new RecvPackOptions();
opts.Type = RecvPackOptions.PackType.WriteResp;
opts.CommandID = commandID;
opts.IsSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? false : true);
opts.Data = null;
var opts = new RecvPackOptions()
{
Timestamp = this.timestamp,
Type = RecvPackOptions.PackType.WriteResp,
CommandID = commandID,
IsSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? false : true),
Data = null,
};
return opts;
}
@@ -469,7 +488,7 @@ namespace WebProtocol
/// </summary>
public bool IsSuccessful
{
get { return Convert.ToBoolean((resp >> 1) == 0b01 ? false : true); }
get { return Convert.ToBoolean((this.resp >> 1) == 0b01 ? false : true); }
}
/// <summary>
@@ -479,12 +498,23 @@ namespace WebProtocol
/// <returns>写响应包</returns>
public static Result<RecvRespPackage> FromBytes(byte[] bytes)
{
if (bytes[0] != (byte)PackSign.RecvResp)
if (bytes[4] != (byte)PackSign.RecvResp)
return new(new ArgumentException(
$"The sign of bytes is not RecvResp Package, Sign: 0x{BitConverter.ToString([bytes[0]])}",
$"The sign of bytes is not RecvResp Package, Sign: 0x{BitConverter.ToString([bytes[4]])}",
nameof(bytes)
));
return new RecvRespPackage(bytes[1], bytes[2]);
var timestamp = Number.BytesToUInt32(bytes[..4]).Value;
return new RecvRespPackage(timestamp, bytes[5], bytes[6]);
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="bytes">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public static bool IsRecvRespPackage(byte[] bytes)
{
return bytes[4] == (byte)PackSign.RecvResp;
}
/// <summary>
@@ -493,11 +523,13 @@ namespace WebProtocol
/// <returns>字节数组</returns>
public byte[] ToBytes()
{
var arr = new byte[4];
arr[0] = this.sign;
arr[1] = this.commandID;
arr[2] = this.resp;
var arr = new byte[8];
Buffer.BlockCopy(
Number.UInt32ArrayToBytes([this.timestamp]).Value, 0, arr, 0, 4);
arr[4] = this.sign;
arr[5] = this.commandID;
arr[6] = this.resp;
arr[7] = this._reserved;
return arr;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,24 @@
<script setup lang="ts">
import Navbar from "./components/Navbar.vue";
import Dialog from "./components/Dialog.vue";
import { Alert, useAlertProvider } from "./components/Alert";
import { ref, provide, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useThemeStore } from "./stores/theme";
const router = useRouter();
const theme = useThemeStore();
// 主题切换状态管理
const isDarkMode = ref(
window.matchMedia("(prefers-color-scheme: dark)").matches,
);
const isDarkMode = ref(theme.isDarkTheme());
// Navbar显示状态管理
const showNavbar = ref(true);
// 切换Navbar显示状态
const toggleNavbar = () => {
showNavbar.value = !showNavbar.value;
};
// 初始化主题设置
onMounted(() => {
@@ -37,6 +46,7 @@ const applyTheme = () => {
// 切换主题
const toggleTheme = () => {
isDarkMode.value = !isDarkMode.value;
theme.toggleTheme();
applyTheme();
};
@@ -46,24 +56,37 @@ provide("theme", {
toggleTheme,
});
// 提供Navbar控制给子组件
provide("navbar", {
showNavbar,
toggleNavbar,
});
const currentRoutePath = computed(() => {
return router.currentRoute.value.path;
});
useAlertProvider();
</script>
<template>
<div>
<header class="relative">
<Navbar></Navbar>
<Dialog></Dialog>
<header class="relative" :class="{ 'navbar-hidden': !showNavbar }">
<Navbar v-show="showNavbar" />
<Dialog />
<Alert />
</header>
<main>
<RouterView />
</main>
<footer v-if="currentRoutePath != '/project'" class="footer footer-center p-4 bg-base-300 text-base-content">
<footer
v-if="currentRoutePath != '/project'"
class="footer footer-center p-4 bg-base-300 text-base-content"
>
<div>
<p>Copyright © 2023 - All right reserved by OurEDA</p>
<p>Copyright © 2025 - All right reserved by OurEDA</p>
</div>
</footer>
</div>
@@ -71,4 +94,25 @@ const currentRoutePath = computed(() => {
<style scoped>
/* 特定于App.vue的样式 */
header {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: top;
}
.navbar-hidden {
transform: scaleY(0);
height: 0;
overflow: hidden;
}
/* Navbar显示/隐藏动画 */
header .navbar {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: top;
}
/* 当header被隐藏时确保navbar也相应变化 */
.navbar-hidden .navbar {
opacity: 0;
}
</style>

View File

@@ -1,22 +0,0 @@
import { type FileParameter } from "./APIClient";
import { isNull, isUndefined } from "lodash";
export namespace Common {
export function toFileParameter(object: File): FileParameter {
if (isNull(object) || isUndefined(object))
throw new Error("File is Null or Undefined");
return {
data: object,
fileName: object.name
}
}
export function toFileParameterOrNull(object?: File | null): FileParameter | null {
if (isNull(object) || isUndefined(object)) return null;
else return {
data: object,
fileName: object.name
}
}
}

10
src/assets/base.css Normal file
View File

@@ -0,0 +1,10 @@
@import "tailwindcss";
@plugin "daisyui" {
themes: winter --default, night --prefersdark;
}
@custom-variant dark (&:where([data-theme=night], [data-theme=night] *));
@custom-variant light (&:where([data-theme=winter], [data-theme=winter] *));

View File

@@ -1,11 +1,4 @@
@import "tailwindcss";
@plugin "daisyui" {
themes: winter --default, night --prefersdark;
}
@custom-variant dark (&:where([data-theme=night], [data-theme=night] *));
@custom-variant light (&:where([data-theme=winter], [data-theme=winter] *));
@import "base.css";
/* 禁止所有图像和SVG选择 */
img, svg {

View File

@@ -1 +0,0 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1741694797806" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2622" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M192 192m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" p-id="2623"></path><path d="M192 512m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" p-id="2624"></path><path d="M192 832m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" p-id="2625"></path><path d="M864 160H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 480H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 800H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32z" p-id="2626"></path></svg>

Before

Width:  |  Height:  |  Size: 870 B

View File

@@ -1 +0,0 @@
<svg t="1741522876251" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3628" width="200" height="200"><path d="M327.04 85.333333h369.92C841.472 85.333333 938.666667 186.794667 938.666667 337.749333v348.501334C938.666667 837.205333 841.472 938.666667 696.874667 938.666667h-229.546667a32 32 0 0 1 0-64h229.546667c107.989333 0 177.792-73.941333 177.792-188.416V337.749333c0-114.474667-69.802667-188.416-177.749334-188.416H327.04C219.093333 149.333333 149.333333 223.274667 149.333333 337.749333v348.501334c0 114.474667 69.76 188.416 177.706667 188.416a32 32 0 0 1 0 64C182.442667 938.666667 85.333333 837.205333 85.333333 686.250667V337.749333C85.333333 186.794667 182.442667 85.333333 327.04 85.333333z m-51.114667 381.098667a31.914667 31.914667 0 0 1 42.325334-16.042667 31.914667 31.914667 0 0 1 16.042666 42.282667A47.061333 47.061333 0 1 0 424.192 512c0-25.898667-21.077333-46.933333-47.018667-46.933333a32 32 0 0 1 0-64c50.048 0 91.904 33.408 105.728 78.933333h242.858667a32 32 0 0 1 32 32v79.018667a32 32 0 0 1-64 0V544h-56.704v47.018667a32 32 0 0 1-64 0V544h-90.154667a110.72 110.72 0 0 1-105.728 79.018667 111.104 111.104 0 0 1-101.248-156.544z" fill="#200E32" p-id="3629"></path></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1 +0,0 @@
<svg t="1741522263287" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2626" width="200" height="200"><path d="M511.913993 941.605241c-255.612968 0-385.311608-57.452713-385.311608-170.810012 0-80.846632 133.654964-133.998992 266.621871-151.88846L393.224257 602.049387c-79.986561-55.904586-118.86175-153.436587-118.86175-297.240383 0-139.33143 87.211154-222.586259 233.423148-222.586259l7.912649 0c146.211994 0 233.423148 83.254829 233.423148 222.586259 0 54.184445 0 214.67361-117.829666 297.412397l-0.344028 16.685369c132.966907 18.061482 266.105829 71.041828 266.105829 151.716445C897.225601 884.152528 767.526961 941.605241 511.913993 941.605241zM507.957668 141.567613c-79.470519 0-174.250294 28.382328-174.250294 163.241391 0 129.698639 34.230808 213.469511 104.584579 255.784982 8.944734 5.332437 14.277171 14.965228 14.277171 25.286074l0 59.344868c0 15.309256-11.524945 28.0383-26.662187 29.414413-144.319839 14.449185-239.959684 67.429531-239.959684 95.983874 0 92.199563 177.346548 111.637158 325.966739 111.637158 148.792206 0 325.966739-19.26558 325.966739-111.637158 0-28.726356-95.639845-81.534688-239.959684-95.983874-15.48127-1.548127-27.006215-14.621199-26.662187-30.102469l1.376113-59.344868c0.172014-10.148833 5.676466-19.437594 14.277171-24.770032 70.525785-42.487485 103.208466-123.678145 103.208466-255.784982 0-135.031077-94.779775-163.241391-174.250294-163.241391L507.957668 141.567613 507.957668 141.567613z" fill="#575B66" p-id="2627"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,103 @@
<template>
<div class="fixed left-1/2 top-30 z-[9999] -translate-x-1/2">
<transition
name="alert"
enter-active-class="alert-enter-active"
leave-active-class="alert-leave-active"
enter-from-class="alert-enter-from"
enter-to-class="alert-enter-to"
leave-from-class="alert-leave-from"
leave-to-class="alert-leave-to"
>
<div
v-if="alertStore?.alertState.value.visible"
:class="alertClasses"
class="alert"
>
<div class="flex items-center gap-2">
<!-- Icons for different alert types -->
<CheckCircle
v-if="alertStore?.alertState.value.type === 'success'"
class="h-6 w-6 shrink-0 stroke-current"
/>
<XCircle
v-else-if="alertStore?.alertState.value.type === 'error'"
class="h-6 w-6 shrink-0 stroke-current"
/>
<AlertTriangle
v-else-if="alertStore?.alertState.value.type === 'warning'"
class="h-6 w-6 shrink-0 stroke-current"
/>
<Info v-else class="h-6 w-6 shrink-0 stroke-current" />
<span>{{ alertStore?.alertState.value.message }}</span>
</div>
<div class="flex-none">
<button
class="btn btn-sm btn-circle btn-ghost"
@click="alertStore?.hide"
>
<X class="h-4 w-4" />
</button>
</div>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { CheckCircle, XCircle, AlertTriangle, Info, X } from "lucide-vue-next";
import { useAlertStore } from ".";
import { useRequiredInjection } from "@/utils/Common";
const alertStore = useRequiredInjection(useAlertStore);
// Computed classes for different alert types
const alertClasses = computed(() => {
const baseClasses = "shadow-lg max-w-sm";
switch (alertStore.alertState.value.type) {
case "success":
return `${baseClasses} alert-success`;
case "error":
return `${baseClasses} alert-error`;
case "warning":
return `${baseClasses} alert-warning`;
case "info":
default:
return `${baseClasses} alert-info`;
}
});
</script>
<style scoped>
/* 进入和离开的过渡动画持续时间 */
.alert-enter-active,
.alert-leave-active {
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
/* 进入的起始状态 */
.alert-enter-from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
/* 进入的结束状态 */
.alert-enter-to {
opacity: 1;
transform: translateY(0) scale(1);
}
/* 离开的起始状态 */
.alert-leave-from {
opacity: 1;
transform: translateY(0) scale(1);
}
/* 离开的结束状态 */
.alert-leave-to {
opacity: 0;
transform: translateY(-10px) scale(0.98);
}
</style>

View File

@@ -0,0 +1,82 @@
import { ref, computed } from "vue";
import Alert from "./Alert.vue";
import { createInjectionState } from "@vueuse/core";
export interface AlertState {
visible: boolean;
message: string;
type: "success" | "error" | "warning" | "info";
}
// create injectivon state using vueuse
const [useAlertProvider, useAlertStore] = createInjectionState(() => {
const alertState = ref<AlertState>({
visible: false,
message: "",
type: "info",
});
let timeoutId: number | null = null;
function show(
message: string,
type: AlertState["type"] = "info",
duration = 2000,
) {
// Clear existing timeout
if (timeoutId) {
window.clearTimeout(timeoutId);
}
alertState.value = {
visible: true,
message,
type,
};
// Auto hide after duration
if (duration > 0) {
timeoutId = window.setTimeout(() => {
hide();
}, duration);
}
}
function hide() {
alertState.value.visible = false;
if (timeoutId) {
window.clearTimeout(timeoutId);
timeoutId = null;
}
}
// Convenience methods for different alert types
function error(message: string, duration = 2000) {
show(message, "error", duration);
}
function info(message: string, duration = 2000) {
show(message, "info", duration);
}
function warn(message: string, duration = 2000) {
show(message, "warning", duration);
}
function success(message: string, duration = 2000) {
show(message, "success", duration);
}
return {
alertState,
show,
hide,
error,
info,
warn,
success,
};
});
export { Alert, useAlertProvider, useAlertStore };

View File

@@ -1,466 +0,0 @@
<template>
<div>
<!-- 元器件选择菜单 (Drawer) -->
<div class="drawer drawer-end z-50">
<input id="component-drawer" type="checkbox" class="drawer-toggle" v-model="showComponentsMenu" />
<div class="drawer-side">
<label for="component-drawer" aria-label="close sidebar" class="drawer-overlay !bg-opacity-50"></label>
<div class="menu p-0 w-[460px] min-h-full bg-base-100 shadow-xl flex flex-col">
<!-- 菜单头部 -->
<div class="p-6 border-b border-base-300 flex justify-between items-center">
<h3 class="text-xl font-bold text-primary flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-primary">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 8v8"></path>
<path d="M8 12h8"></path>
</svg>
添加元器件
</h3>
<label for="component-drawer" class="btn btn-ghost btn-sm btn-circle" @click="closeMenu">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</label>
</div>
<!-- 导航栏 -->
<div class="tabs tabs-boxed bg-base-200 mx-6 mt-4 rounded-box">
<a class="tab" :class="{ 'tab-active': activeTab === 'components' }"
@click="activeTab = 'components'">元器件</a>
<a class="tab" :class="{ 'tab-active': activeTab === 'templates' }" @click="activeTab = 'templates'">模板</a>
<a class="tab" :class="{ 'tab-active': activeTab === 'virtual' }" @click="activeTab = 'virtual'">虚拟外设</a>
</div>
<!-- 搜索框 -->
<div class="px-6 py-4 border-b border-base-300">
<div class="join w-full">
<div class="join-item flex-1 relative">
<input type="text" placeholder="搜索..." class="input input-bordered input-sm w-full pl-10"
v-model="searchQuery" @keyup.enter="searchComponents" />
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content opacity-60">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<button class="btn btn-sm join-item" @click="searchComponents">
搜索
</button>
</div>
</div>
<!-- 元器件列表 (组件选项卡) -->
<div v-if="activeTab === 'components'" class="px-6 py-4 overflow-auto flex-1">
<div v-if="filteredComponents.length > 0" class="grid grid-cols-2 gap-4">
<div v-for="(component, index) in filteredComponents" :key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addComponent(component)">
<div class="card-body p-3 items-center text-center">
<div
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2">
<!-- 直接使用组件作为预览 -->
<component v-if="componentModules[component.type]" :is="componentModules[component.type].default"
class="component-preview" :size="getPreviewSize(component.type)" />
<!-- 加载中状态 -->
<span v-else class="text-xs text-gray-400">加载中...</span>
</div>
<h3 class="card-title text-sm mt-2">{{ component.name }}</h3>
<p class="text-xs opacity-70">{{ component.type }}</p>
</div>
</div>
</div>
<!-- 无搜索结果 -->
<div v-else class="py-16 text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
class="mx-auto text-base-300 mb-3">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
<p class="text-base-content opacity-70">没有找到匹配的元器件</p>
<button class="btn btn-sm btn-ghost mt-3" @click="searchQuery = ''">
清除搜索
</button>
</div>
</div>
<!-- 模板列表 (模板选项卡) -->
<div v-if="activeTab === 'templates'" class="px-6 py-4 overflow-auto flex-1">
<div v-if="filteredTemplates.length > 0" class="grid grid-cols-2 gap-4">
<div v-for="(template, index) in filteredTemplates" :key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addTemplate(template)">
<div class="card-body p-3 items-center text-center">
<div
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2">
<img :src="template.thumbnailUrl || '/placeholder-template.png'
" alt="Template thumbnail" class="max-h-full max-w-full object-contain" />
</div>
<h3 class="card-title text-sm mt-2">{{ template.name }}</h3>
<p class="text-xs opacity-70">
{{ template.description || "模板" }}
</p>
</div>
</div>
</div>
<!-- 无搜索结果 -->
<div v-else class="py-16 text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
class="mx-auto text-base-300 mb-3">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
<p class="text-base-content opacity-70">没有找到匹配的模板</p>
<button class="btn btn-sm btn-ghost mt-3" @click="searchQuery = ''">
清除搜索
</button>
</div>
</div>
<!-- 虚拟外设列表 (虚拟外设选项卡) -->
<div v-if="activeTab === 'virtual'" class="px-6 py-4 overflow-auto flex-1">
<div v-if="filteredVirtualDevices.length > 0" class="grid grid-cols-2 gap-4">
<div v-for="(device, index) in filteredVirtualDevices" :key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addComponent(device)">
<div class="card-body p-3 items-center text-center">
<div
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2">
<!-- 直接使用组件作为预览 -->
<component v-if="componentModules[device.type]" :is="componentModules[device.type].default"
class="component-preview" :size="getPreviewSize(device.type)" />
<!-- 加载中状态 -->
<span v-else class="text-xs text-gray-400">加载中...</span>
</div>
<h3 class="card-title text-sm mt-2">{{ device.name }}</h3>
<p class="text-xs opacity-70">{{ device.type }}</p>
</div>
</div>
</div>
<!-- 无搜索结果 -->
<div v-else class="py-16 text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
class="mx-auto text-base-300 mb-3">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
<p class="text-base-content opacity-70">没有找到匹配的虚拟外设</p>
<button class="btn btn-sm btn-ghost mt-3" @click="searchQuery = ''">
清除搜索
</button>
</div>
</div>
<!-- 底部操作区 -->
<div class="p-4 border-t border-base-300 bg-base-200 flex justify-between">
<label for="component-drawer" class="btn btn-sm btn-ghost" @click="closeMenu">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1">
<path d="M19 12H5M12 19l-7-7 7-7"></path>
</svg>
返回
</label>
<label for="component-drawer" class="btn btn-sm btn-primary" @click="closeMenu">
完成
</label>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, shallowRef, onMounted } from "vue";
import motherboardSvg from "../components/equipments/svg/motherboard.svg";
import buttonSvg from "../components//equipments/svg/button.svg";
// Props 定义
interface Props {
open: boolean;
}
const props = defineProps<Props>();
// 定义组件发出的事件
const emit = defineEmits([
"close",
"add-component",
"add-template",
"update:open",
]);
// 当前激活的选项卡
const activeTab = ref("components");
// --- 搜索功能 ---
const searchQuery = ref("");
// --- 可用元器件列表 ---
const availableComponents = [
{ type: "MechanicalButton", name: "机械按钮" },
{ type: "Switch", name: "开关" },
{ type: "Pin", name: "引脚" },
{ type: "SMT_LED", name: "贴片LED" },
{ type: "SevenSegmentDisplay", name: "数码管" },
{ type: "HDMI", name: "HDMI接口" },
{ type: "DDR", name: "DDR内存" },
{ type: "ETH", name: "以太网接口" },
{ type: "SD", name: "SD卡插槽" },
{ type: "SFP", name: "SFP光纤模块" },
{ type: "SMA", name: "SMA连接器" },
{ type: "MotherBoard", name: "主板" },
{ type: "PG2L100H_FBG676", name: "PG2L100H FBG676芯片" },
{ type: "BaseBoard", name: "通用底板" },
];
// --- 可用虚拟外设列表 ---
const availableVirtualDevices = [{ type: "DDS", name: "信号发生器" }];
// --- 可用模板列表 ---
const availableTemplates = ref([
{
name: "PG2L100H 基础开发板",
id: "PG2L100H_Pango100pro",
description: "包含主板和两个LED的基本设置",
path: "/EquipmentTemplates/PG2L100H_Pango100pro.json",
thumbnailUrl: motherboardSvg,
},
{
name: "矩阵键盘",
id: "MatrixKey",
description: "包含4x4共16个按键的矩阵键盘",
path: "/EquipmentTemplates/MatrixKey.json",
thumbnailUrl: buttonSvg,
},
]);
// 显示/隐藏组件菜单
const showComponentsMenu = computed({
get: () => props.open,
set: (value) => emit("update:open", value),
});
// 组件模块缓存
const componentModules = shallowRef<Record<string, any>>({});
// 动态加载组件定义
async function loadComponentModule(type: string) {
if (!componentModules.value[type]) {
try {
// 假设组件都在 src/components/equipments/ 目录下,且文件名与 type 相同
const module = await import(`../components/equipments/${type}.vue`);
// 将模块添加到缓存中
componentModules.value = {
...componentModules.value,
[type]: module,
};
console.log(`Loaded module for ${type}:`, module);
} catch (error) {
console.error(`Failed to load component module ${type}:`, error);
return null;
}
}
return componentModules.value[type];
}
// 预加载组件模块
async function preloadComponentModules() {
// 加载基础组件
for (const component of availableComponents) {
try {
await loadComponentModule(component.type);
} catch (error) {
console.error(`Failed to preload component ${component.type}:`, error);
}
}
// 加载虚拟外设组件
for (const device of availableVirtualDevices) {
try {
await loadComponentModule(device.type);
} catch (error) {
console.error(`Failed to preload virtual device ${device.type}:`, error);
}
}
}
// 获取组件预览时适合的尺寸
function getPreviewSize(componentType: string): number {
// 根据组件类型返回适当的预览尺寸
const previewSizes: Record<string, number> = {
MechanicalButton: 0.4, // 按钮较大,需要更小尺寸
Switch: 0.35, // 开关较大,需要更小尺寸
Pin: 0.8, // 引脚较小,可以大一些
SMT_LED: 0.7, // LED可以保持适中
SevenSegmentDisplay: 0.4, // 数码管较大,需要较小尺寸
HDMI: 0.5, // HDMI接口较大
DDR: 0.5, // DDR内存较大
ETH: 0.5, // 以太网接口较大
SD: 0.6, // SD卡插槽适中
SFP: 0.4, // SFP光纤模块较大
SMA: 0.7, // SMA连接器可以适中
MotherBoard: 0.13, // 主板最大,需要最小尺寸
DDS: 0.3, // 信号发生器较大,需要较小尺寸
};
// 返回对应尺寸如果没有特定配置则返回默认值0.5
return previewSizes[componentType] || 0.5;
}
// 搜索组件
function searchComponents() {
// 根据用户输入过滤可用组件列表
// 实际逻辑已经在 filteredComponents 计算属性中实现
}
// 关闭菜单
function closeMenu() {
showComponentsMenu.value = false;
emit("close");
}
// 添加新元器件
async function addComponent(componentTemplate: { type: string; name: string }) {
// 先加载组件模块
const moduleRef = await loadComponentModule(componentTemplate.type);
let defaultProps: Record<string, any> = {};
// 尝试直接调用组件导出的getDefaultProps方法
if (moduleRef) {
if (typeof moduleRef.getDefaultProps === "function") {
defaultProps = moduleRef.getDefaultProps();
console.log(
`Got default props from ${componentTemplate.type}:`,
defaultProps,
);
} else {
// 回退到配置文件
console.log(`No getDefaultProps found for ${componentTemplate.type}`);
}
} else {
console.log(`Failed to load module for ${componentTemplate.type}`);
}
// 发送添加组件事件给父组件
emit("add-component", {
type: componentTemplate.type,
name: componentTemplate.name,
props: defaultProps,
});
// 关闭菜单
closeMenu();
}
// 添加模板
async function addTemplate(template: any) {
try {
// 加载模板JSON文件
const response = await fetch(template.path);
if (!response.ok) {
throw new Error(`Failed to load template: ${response.statusText}`);
}
const templateData = await response.json();
console.log("加载模板:", templateData);
// 发出事件,将模板数据传递给父组件
emit("add-template", {
id: template.id,
name: template.name,
template: templateData,
capsPage: template.capsPage
});
// 关闭菜单
closeMenu();
} catch (error) {
console.error("加载模板出错:", error);
alert("无法加载模板文件,请检查控制台错误信息");
}
}
// 过滤后的元器件列表 (用于菜单)
const filteredComponents = computed(() => {
if (!searchQuery.value || activeTab.value !== "components") {
return availableComponents;
}
const query = searchQuery.value.toLowerCase();
return availableComponents.filter(
(component) =>
component.name.toLowerCase().includes(query) ||
component.type.toLowerCase().includes(query),
);
});
// 过滤后的模板列表 (用于菜单)
const filteredTemplates = computed(() => {
if (!searchQuery.value || activeTab.value !== "templates") {
return availableTemplates.value;
}
const query = searchQuery.value.toLowerCase();
return availableTemplates.value.filter(
(template) =>
template.name.toLowerCase().includes(query) ||
(template.description &&
template.description.toLowerCase().includes(query)),
);
});
// 过滤后的虚拟外设列表 (用于菜单)
const filteredVirtualDevices = computed(() => {
if (!searchQuery.value || activeTab.value !== "virtual") {
return availableVirtualDevices;
}
const query = searchQuery.value.toLowerCase();
return availableVirtualDevices.filter(
(device) =>
device.name.toLowerCase().includes(query) ||
device.type.toLowerCase().includes(query),
);
});
// 生命周期钩子
onMounted(() => {
// 预加载组件模块
preloadComponentModules();
});
</script>
<style scoped>
/* 组件预览样式 */
.component-preview {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
/* 动画效果 */
.animate-slideUp {
animation: slideUp 0.3s ease-out forwards;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<div class="form-control">
<label class="label" v-if="label || icon">
<component :is="icon" class="w-4 h-4" v-if="icon" />
<span class="label-text" v-if="label">{{ label }}</span>
</label>
<div class="input-group">
<input
:type="type"
:placeholder="placeholder"
:class="inputClasses"
:value="modelValue"
@input="handleInput"
@blur="handleBlur"
v-bind="$attrs"
/>
<slot name="suffix"></slot>
</div>
<label class="label" v-if="error">
<span class="label-text-alt text-error">{{ error }}</span>
</label>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
modelValue?: string | number
label?: string
placeholder?: string
error?: string
type?: 'text' | 'number' | 'email' | 'password'
size?: 'xs' | 'sm' | 'md' | 'lg'
variant?: 'default' | 'bordered' | 'ghost'
icon?: any
disabled?: boolean
readonly?: boolean
}
const props = withDefaults(defineProps<Props>(), {
type: 'text',
size: 'md',
variant: 'bordered'
})
const emit = defineEmits<{
'update:modelValue': [value: string | number]
'blur': [event: FocusEvent]
}>()
defineOptions({
inheritAttrs: false
})
const inputClasses = computed(() => {
const baseClasses = ['input', 'flex-1']
// 添加变体样式
if (props.variant === 'bordered') baseClasses.push('input-bordered')
else if (props.variant === 'ghost') baseClasses.push('input-ghost')
// 添加尺寸样式
if (props.size === 'xs') baseClasses.push('input-xs')
else if (props.size === 'sm') baseClasses.push('input-sm')
else if (props.size === 'lg') baseClasses.push('input-lg')
// 添加错误样式
if (props.error) baseClasses.push('input-error')
// 添加状态样式
if (props.disabled) baseClasses.push('input-disabled')
return baseClasses
})
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement
let value: string | number = target.value
// 如果是数字类型,转换为数字
if (props.type === 'number' && value !== '') {
value = Number(value)
}
emit('update:modelValue', value)
}
const handleBlur = (event: FocusEvent) => {
emit('blur', event)
}
</script>

View File

@@ -0,0 +1,75 @@
<template>
<BaseInputField
v-model="value"
:label="label"
:placeholder="placeholder || '192.168.1.100'"
:error="validationError"
:icon="icon || Globe"
type="text"
v-bind="$attrs"
@blur="validateOnBlur"
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { z } from 'zod'
import { Globe } from 'lucide-vue-next'
import BaseInputField from './BaseInputField.vue'
interface Props {
modelValue: string
label?: string
placeholder?: string
icon?: any
required?: boolean
validateOnBlur?: boolean
}
const props = withDefaults(defineProps<Props>(), {
label: 'IP 地址',
validateOnBlur: true
})
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
defineOptions({
inheritAttrs: false
})
const hasBlurred = ref(false)
const value = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
// IP地址验证模式
const ipSchema = z.string().ip({
version: 'v4',
message: '请输入有效的IPv4地址'
})
const validationError = computed(() => {
// 如果是必填且为空
if (props.required && !props.modelValue) {
return '请输入IP地址'
}
// 如果有值但格式不正确,并且设置了在失焦时验证且已经失焦过
if (props.modelValue && (!props.validateOnBlur || hasBlurred.value)) {
const result = ipSchema.safeParse(props.modelValue)
return result.success ? '' : result.error.errors[0]?.message || '无效的IP地址'
}
return ''
})
const validateOnBlur = () => {
if (props.validateOnBlur) {
hasBlurred.value = true
}
}
</script>

View File

@@ -0,0 +1,81 @@
<template>
<BaseInputField
v-model="value"
:label="label"
:placeholder="placeholder || '8080'"
:error="validationError"
:icon="icon || Network"
type="number"
v-bind="$attrs"
@blur="validateOnBlur"
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { z } from 'zod'
import { Network } from 'lucide-vue-next'
import BaseInputField from './BaseInputField.vue'
interface Props {
modelValue: number
label?: string
placeholder?: string
icon?: any
required?: boolean
validateOnBlur?: boolean
min?: number
max?: number
}
const props = withDefaults(defineProps<Props>(), {
label: '端口',
validateOnBlur: true,
min: 1,
max: 65535
})
const emit = defineEmits<{
'update:modelValue': [value: number]
}>()
defineOptions({
inheritAttrs: false
})
const hasBlurred = ref(false)
const value = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', Number(val))
})
// 端口验证模式
const portSchema = computed(() =>
z.number()
.int('端口必须是整数')
.min(props.min, `端口必须大于等于${props.min}`)
.max(props.max, `端口必须小于等于${props.max}`)
)
const validationError = computed(() => {
// 如果是必填且为空
if (props.required && (!props.modelValue && props.modelValue !== 0)) {
return '请输入端口号'
}
// 如果有值但格式不正确,并且设置了在失焦时验证且已经失焦过
if ((props.modelValue || props.modelValue === 0) && (!props.validateOnBlur || hasBlurred.value)) {
const result = portSchema.value.safeParse(props.modelValue)
return result.success ? '' : result.error.errors[0]?.message || '无效的端口号'
}
return ''
})
const validateOnBlur = () => {
if (props.validateOnBlur) {
hasBlurred.value = true
}
}
</script>

View File

@@ -0,0 +1,3 @@
export { default as BaseInputField } from './BaseInputField.vue'
export { default as IpInputField } from './IpInputField.vue'
export { default as PortInputField } from './PortInputField.vue'

View File

@@ -0,0 +1,314 @@
<template>
<div>
<!-- 元器件选择菜单 (Drawer) -->
<div class="drawer drawer-end z-50">
<input
id="component-drawer"
type="checkbox"
class="drawer-toggle"
v-model="showComponentsMenu"
/>
<div class="drawer-side">
<label
for="component-drawer"
aria-label="close sidebar"
class="drawer-overlay !bg-opacity-50"
></label>
<div
class="menu p-0 w-[460px] min-h-full bg-base-100 shadow-xl flex flex-col"
>
<!-- 菜单头部 -->
<div
class="p-6 border-b bg-base-200 border-base-300 flex justify-between items-center"
>
<h3 class="text-xl font-bold text-primary flex items-center gap-2">
<Plus :size="20" class="text-primary" />
添加元器件
</h3>
<button
class="btn btn-ghost btn-sm btn-circle"
@click="closeMenu"
>
<X :size="20" />
</button>
</div>
<!-- 导航栏 -->
<div class="tabs tabs-boxed bg-base-200 mx-6 mt-4 rounded-box">
<a
class="tab"
:class="{ 'tab-active': activeTab === 'components' }"
@click="activeTab = 'components'"
>元器件</a
>
<a
class="tab"
:class="{ 'tab-active': activeTab === 'templates' }"
@click="activeTab = 'templates'"
>模板</a
>
<a
class="tab"
:class="{ 'tab-active': activeTab === 'virtual' }"
@click="activeTab = 'virtual'"
>虚拟外设</a
>
</div>
<!-- 搜索框 -->
<div class="px-6 py-4 w-full">
<label class="input w-full">
<Search :size="16" class="h-[1em] opacity-50" />
<input
type="text"
placeholder="搜索..."
class="grow"
v-model="searchQuery"
/>
</label>
</div>
<!-- 统一的项目列表 -->
<ItemList
v-if="activeTab === 'components'"
:items="availableComponents"
:search-query="searchQuery"
:component-modules="componentModules"
:no-results-message="'没有找到匹配的元器件'"
item-type="component"
@item-click="addComponent"
@clear-search="searchQuery = ''"
/>
<ItemList
v-if="activeTab === 'templates'"
:items="availableTemplates"
:search-query="searchQuery"
:component-modules="componentModules"
:no-results-message="'没有找到匹配的模板'"
item-type="template"
@item-click="addTemplate"
@clear-search="searchQuery = ''"
/>
<ItemList
v-if="activeTab === 'virtual'"
:items="availableVirtualDevices"
:search-query="searchQuery"
:component-modules="componentModules"
:no-results-message="'没有找到匹配的虚拟外设'"
item-type="virtual"
@item-click="addComponent"
@clear-search="searchQuery = ''"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, shallowRef, onMounted } from "vue";
import { Plus, X, Search } from "lucide-vue-next";
import ItemList from "./ItemList.vue";
import { useAlertStore } from "@/components/Alert";
import {
availableComponents,
availableVirtualDevices,
availableTemplates,
getAllComponentTypes,
type ComponentConfig,
type VirtualDeviceConfig,
type TemplateConfig,
useComponentManager, // 导入 componentManager
} from "./index.ts";
// Props 定义
interface Props {
open: boolean;
}
const props = defineProps<Props>();
// 定义组件发出的事件(保留部分必要的事件)
const emit = defineEmits([
"close",
"update:open",
]);
// 使用 componentManager
const componentManager = useComponentManager();
// 使用 Alert 系统
const alert = useAlertStore();
// 当前激活的选项卡
const activeTab = ref("components");
// --- 搜索功能 ---
const searchQuery = ref("");
// 显示/隐藏组件菜单
const showComponentsMenu = computed({
get: () => props.open,
set: (value) => emit("update:open", value),
});
// 组件模块缓存
const componentModules = shallowRef<Record<string, any>>({});
// 动态加载组件定义
async function loadComponentModule(type: string) {
if (!componentModules.value[type]) {
try {
// 假设组件都在 src/components/equipments/ 目录下,且文件名与 type 相同
const module = await import(`@/components/equipments/${type}.vue`);
// 将模块添加到缓存中
componentModules.value = {
...componentModules.value,
[type]: module,
};
console.log(`Loaded module for ${type}:`, module);
} catch (error) {
console.error(`Failed to load component module ${type}:`, error);
return null;
}
}
return componentModules.value[type];
}
// 预加载组件模块
async function preloadComponentModules() {
const allTypes = getAllComponentTypes();
for (const type of allTypes) {
try {
await loadComponentModule(type);
} catch (error) {
console.error(`Failed to preload component ${type}:`, error);
}
}
}
// 关闭菜单
function closeMenu() {
emit("update:open", false);
emit("close");
}
// 添加新元器件 - 使用 componentManager
async function addComponent(
componentTemplate: ComponentConfig | VirtualDeviceConfig,
) {
if (!componentManager) {
console.error("ComponentManager not available");
return;
}
// 先加载组件模块
const moduleRef = await loadComponentModule(componentTemplate.type);
let defaultProps: Record<string, any> = {};
// 尝试直接调用组件导出的getDefaultProps方法
if (moduleRef) {
if (typeof moduleRef.getDefaultProps === "function") {
defaultProps = moduleRef.getDefaultProps();
console.log(
`Got default props from ${componentTemplate.type}:`,
defaultProps,
);
} else {
// 回退到配置文件
console.log(`No getDefaultProps found for ${componentTemplate.type}`);
}
} else {
console.log(`Failed to load module for ${componentTemplate.type}`);
}
try {
// 使用 componentManager 添加组件
await componentManager.addComponent({
type: componentTemplate.type,
name: componentTemplate.name,
props: defaultProps,
});
// 显示成功消息
alert?.success(`成功添加元器件: ${componentTemplate.name}`);
// 关闭菜单
closeMenu();
} catch (error) {
console.error("添加元器件失败:", error);
alert?.error("添加元器件失败,请检查控制台错误信息");
}
}
// 添加模板 - 使用 componentManager
async function addTemplate(template: TemplateConfig) {
if (!componentManager) {
console.error("ComponentManager not available");
return;
}
try {
// 加载模板JSON文件
const response = await fetch(template.path);
if (!response.ok) {
throw new Error(`Failed to load template: ${response.statusText}`);
}
const templateData = await response.json();
console.log("加载模板:", templateData);
// 使用 componentManager 添加模板
const result = await componentManager.addTemplate({
id: template.id,
name: template.name,
template: templateData,
});
if (result) {
// 使用 Alert 显示结果消息
if (result.success) {
alert?.success(result.message);
} else {
alert?.error(result.message);
}
}
// 关闭菜单
closeMenu();
} catch (error) {
console.error("加载模板出错:", error);
alert?.error("无法加载模板文件,请检查控制台错误信息");
}
}
// 生命周期钩子
onMounted(() => {
// 预加载组件模块
preloadComponentModules();
});
</script>
<style scoped>
/* 动画效果 */
.animate-slideUp {
animation: slideUp 0.3s ease-out forwards;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<div class="px-6 py-4 overflow-auto flex-1">
<div v-if="filteredItems.length > 0" class="grid grid-cols-2 gap-4">
<div
v-for="(item, index) in filteredItems"
:key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="handleItemClick(item)"
>
<div class="card-body p-3 items-center text-center">
<div
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2"
>
<!-- 组件预览 -->
<component
v-if="item.type && componentModules[item.type]"
:is="componentModules[item.type].default"
class="component-preview"
:size="getPreviewSize(item.type)"
/>
<!-- 模板预览 -->
<img
v-else-if="item.thumbnailUrl"
:src="item.thumbnailUrl || '/placeholder-template.png'"
alt="Template thumbnail"
class="max-h-full max-w-full object-contain"
/>
<!-- 加载中状态 -->
<span v-else class="text-xs text-gray-400">加载中...</span>
</div>
<h3 class="card-title text-sm mt-2">{{ item.name }}</h3>
<p class="text-xs opacity-70">
{{ item.description || item.type || getItemSubtitle(item) }}
</p>
</div>
</div>
</div>
<!-- 无搜索结果 -->
<div v-else class="py-16 text-center">
<SearchX :size="48" class="mx-auto text-base-300 mb-3" />
<p class="text-base-content opacity-70">{{ noResultsMessage }}</p>
<button class="btn btn-sm btn-ghost mt-3" @click="$emit('clear-search')">
清除搜索
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { SearchX } from "lucide-vue-next";
import { getPreviewSize } from "./index.ts";
interface Props {
items: any[];
searchQuery: string;
componentModules: Record<string, any>;
noResultsMessage: string;
itemType: "component" | "template" | "virtual";
}
const props = defineProps<Props>();
const emit = defineEmits(["item-click", "clear-search"]);
// 过滤后的项目列表
const filteredItems = computed(() => {
if (!props.searchQuery) {
return props.items;
}
const query = props.searchQuery.toLowerCase();
return props.items.filter(
(item) =>
item.name.toLowerCase().includes(query) ||
(item.type && item.type.toLowerCase().includes(query)) ||
(item.description && item.description.toLowerCase().includes(query)),
);
});
// 获取项目副标题
function getItemSubtitle(item: any): string {
if (props.itemType === "template") {
return "模板";
}
return item.type || "";
}
// 处理项目点击
function handleItemClick(item: any) {
emit("item-click", item);
}
</script>
<style scoped>
.component-preview {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
</style>

View File

@@ -0,0 +1,841 @@
import { ref, shallowRef, computed, reactive } from "vue";
import { createInjectionState } from "@vueuse/core";
import {
type DiagramData,
type DiagramPart,
} from "./diagramManager";
import type { PropertyConfig } from "@/components/equipments/componentConfig";
import {
generatePropertyConfigs,
generatePropsFromDefault,
generatePropsFromAttrs,
} from "@/components/equipments/componentConfig";
// 存储动态导入的组件模块
interface ComponentModule {
default: any;
getDefaultProps?: () => Record<string, any>;
config?: {
props?: Array<PropertyConfig>;
};
__esModule?: boolean; // 添加 __esModule 属性
}
// 定义组件管理器的状态和方法
const [useProvideComponentManager, useComponentManager] = createInjectionState(
() => {
// --- 状态管理 ---
const componentModules = ref<Record<string, ComponentModule>>({});
const selectedComponentId = ref<string | null>(null);
const selectedComponentConfig = shallowRef<{
props: PropertyConfig[];
} | null>(null);
const diagramCanvas = ref<any>(null);
const componentRefs = ref<Record<string, any>>({});
// 新增直接管理canvas的位置和缩放
const canvasPosition = reactive({ x: 0, y: 0 });
const canvasScale = ref(1);
// 计算当前选中的组件数据
const selectedComponentData = computed(() => {
if (!diagramCanvas.value || !selectedComponentId.value) return null;
const canvasInstance = diagramCanvas.value as any;
if (canvasInstance && canvasInstance.getDiagramData) {
const data = canvasInstance.getDiagramData();
return (
data.parts.find(
(p: DiagramPart) => p.id === selectedComponentId.value,
) || null
);
}
return null;
});
// --- Canvas 控制方法 ---
/**
* 设置canvas位置
*/
function setCanvasPosition(x: number, y: number) {
canvasPosition.x = x;
canvasPosition.y = y;
}
/**
* 更新canvas位置相对偏移
*/
function updateCanvasPosition(deltaX: number, deltaY: number) {
canvasPosition.x += deltaX;
canvasPosition.y += deltaY;
}
/**
* 设置canvas缩放
*/
function setCanvasScale(scale: number) {
canvasScale.value = Math.max(0.2, Math.min(scale, 10.0));
}
/**
* 获取canvas位置
*/
function getCanvasPosition() {
return { x: canvasPosition.x, y: canvasPosition.y };
}
/**
* 获取canvas缩放
*/
function getCanvasScale() {
return canvasScale.value;
}
/**
* 缩放到指定位置(以鼠标位置为中心)
*/
function zoomAtPosition(
mouseX: number,
mouseY: number,
zoomFactor: number,
) {
// 计算鼠标在画布坐标系中的位置
const mouseXCanvas = (mouseX - canvasPosition.x) / canvasScale.value;
const mouseYCanvas = (mouseY - canvasPosition.y) / canvasScale.value;
// 计算新的缩放值
const newScale = Math.max(
0.2,
Math.min(canvasScale.value * zoomFactor, 10.0),
);
// 计算新的位置,使鼠标指针位置在缩放前后保持不变
canvasPosition.x = mouseX - mouseXCanvas * newScale;
canvasPosition.y = mouseY - mouseYCanvas * newScale;
canvasScale.value = newScale;
return {
scale: newScale,
position: { x: canvasPosition.x, y: canvasPosition.y },
};
}
/**
* 将屏幕坐标转换为画布坐标
*/
function screenToCanvas(screenX: number, screenY: number) {
return {
x: (screenX - canvasPosition.x) / canvasScale.value,
y: (screenY - canvasPosition.y) / canvasScale.value,
};
}
/**
* 将画布坐标转换为屏幕坐标
*/
function canvasToScreen(canvasX: number, canvasY: number) {
return {
x: canvasX * canvasScale.value + canvasPosition.x,
y: canvasY * canvasScale.value + canvasPosition.y,
};
}
/**
* 居中显示指定区域
*/
function centerView(
bounds: { x: number; y: number; width: number; height: number },
containerWidth: number,
containerHeight: number,
) {
// 计算合适的缩放比例
const scaleX = containerWidth / bounds.width;
const scaleY = containerHeight / bounds.height;
const newScale = Math.min(scaleX, scaleY, 1) * 0.8; // 留一些边距
// 计算居中位置
const centerX = bounds.x + bounds.width / 2;
const centerY = bounds.y + bounds.height / 2;
canvasScale.value = newScale;
canvasPosition.x = containerWidth / 2 - centerX * newScale;
canvasPosition.y = containerHeight / 2 - centerY * newScale;
return {
scale: newScale,
position: { x: canvasPosition.x, y: canvasPosition.y },
};
}
// --- 组件模块管理 ---
/**
* 动态加载组件模块
*/
async function loadComponentModule(type: string) {
console.log(`尝试加载组件模块: ${type}`);
console.log(`当前已加载的模块:`, Object.keys(componentModules.value));
if (!componentModules.value[type]) {
try {
console.log(`正在动态导入模块: @/components/equipments/${type}.vue`);
const module = await import(`@/components/equipments/${type}.vue`);
console.log(`成功导入模块 ${type}:`, module);
// 直接设置新的对象引用以触发响应性
componentModules.value = {
...componentModules.value,
[type]: module,
};
console.log(`模块 ${type} 已添加到 componentModules`);
console.log(`更新后的模块列表:`, Object.keys(componentModules.value));
} catch (error) {
console.error(`Failed to load component module ${type}:`, error);
return null;
}
} else {
console.log(`模块 ${type} 已经存在`);
}
return componentModules.value[type];
}
/**
* 预加载所有组件模块
*/
async function preloadComponentModules(componentTypes: string[]) {
console.log("Preloading component modules:", componentTypes);
await Promise.all(
componentTypes.map((type) => loadComponentModule(type)),
);
console.log("All component modules loaded");
}
// --- 组件操作 ---
/**
* 添加新组件到画布
*/
async function addComponent(componentData: {
type: string;
name: string;
props: Record<string, any>;
}) {
console.log("=== 开始添加组件 ===");
console.log("组件数据:", componentData);
const canvasInstance = diagramCanvas.value as any;
if (!canvasInstance) {
console.error("没有可用的画布实例");
return;
}
// 预加载组件模块,确保组件能正常渲染
console.log(`预加载组件模块: ${componentData.type}`);
const componentModule = await loadComponentModule(componentData.type);
if (!componentModule) {
console.error(`无法加载组件模块: ${componentData.type}`);
return;
}
console.log(`组件模块加载成功: ${componentData.type}`, componentModule);
// 使用内部管理的位置和缩放信息
let position = { x: 100, y: 100 };
try {
const canvasContainer = canvasInstance.$el as HTMLElement;
if (canvasContainer) {
const viewportWidth = canvasContainer.clientWidth;
const viewportHeight = canvasContainer.clientHeight;
position.x =
(viewportWidth / 2 - canvasPosition.x) / canvasScale.value;
position.y =
(viewportHeight / 2 - canvasPosition.y) / canvasScale.value;
}
} catch (error) {
console.error("获取画布位置时出错:", error);
}
// 添加随机偏移
const offsetX = Math.floor(Math.random() * 100) - 50;
const offsetY = Math.floor(Math.random() * 100) - 50;
// 获取组件能力页面
let capsPage = null;
if (
componentModule &&
componentModule.default &&
typeof componentModule.default.getCapabilities === "function"
) {
try {
capsPage = componentModule.default.getCapabilities();
console.log(`获取到${componentData.type}组件的能力页面`);
} catch (error) {
console.error(`获取${componentData.type}组件能力页面失败:`, error);
}
}
// 创建新组件
const newComponent: DiagramPart = {
id: `component-${Date.now()}`,
type: componentData.type,
x: Math.round(position.x + offsetX),
y: Math.round(position.y + offsetY),
attrs: componentData.props,
rotate: 0,
group: "",
positionlock: false,
hidepins: true,
isOn: true,
index: 0,
};
// 通过画布实例添加组件
if (
canvasInstance.getDiagramData &&
canvasInstance.updateDiagramDataDirectly
) {
const currentData = canvasInstance.getDiagramData();
currentData.parts.push(newComponent);
// 使用 updateDiagramDataDirectly 避免触发加载状态
canvasInstance.updateDiagramDataDirectly(currentData);
// 移除自动保存功能
console.log("组件添加完成:", newComponent);
// 等待Vue的下一个tick确保组件模块已经更新
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
/**
* 添加模板到画布
*/
async function addTemplate(templateData: {
id: string;
name: string;
template: any;
}) {
console.log("添加模板:", templateData);
const canvasInstance = diagramCanvas.value as any;
if (
!canvasInstance?.getDiagramData ||
!canvasInstance?.updateDiagramDataDirectly
) {
console.error("没有可用的画布实例添加模板");
return;
}
const currentData = canvasInstance.getDiagramData();
console.log("=== 当前图表组件数量:", currentData.parts.length);
// 生成唯一ID前缀
const idPrefix = `template-${Date.now()}-`;
if (templateData.template?.parts) {
// 使用内部管理的位置和缩放信息获取视口中心位置
let viewportCenter = { x: 300, y: 200 };
try {
const canvasContainer = canvasInstance.$el as HTMLElement;
if (canvasContainer) {
const viewportWidth = canvasContainer.clientWidth;
const viewportHeight = canvasContainer.clientHeight;
viewportCenter.x =
(viewportWidth / 2 - canvasPosition.x) / canvasScale.value;
viewportCenter.y =
(viewportHeight / 2 - canvasPosition.y) / canvasScale.value;
}
} catch (error) {
console.error("获取视口中心位置时出错:", error);
}
const mainPart = templateData.template.parts[0];
// 创建新组件
const newParts = await Promise.all(
templateData.template.parts.map(async (part: any) => {
const newPart = JSON.parse(JSON.stringify(part));
newPart.id = `${idPrefix}${part.id}`;
// 加载组件模块并获取能力页面
try {
const componentModule = await loadComponentModule(part.type);
if (
componentModule?.default &&
typeof componentModule.default.getCapabilities === "function"
) {
newPart.capsPage = componentModule.default.getCapabilities();
console.log(`加载模板组件${part.type}组件的能力页面成功`);
}
} catch (error) {
console.error(`加载模板组件${part.type}的能力页面失败:`, error);
}
// 计算新位置
if (
typeof newPart.x === "number" &&
typeof newPart.y === "number"
) {
const relativeX = part.x - mainPart.x;
const relativeY = part.y - mainPart.y;
newPart.x = viewportCenter.x + relativeX;
newPart.y = viewportCenter.y + relativeY;
}
return newPart;
}),
);
currentData.parts.push(...newParts);
// 处理连接关系
if (templateData.template.connections) {
const idMap: Record<string, string> = {};
templateData.template.parts.forEach((part: any) => {
idMap[part.id] = `${idPrefix}${part.id}`;
});
const newConnections = templateData.template.connections.map(
(conn: any) => {
if (Array.isArray(conn)) {
const [from, to, type, path] = conn;
const fromParts = from.split(":");
const toParts = to.split(":");
if (fromParts.length === 2 && toParts.length === 2) {
const fromComponentId = fromParts[0];
const fromPinId = fromParts[1];
const toComponentId = toParts[0];
const toPinId = toParts[1];
const newFrom = `${idMap[fromComponentId] || fromComponentId}:${fromPinId}`;
const newTo = `${idMap[toComponentId] || toComponentId}:${toPinId}`;
return [newFrom, newTo, type, path];
}
}
return conn;
},
);
currentData.connections.push(...newConnections);
}
canvasInstance.updateDiagramDataDirectly(currentData);
console.log(
"=== 更新图表数据完成,新组件数量:",
currentData.parts.length,
);
// 移除自动保存功能
return { success: true, message: `已添加 ${templateData.name} 模板` };
} else {
console.error("模板格式错误缺少parts数组");
return { success: false, message: "模板格式错误" };
}
}
/**
* 删除组件
*/
function deleteComponent(componentId: string) {
const canvasInstance = diagramCanvas.value as any;
if (
!canvasInstance?.getDiagramData ||
!canvasInstance?.updateDiagramDataDirectly
) {
return;
}
const currentData = canvasInstance.getDiagramData();
const component = currentData.parts.find(
(p: DiagramPart) => p.id === componentId,
);
if (!component) return;
const componentsToDelete: string[] = [componentId];
// 处理组件组
if (component.group && component.group !== "") {
const groupMembers = currentData.parts.filter(
(p: DiagramPart) =>
p.group === component.group && p.id !== componentId,
);
componentsToDelete.push(...groupMembers.map((p: DiagramPart) => p.id));
console.log(
`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`,
);
}
// 删除组件
currentData.parts = currentData.parts.filter(
(p: DiagramPart) => !componentsToDelete.includes(p.id),
);
// 删除相关连接
currentData.connections = currentData.connections.filter(
(connection: any) => {
for (const id of componentsToDelete) {
if (
connection[0].startsWith(`${id}:`) ||
connection[1].startsWith(`${id}:`)
) {
return false;
}
}
return true;
},
);
// 清除选中状态
if (
selectedComponentId.value &&
componentsToDelete.includes(selectedComponentId.value)
) {
selectedComponentId.value = null;
selectedComponentConfig.value = null;
}
canvasInstance.updateDiagramDataDirectly(currentData);
// 移除自动保存功能
}
/**
* 选中组件
*/
async function selectComponent(componentData: DiagramPart | null) {
selectedComponentId.value = componentData ? componentData.id : null;
selectedComponentConfig.value = null;
if (componentData) {
const moduleRef = await loadComponentModule(componentData.type);
if (moduleRef) {
try {
const propConfigs: PropertyConfig[] = [];
const addedProps = new Set<string>();
// 从 getDefaultProps 方法获取默认配置
if (typeof moduleRef.getDefaultProps === "function") {
const defaultProps = moduleRef.getDefaultProps();
const defaultPropConfigs = generatePropsFromDefault(defaultProps);
defaultPropConfigs.forEach((config) => {
propConfigs.push(config);
addedProps.add(config.name);
});
}
// 添加组件直接属性
const directPropConfigs = generatePropertyConfigs(componentData);
const newDirectProps = directPropConfigs.filter(
(config) => !addedProps.has(config.name),
);
propConfigs.push(...newDirectProps);
// 添加 attrs 中的属性
if (componentData.attrs) {
const attrPropConfigs = generatePropsFromAttrs(
componentData.attrs,
);
attrPropConfigs.forEach((attrConfig) => {
const existingIndex = propConfigs.findIndex(
(p) => p.name === attrConfig.name,
);
if (existingIndex >= 0) {
propConfigs[existingIndex] = attrConfig;
} else {
propConfigs.push(attrConfig);
}
});
}
selectedComponentConfig.value = { props: propConfigs };
console.log(
`Built config for ${componentData.type}:`,
selectedComponentConfig.value,
);
} catch (error) {
console.error(
`Error building config for ${componentData.type}:`,
error,
);
selectedComponentConfig.value = { props: [] };
}
} else {
console.warn(`Module for component ${componentData.type} not found.`);
selectedComponentConfig.value = { props: [] };
}
}
}
/**
* 更新组件属性
*/
function updateComponentProp(
componentId: string,
propName: string,
value: any,
) {
const canvasInstance = diagramCanvas.value as any;
if (
!canvasInstance?.getDiagramData ||
!canvasInstance?.updateDiagramDataDirectly
) {
console.error("没有可用的画布实例进行属性更新");
return;
}
// 检查值格式
if (value !== null && typeof value === "object" && "value" in value) {
value = value.value;
}
const currentData = canvasInstance.getDiagramData();
const part = currentData.parts.find(
(p: DiagramPart) => p.id === componentId,
);
if (part) {
if (propName in part) {
(part as any)[propName] = value;
} else {
if (!part.attrs) {
part.attrs = {};
}
part.attrs[propName] = value;
}
canvasInstance.updateDiagramDataDirectly(currentData);
console.log(
`更新组件${componentId}的属性${propName}为:`,
value,
typeof value,
);
}
}
/**
* 更新组件直接属性
*/
function updateComponentDirectProp(
componentId: string,
propName: string,
value: any,
) {
const canvasInstance = diagramCanvas.value as any;
if (
!canvasInstance?.getDiagramData ||
!canvasInstance?.updateDiagramDataDirectly
) {
console.error("没有可用的画布实例进行属性更新");
return;
}
const currentData = canvasInstance.getDiagramData();
const part = currentData.parts.find(
(p: DiagramPart) => p.id === componentId,
);
if (part) {
(part as any)[propName] = value;
canvasInstance.updateDiagramDataDirectly(currentData);
console.log(
`更新组件${componentId}的直接属性${propName}为:`,
value,
typeof value,
);
}
}
/**
* 移动组件
*/
function moveComponent(moveData: { id: string; x: number; y: number }) {
const canvasInstance = diagramCanvas.value as any;
if (
!canvasInstance?.getDiagramData ||
!canvasInstance?.updateDiagramDataDirectly
) {
return;
}
const currentData = canvasInstance.getDiagramData();
const part = currentData.parts.find(
(p: DiagramPart) => p.id === moveData.id,
);
if (part) {
part.x = moveData.x;
part.y = moveData.y;
canvasInstance.updateDiagramDataDirectly(currentData);
}
}
/**
* 设置画布实例引用
*/
function setCanvasRef(canvasRef: any) {
diagramCanvas.value = canvasRef;
}
/**
* 设置组件DOM引用
*/
function setComponentRef(componentId: string, el: any) {
if (el) {
componentRefs.value[componentId] = el;
} else {
delete componentRefs.value[componentId];
}
}
/**
* 获取组件DOM引用
*/
function getComponentRef(componentId: string) {
return componentRefs.value[componentId];
}
/**
* 获取当前图表数据
*/
function getDiagramData() {
const canvasInstance = diagramCanvas.value;
if (canvasInstance && canvasInstance.getDiagramData) {
return canvasInstance.getDiagramData();
}
return {
parts: [],
connections: [],
version: 1,
author: "admin",
editor: "me",
};
}
/**
* 更新图表数据
*/
function updateDiagramData(data: any) {
const canvasInstance = diagramCanvas.value;
if (canvasInstance && canvasInstance.updateDiagramDataDirectly) {
canvasInstance.updateDiagramDataDirectly(data);
}
}
/**
* 获取组件定义
*/
function getComponentDefinition(type: string) {
const module = componentModules.value[type];
if (!module) {
console.warn(`No module found for component type: ${type}`);
// 尝试异步加载组件模块
loadComponentModule(type);
return null;
}
// 确保我们返回一个有效的组件定义
if (module.default) {
return module.default;
} else if (module.__esModule && module.default) {
// 有时 Vue 的动态导入会将默认导出包装在 __esModule 属性下
return module.default;
} else {
console.warn(
`Module for ${type} found but default export is missing`,
module,
);
return null;
}
}
/**
* 准备组件属性
*/
function prepareComponentProps(
attrs: Record<string, any>,
componentId?: string,
examId?: string,
): Record<string, any> {
const result: Record<string, any> = { ...attrs };
if (componentId) {
result.componentId = componentId;
}
if (examId) {
result.examId = examId;
}
return result;
}
/**
* 初始化组件管理器
*/
async function initialize() {
const canvasInstance = diagramCanvas.value as any;
if (canvasInstance?.getDiagramData) {
const diagramData = canvasInstance.getDiagramData();
// 收集所有组件类型
const componentTypes = new Set<string>();
diagramData.parts.forEach((part: DiagramPart) => {
componentTypes.add(part.type);
});
// 预加载组件模块
await preloadComponentModules(Array.from(componentTypes));
}
}
return {
// 状态
componentModules,
selectedComponentId,
selectedComponentData,
selectedComponentConfig,
componentRefs,
// Canvas控制状态
canvasPosition,
canvasScale,
// 方法
loadComponentModule,
preloadComponentModules,
addComponent,
addTemplate,
deleteComponent,
selectComponent,
updateComponentProp,
updateComponentDirectProp,
moveComponent,
setCanvasRef,
setComponentRef,
getComponentRef,
getDiagramData,
updateDiagramData,
getComponentDefinition,
prepareComponentProps,
initialize,
// Canvas控制方法
setCanvasPosition,
updateCanvasPosition,
setCanvasScale,
getCanvasPosition,
getCanvasScale,
zoomAtPosition,
screenToCanvas,
canvasToScreen,
centerView,
};
},
);
export { useProvideComponentManager, useComponentManager };

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