38 Commits

Author SHA1 Message Date
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
67 changed files with 12443 additions and 4305 deletions

1
.gitignore vendored
View File

@@ -28,7 +28,6 @@ DebuggerCmd.md
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln
*.sw? *.sw?
*.tsbuildinfo *.tsbuildinfo

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

View File

@@ -39,10 +39,10 @@
typescript-language-server typescript-language-server
]; ];
shellHook = '' shellHook = ''
export PATH=$PATH: export PATH=$PATH:/home/sikongjueluo/.dotnet/tools
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${pkgs.zlib}/lib export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${pkgs.zlib}/lib
export DOTNET_ROOT=${pkgs.dotnetCorePackages.sdk_9_0}/share/dotnet
''; '';
}; };
}); });
}; };

495
package-lock.json generated
View File

@@ -8,11 +8,14 @@
"name": "fpga-weblab", "name": "fpga-weblab",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@microsoft/signalr": "^9.0.6",
"@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.js": "^3.2.4",
"@tanstack/vue-table": "^8.21.3", "@tanstack/vue-table": "^8.21.3",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/signalr": "^2.4.3",
"@vueuse/core": "^13.5.0", "@vueuse/core": "^13.5.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"axios": "^1.11.0",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"konva": "^9.3.20", "konva": "^9.3.20",
@@ -1127,6 +1130,39 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@microsoft/signalr": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-9.0.6.tgz",
"integrity": "sha512-DrhgzFWI9JE4RPTsHYRxh4yr+OhnwKz8bnJe7eIi7mLLjqhJpEb62CiUy/YbFvLqLzcGzlzz1QWgVAW0zyipMQ==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"eventsource": "^2.0.2",
"fetch-cookie": "^2.0.3",
"node-fetch": "^2.6.7",
"ws": "^7.5.10"
}
},
"node_modules/@microsoft/signalr/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/@polka/url": { "node_modules/@polka/url": {
"version": "1.0.0-next.29", "version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
@@ -1844,6 +1880,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/jquery": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz",
"integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==",
"license": "MIT",
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.17.16", "version": "4.17.16",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
@@ -1860,6 +1905,21 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/signalr": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@types/signalr/-/signalr-2.4.3.tgz",
"integrity": "sha512-W6C1wMRIIhJV9nsw19yhw4h9zlkLnJzsu9dYlH35aHUQblPsDF6UpCcAVu4Ljy4RS3c3uJyV88wf2M2SOWqqZg==",
"license": "MIT",
"dependencies": {
"@types/jquery": "*"
}
},
"node_modules/@types/sizzle": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz",
"integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==",
"license": "MIT"
},
"node_modules/@types/web-bluetooth": { "node_modules/@types/web-bluetooth": {
"version": "0.0.21", "version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
@@ -2256,6 +2316,18 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2357,6 +2429,12 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.21", "version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@@ -2395,6 +2473,17 @@
"postcss": "^8.1.0" "postcss": "^8.1.0"
} }
}, },
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2496,6 +2585,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001715", "version": "1.0.30001715",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
@@ -2542,6 +2644,18 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/complex.js": { "node_modules/complex.js": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz", "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz",
@@ -2735,6 +2849,15 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
@@ -2755,6 +2878,20 @@
"node": ">=0.3.1" "node": ">=0.3.1"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/echarts": { "node_modules/echarts": {
"version": "5.6.0", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
@@ -2814,6 +2951,51 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.2", "version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
@@ -2877,6 +3059,24 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/eventsource": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/execa": { "node_modules/execa": {
"version": "9.5.2", "version": "9.5.2",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz",
@@ -2950,6 +3150,16 @@
"node": "^12.20 || >= 14.13" "node": "^12.20 || >= 14.13"
} }
}, },
"node_modules/fetch-cookie": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz",
"integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==",
"license": "Unlicense",
"dependencies": {
"set-cookie-parser": "^2.4.8",
"tough-cookie": "^4.0.0"
}
},
"node_modules/figures": { "node_modules/figures": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
@@ -2979,6 +3189,42 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": { "node_modules/formdata-polyfill": {
"version": "4.0.10", "version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -3036,6 +3282,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -3046,6 +3301,43 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "9.0.1", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
@@ -3099,6 +3391,18 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -3106,6 +3410,45 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/he": { "node_modules/he": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -3723,6 +4066,15 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mathjs": { "node_modules/mathjs": {
"version": "14.4.0", "version": "14.4.0",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.4.0.tgz", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.4.0.tgz",
@@ -3768,6 +4120,27 @@
"node": ">= 0.10.0" "node": ">= 0.10.0"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -4195,6 +4568,33 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"funding": {
"url": "https://github.com/sponsors/lupomontero"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/quansync": { "node_modules/quansync": {
"version": "0.2.10", "version": "0.2.10",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
@@ -4212,6 +4612,12 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/read-package-json-fast": { "node_modules/read-package-json-fast": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz",
@@ -4297,6 +4703,12 @@
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/resolve-pkg-maps": { "node_modules/resolve-pkg-maps": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@@ -4382,6 +4794,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4552,6 +4970,36 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tough-cookie/node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-log": { "node_modules/ts-log": {
"version": "2.2.7", "version": "2.2.7",
"resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.7.tgz", "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.7.tgz",
@@ -4793,6 +5241,16 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/v8-compile-cache-lib": { "node_modules/v8-compile-cache-lib": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@@ -5112,6 +5570,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/webpack-virtual-modules": { "node_modules/webpack-virtual-modules": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
@@ -5119,6 +5583,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
@@ -5135,6 +5609,27 @@
"node": "^18.17.0 || >=20.5.0" "node": "^18.17.0 || >=20.5.0"
} }
}, },
"node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -12,11 +12,14 @@
"gen-api": "npx tsx scripts/GenerateWebAPI.ts" "gen-api": "npx tsx scripts/GenerateWebAPI.ts"
}, },
"dependencies": { "dependencies": {
"@microsoft/signalr": "^9.0.6",
"@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.js": "^3.2.4",
"@tanstack/vue-table": "^8.21.3", "@tanstack/vue-table": "^8.21.3",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"@types/signalr": "^2.4.3",
"@vueuse/core": "^13.5.0", "@vueuse/core": "^13.5.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"axios": "^1.11.0",
"echarts": "^5.6.0", "echarts": "^5.6.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"konva": "^9.3.20", "konva": "^9.3.20",

View File

@@ -1,36 +1,42 @@
import { spawn, exec, ChildProcess } from 'child_process'; import { spawn, exec, ChildProcess } from "child_process";
import { promisify } from 'util'; import { promisify } from "util";
import fetch from 'node-fetch'; import fetch from "node-fetch";
import * as fs from 'fs'; import * as fs from "fs";
const execAsync = promisify(exec); const execAsync = promisify(exec);
// Windows 支持函数 // Windows 支持函数
function getCommand(command: string): string { function getCommand(command: string): string {
// dotnet 在 Windows 上不需要 .cmd 后缀 // dotnet 在 Windows 上不需要 .cmd 后缀
if (command === 'dotnet') { if (command === "dotnet") {
return 'dotnet'; return "dotnet";
} }
return process.platform === 'win32' ? `${command}.cmd` : command; return process.platform === "win32" ? `${command}.cmd` : command;
} }
function getSpawnOptions() { function getSpawnOptions() {
return process.platform === 'win32' ? { stdio: 'pipe', shell: true } : { stdio: 'pipe' }; return process.platform === "win32"
? { stdio: "pipe", shell: true }
: { stdio: "pipe" };
} }
async function waitForServer(url: string, maxRetries: number = 30, interval: number = 1000): Promise<boolean> { async function waitForServer(
url: string,
maxRetries: number = 30,
interval: number = 1000,
): Promise<boolean> {
for (let i = 0; i < maxRetries; i++) { for (let i = 0; i < maxRetries; i++) {
try { try {
const response = await fetch(url); const response = await fetch(url);
if (response.ok) { if (response.ok) {
console.log('✓ Server is ready'); console.log("✓ Server is ready");
return true; return true;
} }
} catch (error) { } catch (error) {
// Server not ready yet // Server not ready yet
} }
console.log(`Waiting for server... (${i + 1}/${maxRetries})`); console.log(`Waiting for server... (${i + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, interval)); await new Promise((resolve) => setTimeout(resolve, interval));
} }
return false; return false;
} }
@@ -40,32 +46,39 @@ let serverProcess: ChildProcess | null = null;
let webProcess: ChildProcess | null = null; let webProcess: ChildProcess | null = null;
async function startWeb(): Promise<ChildProcess> { async function startWeb(): Promise<ChildProcess> {
console.log('Starting Vite frontend...'); console.log("Starting Vite frontend...");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const process = spawn(getCommand('npm'), ['run', 'dev'], getSpawnOptions() as any); const process = spawn(
getCommand("npm"),
["run", "dev"],
getSpawnOptions() as any,
);
let webStarted = false; let webStarted = false;
process.stdout?.on('data', (data) => { process.stdout?.on("data", (data) => {
const output = data.toString(); const output = data.toString();
console.log(`Web: ${output}`); console.log(`Web: ${output}`);
// 检查 Vite 是否已启动 // 检查 Vite 是否已启动
if ((output.includes('Local:') || output.includes('ready in')) && !webStarted) { if (
(output.includes("Local:") || output.includes("ready in")) &&
!webStarted
) {
webStarted = true; webStarted = true;
resolve(process); resolve(process);
} }
}); });
process.stderr?.on('data', (data) => { process.stderr?.on("data", (data) => {
console.error(`Web Error: ${data}`); console.error(`Web Error: ${data}`);
}); });
process.on('error', (error) => { process.on("error", (error) => {
reject(error); reject(error);
}); });
process.on('exit', (code, signal) => { process.on("exit", (code, signal) => {
console.log(`Web process exited with code ${code} and signal ${signal}`); console.log(`Web process exited with code ${code} and signal ${signal}`);
if (!webStarted) { if (!webStarted) {
reject(new Error(`Web process exited unexpectedly with code ${code}`)); reject(new Error(`Web process exited unexpectedly with code ${code}`));
@@ -78,45 +91,53 @@ async function startWeb(): Promise<ChildProcess> {
// 超时处理 // 超时处理
setTimeout(() => { setTimeout(() => {
if (!webStarted) { if (!webStarted) {
reject(new Error('Web server failed to start within timeout')); reject(new Error("Web server failed to start within timeout"));
} }
}, 30000); // 30秒超时 }, 10000); // 10秒超时
}); });
} }
async function startServer(): Promise<ChildProcess> { async function startServer(): Promise<ChildProcess> {
console.log('Starting .NET server...'); console.log("Starting .NET server...");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const process = spawn(getCommand('dotnet'), ['run', '--property:Configuration=Release'], { const process = spawn(
cwd: 'server', getCommand("dotnet"),
...getSpawnOptions() ["run", "--property:Configuration=Release"],
} as any); {
cwd: "server",
...getSpawnOptions(),
} as any,
);
let serverStarted = false; let serverStarted = false;
process.stdout?.on('data', (data) => { process.stdout?.on("data", (data) => {
const output = data.toString(); const output = data.toString();
console.log(`Server: ${output}`); console.log(`Server: ${output}`);
// 检查服务器是否已启动 // 检查服务器是否已启动
if (output.includes('Now listening on:') && !serverStarted) { if (output.includes("Now listening on:") && !serverStarted) {
serverStarted = true; serverStarted = true;
resolve(process); resolve(process);
} }
}); });
process.stderr?.on('data', (data) => { process.stderr?.on("data", (data) => {
console.error(`Server Error: ${data}`); console.error(`Server Error: ${data}`);
}); });
process.on('error', (error) => { process.on("error", (error) => {
reject(error); reject(error);
}); });
process.on('exit', (code, signal) => { process.on("exit", (code, signal) => {
console.log(`Server process exited with code ${code} and signal ${signal}`); console.log(
`Server process exited with code ${code} and signal ${signal}`,
);
if (!serverStarted) { if (!serverStarted) {
reject(new Error(`Server process exited unexpectedly with code ${code}`)); reject(
new Error(`Server process exited unexpectedly with code ${code}`),
);
} }
}); });
@@ -126,62 +147,53 @@ async function startServer(): Promise<ChildProcess> {
// 超时处理 // 超时处理
setTimeout(() => { setTimeout(() => {
if (!serverStarted) { if (!serverStarted) {
reject(new Error('Server failed to start within timeout')); reject(new Error("Server failed to start within timeout"));
} }
}, 30000); // 30秒超时 }, 10000); // 10秒超时
}); });
} }
async function stopServer(): Promise<void> { async function stopServer(): Promise<void> {
console.log('Stopping server...'); console.log("Stopping server...");
if (!serverProcess) { if (!serverProcess) {
console.log('No server process to stop'); console.log("No server process to stop");
return; return;
} }
try { try {
// 检查进程是否还存在 // 检查进程是否还存在
if (serverProcess.killed || serverProcess.exitCode !== null) { if (serverProcess.killed || serverProcess.exitCode !== null) {
console.log('✓ Server process already terminated'); console.log("✓ Server process already terminated");
serverProcess = null; serverProcess = null;
return; return;
} }
// 发送 SIGTERM 信号 // 发送 SIGTERM 信号
const killed = serverProcess.kill('SIGTERM'); const killed = serverProcess.kill("SIGTERM");
if (!killed) { if (!killed) {
console.warn('Failed to send SIGTERM to server process'); console.warn("Failed to send SIGTERM to server process");
return; return;
} }
// 等待进程优雅退出
const exitPromise = new Promise<void>((resolve) => {
if (serverProcess) {
serverProcess.on('exit', () => {
console.log('✓ Server stopped gracefully');
resolve();
});
} else {
resolve();
}
});
// 设置超时,如果 3 秒内没有退出则强制终止 // 设置超时,如果 3 秒内没有退出则强制终止
const timeoutPromise = new Promise<void>((resolve) => { const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => { setTimeout(() => {
if (serverProcess && !serverProcess.killed && serverProcess.exitCode === null) { if (
console.log('Force killing server process...'); serverProcess &&
serverProcess.kill('SIGKILL'); !serverProcess.killed &&
serverProcess.exitCode === null
) {
console.log("Force killing server process...");
serverProcess.kill("SIGKILL");
} }
resolve(); resolve();
}, 3000); // 减少超时时间到3秒 }, 3000); // 减少超时时间到3秒
}); });
await Promise.race([exitPromise, timeoutPromise]); await Promise.race([timeoutPromise]);
} catch (error) { } catch (error) {
console.warn('Warning: Could not stop server process:', error); console.warn("Warning: Could not stop server process:", error);
} finally { } finally {
serverProcess = null; serverProcess = null;
@@ -191,67 +203,51 @@ async function stopServer(): Promise<void> {
} }
async function stopWeb(): Promise<void> { async function stopWeb(): Promise<void> {
console.log('Stopping web server...'); console.log("Stopping web server...");
if (!webProcess) { if (!webProcess) {
console.log('No web process to stop'); console.log("No web process to stop");
return; return;
} }
try { try {
// 检查进程是否还存在 // 检查进程是否还存在
if (webProcess.killed || webProcess.exitCode !== null) { if (webProcess.killed || webProcess.exitCode !== null) {
console.log('✓ Web process already terminated'); console.log("✓ Web process already terminated");
webProcess = null; webProcess = null;
return; return;
} }
// 发送 SIGTERM 信号 // 发送 SIGTERM 信号
const killed = webProcess.kill('SIGTERM'); const killed = webProcess.kill("SIGTERM");
if (!killed) { if (!killed) {
console.warn('Failed to send SIGTERM to web process'); console.warn("Failed to send SIGTERM to web process");
return; return;
} }
// 等待进程优雅退出
const exitPromise = new Promise<void>((resolve) => {
if (webProcess) {
webProcess.on('exit', () => {
console.log('✓ Web server stopped gracefully');
resolve();
});
} else {
resolve();
}
});
// 设置超时,如果 3 秒内没有退出则强制终止 // 设置超时,如果 3 秒内没有退出则强制终止
const timeoutPromise = new Promise<void>((resolve) => { const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => { setTimeout(() => {
if (webProcess && !webProcess.killed && webProcess.exitCode === null) { if (webProcess && !webProcess.killed && webProcess.exitCode === null) {
console.log('Force killing web process...'); console.log("Force killing web process...");
webProcess.kill('SIGKILL'); webProcess.kill("SIGKILL");
} }
resolve(); resolve();
}, 3000); // 减少超时时间到3秒 }, 3000);
}); });
await Promise.race([exitPromise, timeoutPromise]); await Promise.race([timeoutPromise]);
} catch (error) { } catch (error) {
console.warn('Warning: Could not stop web process:', error); console.warn("Warning: Could not stop web process:", error);
} finally { } finally {
webProcess = null; webProcess = null;
// 只有在进程可能没有正常退出时才执行清理
// 移除自动清理逻辑,因为正常退出时不需要
} }
} }
async function postProcessApiClient(): Promise<void> { async function postProcessApiClient(): Promise<void> {
console.log('Post-processing API client...'); console.log("Post-processing API client...");
try { try {
const filePath = 'src/APIClient.ts'; const filePath = "src/APIClient.ts";
// 检查文件是否存在 // 检查文件是否存在
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
@@ -259,29 +255,43 @@ async function postProcessApiClient(): Promise<void> {
} }
// 读取文件内容 // 读取文件内容
let content = fs.readFileSync(filePath, 'utf8'); let content = fs.readFileSync(filePath, "utf8");
// 替换 ArgumentException 中的 message 属性声明 // 替换 ArgumentException 中的 message 属性声明
content = content.replace( content = content.replace(
/(\s+)message!:\s*string;/g, /(\s+)message!:\s*string;/g,
'$1declare message: string;' "$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'); fs.writeFileSync(filePath, content, "utf8");
console.log('✓ API client post-processing completed'); console.log("✓ API client post-processing completed");
} catch (error) { } catch (error) {
throw new Error(`Failed to post-process API client: ${error}`); throw new Error(`Failed to post-process API client: ${error}`);
} }
} }
async function generateApiClient(): Promise<void> { async function generateApiClient(): Promise<void> {
console.log('Generating API client...'); console.log("Generating API client...");
try { try {
const npxCommand = getCommand('npx'); const url = "http://127.0.0.1:5000/GetAPIClientCode";
await execAsync(`${npxCommand} nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts`); const response = await fetch(url);
console.log('✓ API client generated successfully'); 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(); await postProcessApiClient();
@@ -290,35 +300,58 @@ async function generateApiClient(): Promise<void> {
} }
} }
async function generateSignalRClient(): Promise<void> {
console.log("Generating SignalR TypeScript client...");
try {
// TypedSignalR.Client.TypeScript.Analyzer 会在编译时自动生成客户端
// 我们只需要确保服务器项目构建一次即可生成 TypeScript 客户端
const { stdout, stderr } = await execAsync(
"dotnet build --configuration Release",
{ cwd: "./server" }
);
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> { async function main(): Promise<void> {
try { try {
// Generate SignalR client
await generateSignalRClient();
console.log("✓ SignalR TypeScript client generated successfully");
// Start web frontend first // Start web frontend first
await startWeb(); await startWeb();
console.log('✓ Frontend started'); console.log("✓ Frontend started");
// Wait a bit for frontend to fully initialize // Wait a bit for frontend to fully initialize
await new Promise(resolve => setTimeout(resolve, 3000)); await new Promise((resolve) => setTimeout(resolve, 3000));
// Start server // Start server
await startServer(); await startServer();
console.log('✓ Backend started'); console.log("✓ Backend started");
// Wait for server to be ready (给服务器额外时间完全启动) // Wait for server to be ready (给服务器额外时间完全启动)
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
// Check if swagger endpoint is available // Check if swagger endpoint is available
const serverReady = await waitForServer('http://localhost:5000/swagger/v1/swagger.json'); const serverReady = await waitForServer(
"http://localhost:5000/swagger/v1/swagger.json",
);
if (!serverReady) { if (!serverReady) {
throw new Error('Server failed to start within the expected time'); throw new Error("Server failed to start within the expected time");
} }
// Generate API client // Generate API client
await generateApiClient(); await generateApiClient();
console.log('✓ API generation completed successfully'); console.log("✓ API generation completed successfully");
} catch (error) { } catch (error) {
console.error('❌ Error:', error); console.error("❌ Error:", error);
process.exit(1); process.exit(1);
} finally { } finally {
// Always try to stop processes in order: server first, then web // Always try to stop processes in order: server first, then web
@@ -332,7 +365,7 @@ let isCleaningUp = false;
const cleanup = async (signal: string) => { const cleanup = async (signal: string) => {
if (isCleaningUp) { if (isCleaningUp) {
console.log('Cleanup already in progress, ignoring signal'); console.log("Cleanup already in progress, ignoring signal");
return; return;
} }
@@ -340,53 +373,44 @@ const cleanup = async (signal: string) => {
console.log(`\nReceived ${signal}, cleaning up...`); console.log(`\nReceived ${signal}, cleaning up...`);
try { try {
await Promise.all([ await Promise.all([stopServer(), stopWeb()]);
stopServer(),
stopWeb()
]);
} catch (error) { } catch (error) {
console.error('Error during cleanup:', error); console.error("Error during cleanup:", error);
} }
// 立即退出,不等待 // 立即退出,不等待
process.exit(0); process.exit(0);
}; };
process.on('SIGINT', () => cleanup('SIGINT')); process.on("SIGINT", () => cleanup("SIGINT"));
process.on('SIGTERM', () => cleanup('SIGTERM')); process.on("SIGTERM", () => cleanup("SIGTERM"));
// 处理未捕获的异常 // 处理未捕获的异常
process.on('uncaughtException', async (error) => { process.on("uncaughtException", async (error) => {
if (isCleaningUp) return; if (isCleaningUp) return;
console.error('❌ Uncaught exception:', error); console.error("❌ Uncaught exception:", error);
isCleaningUp = true; isCleaningUp = true;
try { try {
await Promise.all([ await Promise.all([stopServer(), stopWeb()]);
stopServer(),
stopWeb()
]);
} catch (cleanupError) { } catch (cleanupError) {
console.error('Error during cleanup:', cleanupError); console.error("Error during cleanup:", cleanupError);
} }
process.exit(1); process.exit(1);
}); });
process.on('unhandledRejection', async (reason, promise) => { process.on("unhandledRejection", async (reason, promise) => {
if (isCleaningUp) return; if (isCleaningUp) return;
console.error('❌ Unhandled rejection at:', promise, 'reason:', reason); console.error("❌ Unhandled rejection at:", promise, "reason:", reason);
isCleaningUp = true; isCleaningUp = true;
try { try {
await Promise.all([ await Promise.all([stopServer(), stopWeb()]);
stopServer(),
stopWeb()
]);
} catch (cleanupError) { } catch (cleanupError) {
console.error('Error during cleanup:', cleanupError); console.error("Error during cleanup:", cleanupError);
} }
process.exit(1); process.exit(1);
@@ -395,16 +419,13 @@ process.on('unhandledRejection', async (reason, promise) => {
main().catch(async (error) => { main().catch(async (error) => {
if (isCleaningUp) return; if (isCleaningUp) return;
console.error('❌ Unhandled error:', error); console.error("❌ Unhandled error:", error);
isCleaningUp = true; isCleaningUp = true;
try { try {
await Promise.all([ await Promise.all([stopServer(), stopWeb()]);
stopServer(),
stopWeb()
]);
} catch (cleanupError) { } catch (cleanupError) {
console.error('Error during cleanup:', cleanupError); console.error("Error during cleanup:", cleanupError);
} }
process.exit(1); 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);
}
}

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

@@ -0,0 +1,286 @@
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()
{
// 正常大端
var bytes = new byte[] { 0x12, 0x34, 0x56, 0x78, 0xAB, 0xCD, 0xEF, 0x01 };
var result = Number.BytesToUInt64((byte[])bytes.Clone(), false);
Assert.True(result.IsSuccessful);
Assert.Equal(0x12345678ABCDEF01UL, result.Value);
// 正常小端
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);
// 异常:长度超限
var result3 = Number.BytesToUInt64(new byte[9], false);
Assert.False(result3.IsSuccessful);
// 异常不足8字节
var result4 = Number.BytesToUInt64(new byte[] { 0x01, 0x02 }, false);
Assert.False(result4.IsSuccessful); // BitConverter.ToUInt64 需要8字节
}
/// <summary>
/// 测试 BytesToUInt32 的正常与异常情况
/// </summary>
[Fact]
public void Test_BytesToUInt32()
{
// 正常大端
var bytes = new byte[] { 0x12, 0x34, 0x56, 0x78 };
var result = Number.BytesToUInt32((byte[])bytes.Clone(), false);
Assert.True(result.IsSuccessful);
Assert.Equal(0x12345678U, result.Value);
// 正常小端
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);
// 异常:长度超限
var result3 = Number.BytesToUInt32(new byte[5], false);
Assert.False(result3.IsSuccessful);
// 异常不足4字节
var result4 = Number.BytesToUInt32(new byte[] { 0x01, 0x02 }, false);
Assert.False(result4.IsSuccessful); // BitConverter.ToUInt32 需要4字节
}
/// <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);
}
}

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

@@ -7,8 +7,11 @@ using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json; using Newtonsoft.Json;
using NLog; using NLog;
using NLog.Web; using NLog.Web;
using NSwag;
using NSwag.CodeGeneration.TypeScript;
using NSwag.Generation.Processors.Security; using NSwag.Generation.Processors.Security;
using server.Services; using server.Services;
using TypedSignalR.Client.DevTools;
// Early init of NLog to allow startup and exception logging, before host is built // Early init of NLog to allow startup and exception logging, before host is built
var logger = NLog.LogManager.Setup() var logger = NLog.LogManager.Setup()
@@ -92,8 +95,17 @@ try
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyHeader() .AllowAnyHeader()
); );
options.AddPolicy("SignalR", policy => policy
.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
}); });
// Use SignalR
builder.Services.AddSignalR();
// Add Swagger // Add Swagger
builder.Services.AddSwaggerDocument(options => builder.Services.AddSwaggerDocument(options =>
{ {
@@ -165,6 +177,17 @@ try
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")), FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")),
RequestPath = "/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"); app.MapFallbackToFile("index.html");
} }
@@ -185,12 +208,46 @@ try
}); });
app.UseSwaggerUi(); app.UseSwaggerUi();
// SignalR
app.UseWebSockets();
app.UseSignalRHubSpecification();
app.UseSignalRHubDevelopmentUI();
// Router // Router
app.MapControllers(); app.MapControllers();
app.MapHub<server.Hubs.JtagHub.JtagHub>("hubs/JtagHub");
// Setup Program // Setup Program
MsgBus.Init(); MsgBus.Init();
// 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(); app.Run();
} }
catch (Exception exception) catch (Exception exception)
@@ -210,4 +267,3 @@ finally
// Close Program // Close Program
MsgBus.Exit(); MsgBus.Exit();
} }

View File

@@ -14,6 +14,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ArpLookup" Version="2.0.3" />
<PackageReference Include="DotNext" Version="5.23.0" /> <PackageReference Include="DotNext" Version="5.23.0" />
<PackageReference Include="DotNext.Threading" Version="5.23.0" /> <PackageReference Include="DotNext.Threading" Version="5.23.0" />
<PackageReference Include="Honoo.IO.Hashing.Crc" Version="1.3.3" /> <PackageReference Include="Honoo.IO.Hashing.Crc" Version="1.3.3" />
@@ -26,10 +27,21 @@
<PackageReference Include="NLog" Version="5.4.0" /> <PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" /> <PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" />
<PackageReference Include="NSwag.AspNetCore" Version="14.3.0" /> <PackageReference Include="NSwag.AspNetCore" Version="14.3.0" />
<PackageReference Include="NSwag.CodeGeneration.TypeScript" Version="14.4.0" />
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" /> <PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" /> <PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" /> <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> </ItemGroup>
</Project> </Project>

View File

@@ -1,11 +1,14 @@
using System.Diagnostics; using System.Diagnostics;
using System.Net.NetworkInformation;
using ArpLookup;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
/// <summary> /// <summary>
/// ARP 记录管理静态类(跨平台支持) /// ARP 记录管理静态类(跨平台支持)
/// </summary> /// </summary>
public static class Arp public static class ArpClient
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -44,20 +47,28 @@ public static class Arp
} }
/// <summary> /// <summary>
/// 通过 Ping 动态更新指定 IP 的 ARP 记录 /// 动态更新指定 IP 的 ARP 记录
/// </summary> /// </summary>
/// <param name="ipAddress">要更新的 IP 地址</param> /// <param name="ipAddress">要更新的 IP 地址</param>
/// <returns>是否成功发送 Ping</returns> /// <returns>是否成功发送 Ping</returns>
public static async Task<bool> UpdateArpEntryByPingAsync(string ipAddress) public static async Task<bool> UpdateArpEntryAsync(string ipAddress)
{ {
if (string.IsNullOrWhiteSpace(ipAddress)) if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress)); throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
try try
{ {
using var ping = new System.Net.NetworkInformation.Ping(); var ret = await ArpClient.DeleteArpEntryAsync(ipAddress);
var reply = await ping.SendPingAsync(ipAddress, 100); if (!ret)
return reply.Status == System.Net.NetworkInformation.IPStatus.Success; {
logger.Error($"删除 ARP 记录失败: {ipAddress}");
}
PhysicalAddress? mac = await Arp.LookupAsync(IPAddress.Parse(ipAddress));
if (mac == null)
return false;
return true;
} }
catch catch
{ {
@@ -534,7 +545,7 @@ public static class Arp
// 新增 ARP 记录 // 新增 ARP 记录
var ret = await AddArpEntryAsync(ipAddress, formattedMac, interfaceName); var ret = await AddArpEntryAsync(ipAddress, formattedMac, interfaceName);
if(!ret) logger.Error($"添加 ARP 记录失败: {ipAddress} -> {formattedMac} on {interfaceName}"); if (!ret) logger.Error($"添加 ARP 记录失败: {ipAddress} -> {formattedMac} on {interfaceName}");
return true; return true;
} }

View File

@@ -1,9 +1,10 @@
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
public static class Global { public static class Global
{
public static readonly string localhost = "192.168.137.1"; public static readonly string localhost = "127.0.0.1";
public static string GetLocalIPAddress() public static string GetLocalIPAddress()
{ {

View File

@@ -65,7 +65,7 @@ public class Number
{ {
for (var i = 0; i < length; i++) for (var i = 0; i < length; i++)
{ {
arr[length - 1 - i] = Convert.ToByte((num >> (i << 3)) & (0xFF)); arr[i] = Convert.ToByte((num >> (i << 3)) & (0xFF));
} }
} }
else else
@@ -99,20 +99,11 @@ public class Number
try try
{ {
if (isLowNumHigh) if (!isLowNumHigh)
{ {
for (var i = 0; i < len; i++) Array.Reverse(bytes);
{
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));
}
} }
num = BitConverter.ToUInt64(bytes, 0);
return num; return num;
} }
@@ -143,20 +134,11 @@ public class Number
try try
{ {
if (isLowNumHigh) if (!isLowNumHigh)
{ {
for (var i = 0; i < len; i++) Array.Reverse(bytes);
{
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));
}
} }
num = BitConverter.ToUInt32(bytes, 0);
return num; return num;
} }
@@ -333,10 +315,9 @@ public class Number
for (int i = 0; i < srcBytesLen; i += distance) for (int i = 0; i < srcBytesLen; i += distance)
{ {
var end = i + distance; Buffer.BlockCopy(srcBytes, i, buffer, 0, distance);
buffer = srcBytes[i..end];
Array.Reverse(buffer); Array.Reverse(buffer);
Array.Copy(buffer, 0, dstBytes, i, distance); Buffer.BlockCopy(buffer, 0, dstBytes, i, distance);
} }
return dstBytes; return dstBytes;

View File

@@ -278,7 +278,7 @@ public class DataController : ControllerBase
return NotFound("没有可用的实验板"); return NotFound("没有可用的实验板");
var boardInfo = boardOpt.Value; var boardInfo = boardOpt.Value;
if (!(await Arp.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString()))) if (!(await ArpClient.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString())))
{ {
logger.Error($"无法配置ARP实验板可能会无法连接"); logger.Error($"无法配置ARP实验板可能会无法连接");
} }
@@ -346,7 +346,7 @@ public class DataController : ControllerBase
return NotFound("未找到对应的实验板"); return NotFound("未找到对应的实验板");
var boardInfo = ret.Value.Value; var boardInfo = ret.Value.Value;
if (!(await Arp.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString()))) if (!(await ArpClient.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString())))
{ {
logger.Error($"无法配置ARP实验板可能会无法连接"); logger.Error($"无法配置ARP实验板可能会无法连接");
} }
@@ -490,4 +490,3 @@ public class DataController : ControllerBase
} }
} }
} }

View File

@@ -0,0 +1,467 @@
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();
/// <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;
}
/// <summary>
/// 获取当前用户绑定的调试器实例
/// </summary>
private DebuggerClient? GetDebugger()
{
try
{
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return null;
using var db = new Database.AppDataConnection();
var userRet = db.GetUserByName(userName);
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
return null;
var user = userRet.Value.Value;
if (user.BoardID == Guid.Empty)
return null;
var boardRet = db.GetBoardByID(user.BoardID);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
return null;
var board = boardRet.Value.Value;
return new DebuggerClient(board.IpAddr, board.Port, 1);
}
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, "操作失败,请稍后重试");
}
}
}

View File

@@ -0,0 +1,291 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using DotNext;
namespace server.Controllers;
/// <summary>
/// 实验控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ExamController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 实验信息类
/// </summary>
public class ExamInfo
{
/// <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 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;
}
/// <summary>
/// 实验简要信息类(用于列表显示)
/// </summary>
public class ExamSummary
{
/// <summary>
/// 实验的唯一标识符
/// </summary>
public required string ID { get; set; }
/// <summary>
/// 实验名称
/// </summary>
public required string Name { 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;
}
/// <summary>
/// 创建实验请求类
/// </summary>
public class CreateExamRequest
{
/// <summary>
/// 实验ID
/// </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;
}
/// <summary>
/// 获取所有实验列表
/// </summary>
/// <returns>实验列表</returns>
[Authorize]
[HttpGet("list")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ExamSummary[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetExamList()
{
try
{
using var db = new Database.AppDataConnection();
var exams = db.GetAllExams();
var examSummaries = exams.Select(exam => new ExamSummary
{
ID = exam.ID,
Name = exam.Name,
CreatedTime = exam.CreatedTime,
UpdatedTime = exam.UpdatedTime,
Tags = exam.GetTagsList(),
Difficulty = exam.Difficulty,
IsVisibleToUsers = exam.IsVisibleToUsers
}).ToArray();
logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验");
return Ok(examSummaries);
}
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
{
using var db = new Database.AppDataConnection();
var result = db.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
{
ID = exam.ID,
Name = exam.Name,
Description = exam.Description,
CreatedTime = exam.CreatedTime,
UpdatedTime = exam.UpdatedTime,
Tags = exam.GetTagsList(),
Difficulty = exam.Difficulty,
IsVisibleToUsers = exam.IsVisibleToUsers
};
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]
[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] CreateExamRequest request)
{
if (string.IsNullOrWhiteSpace(request.ID) || string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Description))
return BadRequest("实验ID、名称和描述不能为空");
try
{
using var db = new Database.AppDataConnection();
var result = db.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
{
ID = exam.ID,
Name = exam.Name,
Description = exam.Description,
CreatedTime = exam.CreatedTime,
UpdatedTime = exam.UpdatedTime,
Tags = exam.GetTagsList(),
Difficulty = exam.Difficulty,
IsVisibleToUsers = exam.IsVisibleToUsers
};
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}");
}
}
}

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Database;
namespace server.Controllers; namespace server.Controllers;
@@ -14,8 +15,6 @@ public class JtagController : ControllerBase
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string BITSTREAM_PATH = "bitstream/Jtag";
/// <summary> /// <summary>
/// 控制器首页信息 /// 控制器首页信息
/// </summary> /// </summary>
@@ -112,64 +111,12 @@ public class JtagController : ControllerBase
} }
} }
/// <summary>
/// 上传比特流文件到服务器
/// </summary>
/// <param name="address">目标设备地址</param>
/// <param name="file">比特流文件</param>
/// <returns>上传结果</returns>
[HttpPost("UploadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
{
logger.Info($"User {User.Identity?.Name} uploading bitstream for device {address}");
if (file == null || file.Length == 0)
{
logger.Warn($"User {User.Identity?.Name} attempted to upload empty file for device {address}");
return TypedResults.BadRequest("未选择文件");
}
try
{
// 生成安全的文件名(避免路径遍历攻击)
var fileName = Path.GetRandomFileName();
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
// 如果存在文件,则删除原文件再上传
if (Directory.Exists(uploadsFolder))
{
Directory.Delete(uploadsFolder, true);
logger.Info($"User {User.Identity?.Name} removed existing bitstream folder for device {address}");
}
Directory.CreateDirectory(uploadsFolder);
var filePath = Path.Combine(uploadsFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
logger.Info($"User {User.Identity?.Name} successfully uploaded bitstream for device {address}, file size: {file.Length} bytes");
return TypedResults.Ok(true);
}
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} failed to upload bitstream for device {address}");
return TypedResults.InternalServerError(ex);
}
}
/// <summary> /// <summary>
/// 通过 JTAG 下载比特流文件到 FPGA 设备 /// 通过 JTAG 下载比特流文件到 FPGA 设备
/// </summary> /// </summary>
/// <param name="address">JTAG 设备地址</param> /// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param> /// <param name="port">JTAG 设备端口</param>
/// <param name="bitstreamId">比特流ID</param>
/// <returns>下载结果</returns> /// <returns>下载结果</returns>
[HttpPost("DownloadBitstream")] [HttpPost("DownloadBitstream")]
[EnableCors("Users")] [EnableCors("Users")]
@@ -177,50 +124,75 @@ public class JtagController : ControllerBase
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> DownloadBitstream(string address, int port) public async ValueTask<IResult> DownloadBitstream(string address, int port, int bitstreamId)
{ {
logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port}"); logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
// 检查文件
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
if (!Directory.Exists(fileDir))
{
logger.Warn($"User {User.Identity?.Name} attempted to download non-existent bitstream for device {address}");
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
}
try try
{ {
// 读取文件 // 获取当前用户名
var filePath = Directory.GetFiles(fileDir)[0]; var username = User.Identity?.Name;
logger.Info($"User {User.Identity?.Name} reading bitstream file: {filePath}"); if (string.IsNullOrEmpty(username))
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
{ {
if (fileStream is null || fileStream.Length <= 0) logger.Warn("Anonymous user attempted to download bitstream");
{ return TypedResults.Unauthorized();
logger.Warn($"User {User.Identity?.Name} found invalid bitstream file for device {address}");
return TypedResults.BadRequest("Wrong bitstream, Please upload it again");
} }
logger.Info($"User {User.Identity?.Name} processing bitstream file of size: {fileStream.Length} bytes"); // 从数据库获取用户信息
using var db = new Database.AppDataConnection();
var userResult = db.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 bitstreamResult = db.GetResourceById(bitstreamId);
if (!bitstreamResult.IsSuccessful)
{
logger.Error($"User {username} failed to get bitstream from database: {bitstreamResult.Error}");
return TypedResults.InternalServerError($"数据库查询失败: {bitstreamResult.Error?.Message}");
}
if (!bitstreamResult.Value.HasValue)
{
logger.Warn($"User {username} attempted to download non-existent bitstream ID: {bitstreamId}");
return TypedResults.BadRequest("比特流不存在");
}
var bitstream = bitstreamResult.Value.Value;
// 处理比特流数据
var fileBytes = bitstream.Data;
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");
// 定义缓冲区大小: 32KB // 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024]; byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = 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; int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0) while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{ {
// 反转 32bits // 反转 32bits
var retBuffer = Common.Number.ReverseBytes(buffer, 4); var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful) if (!retBuffer.IsSuccessful)
{ {
logger.Error($"User {User.Identity?.Name} failed to reverse bytes: {retBuffer.Error}"); logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
return TypedResults.InternalServerError(retBuffer.Error); return TypedResults.InternalServerError(retBuffer.Error);
} }
revBuffer = retBuffer.Value; revBuffer = retBuffer.Value;
@@ -230,34 +202,33 @@ public class JtagController : ControllerBase
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]); revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
} }
await memoryStream.WriteAsync(revBuffer, 0, bytesRead); await outputStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead; totalBytesProcessed += bytesRead;
} }
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存) // 获取处理后的数据
var fileBytes = memoryStream.ToArray(); var processedBytes = outputStream.ToArray();
logger.Info($"User {User.Identity?.Name} processed {totalBytesRead} bytes for device {address}"); logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}");
// 下载比特流 // 下载比特流
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(fileBytes); var ret = await jtagCtrl.DownloadBitstream(processedBytes);
if (ret.IsSuccessful) if (ret.IsSuccessful)
{ {
logger.Info($"User {User.Identity?.Name} successfully downloaded bitstream to device {address}"); logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}");
return TypedResults.Ok(ret.Value); return TypedResults.Ok(ret.Value);
} }
else else
{ {
logger.Error($"User {User.Identity?.Name} failed to download bitstream to device {address}: {ret.Error}"); logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
return TypedResults.InternalServerError(ret.Error); return TypedResults.InternalServerError(ret.Error);
} }
} }
} }
}
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while downloading bitstream to device {address}"); logger.Error(ex, $"User encountered exception while downloading bitstream to device {address}");
return TypedResults.InternalServerError(ex); return TypedResults.InternalServerError(ex);
} }
} }

View File

@@ -45,7 +45,18 @@ public class LogicAnalyzerController : ControllerBase
/// 全局触发模式 /// 全局触发模式
/// </summary> /// </summary>
public GlobalCaptureMode GlobalMode { get; set; } 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>
/// 信号触发配置列表 /// 信号触发配置列表
/// </summary> /// </summary>
@@ -77,7 +88,7 @@ public class LogicAnalyzerController : ControllerBase
return null; return null;
var board = boardRet.Value.Value; var board = boardRet.Value.Value;
return new Analyzer(board.IpAddr, board.Port, 2); return new Analyzer(board.IpAddr, board.Port, 0);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -208,8 +219,8 @@ public class LogicAnalyzerController : ControllerBase
{ {
try try
{ {
if (signalIndex < 0 || signalIndex > 7) if (signalIndex < 0 || signalIndex > 31)
return BadRequest("信号索引必须在0-7之间"); return BadRequest("信号索引必须在0-31之间");
var analyzer = GetAnalyzer(); var analyzer = GetAnalyzer();
if (analyzer == null) if (analyzer == null)
@@ -231,6 +242,48 @@ public class LogicAnalyzerController : ControllerBase
} }
} }
/// <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>
/// <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)
{
try
{
if (capture_length < 0 || capture_length > 2048*32)
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);
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>
/// 批量配置捕获参数 /// 批量配置捕获参数
/// </summary> /// </summary>
@@ -264,8 +317,8 @@ public class LogicAnalyzerController : ControllerBase
// 设置信号触发模式 // 设置信号触发模式
foreach (var signalConfig in config.SignalConfigs) foreach (var signalConfig in config.SignalConfigs)
{ {
if (signalConfig.SignalIndex < 0 || signalConfig.SignalIndex > 7) if (signalConfig.SignalIndex < 0 || signalConfig.SignalIndex > 31)
return BadRequest($"信号索引{signalConfig.SignalIndex}超出范围0-7"); return BadRequest($"信号索引{signalConfig.SignalIndex}超出范围0-31");
var signalResult = await analyzer.SetSignalTrigMode( var signalResult = await analyzer.SetSignalTrigMode(
signalConfig.SignalIndex, signalConfig.Operator, signalConfig.Value); signalConfig.SignalIndex, signalConfig.Operator, signalConfig.Value);
@@ -276,6 +329,14 @@ public class LogicAnalyzerController : ControllerBase
$"设置信号{signalConfig.SignalIndex}触发模式失败"); $"设置信号{signalConfig.SignalIndex}触发模式失败");
} }
} }
// 设置深度、预采样深度、有效通道
var paramsResult = await analyzer.SetCaptureParams(
config.CaptureLength, config.PreCaptureLength, config.ChannelDiv);
if (!paramsResult.IsSuccessful)
{
logger.Error($"设置深度、预采样深度、有效通道失败: {paramsResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置深度、预采样深度、有效通道失败");
}
return Ok(true); return Ok(true);
} }
@@ -330,7 +391,7 @@ public class LogicAnalyzerController : ControllerBase
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetCaptureData() public async Task<IActionResult> GetCaptureData(int capture_length = 2048 * 32)
{ {
try try
{ {
@@ -338,7 +399,7 @@ public class LogicAnalyzerController : ControllerBase
if (analyzer == null) if (analyzer == null)
return BadRequest("用户未绑定有效的实验板"); return BadRequest("用户未绑定有效的实验板");
var result = await analyzer.ReadCaptureData(); var result = await analyzer.ReadCaptureData(capture_length);
if (!result.IsSuccessful) if (!result.IsSuccessful)
{ {
logger.Error($"读取捕获数据失败: {result.Error}"); logger.Error($"读取捕获数据失败: {result.Error}");

View File

@@ -19,7 +19,6 @@ public class NetConfigController : ControllerBase
// 固定的实验板IP,端口,MAC地址 // 固定的实验板IP,端口,MAC地址
private const string BOARD_IP = "169.254.109.0"; private const string BOARD_IP = "169.254.109.0";
private const int BOARD_PORT = 1234; private const int BOARD_PORT = 1234;
private const string BOARD_MAC = "12:34:56:78:9a:bc";
// 本机网络信息 // 本机网络信息
private readonly IPAddress _localIP; private readonly IPAddress _localIP;
@@ -130,7 +129,7 @@ public class NetConfigController : ControllerBase
{ {
try try
{ {
return await Arp.CheckOrAddAsync(BOARD_IP, BOARD_MAC, _localInterface); return await ArpClient.UpdateArpEntryAsync(BOARD_IP);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -0,0 +1,377 @@
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();
/// <summary>
/// 资源信息类
/// </summary>
public class ResourceInfo
{
/// <summary>
/// 资源ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// 资源名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 资源类型
/// </summary>
public required string Type { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required string 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; }
}
/// <summary>
/// 添加资源请求类
/// </summary>
public class AddResourceRequest
{
/// <summary>
/// 资源类型
/// </summary>
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required string ResourcePurpose { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
}
/// <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) || string.IsNullOrWhiteSpace(request.ResourcePurpose) || file == null)
return BadRequest("资源类型、资源用途和文件不能为空");
// 验证资源用途
if (request.ResourcePurpose != Resource.ResourcePurposes.Template && request.ResourcePurpose != Resource.ResourcePurposes.User)
return BadRequest($"无效的资源用途: {request.ResourcePurpose}");
// 模板资源需要管理员权限
if (request.ResourcePurpose == Resource.ResourcePurposes.Template && !User.IsInRole("Admin"))
return Forbid("只有管理员可以添加模板资源");
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.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 = db.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 resource = result.Value;
var resourceInfo = new ResourceInfo
{
ID = resource.ID,
Name = resource.ResourceName,
Type = resource.ResourceType,
Purpose = resource.ResourcePurpose,
UploadTime = resource.UploadTime,
ExamID = resource.ExamID,
MimeType = resource.MimeType
};
logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName}");
return CreatedAtAction(nameof(GetResourceById), new { resourceId = resource.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] string? resourcePurpose = null)
{
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 普通用户只能查看自己的资源和模板资源
Guid? userId = null;
if (!User.IsInRole("Admin"))
{
// 如果指定了用户资源用途,则只查看自己的资源
if (resourcePurpose == Resource.ResourcePurposes.User)
{
userId = user.ID;
}
// 如果指定了模板资源用途则不限制用户ID
else if (resourcePurpose == Resource.ResourcePurposes.Template)
{
userId = null;
}
// 如果没有指定用途,则查看自己的用户资源和所有模板资源
else
{
// 这种情况下需要分别查询并合并结果
var userResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.User, user.ID);
var templateResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.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
{
ID = r.ID,
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.ResourcePurpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).ToArray();
logger.Info($"成功获取资源列表,共 {mergedResourceInfos.Length} 个资源");
return Ok(mergedResourceInfos);
}
}
var result = db.GetFullResourceList(examId, resourceType, resourcePurpose, userId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源列表时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {result.Error.Message}");
}
var resources = result.Value.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.ResourcePurpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).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(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
var result = db.GetResourceById(resourceId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}");
}
if (!result.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = result.Value.Value;
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
return File(resource.Data, 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(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户信息
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 先获取资源信息以验证权限
var resourceResult = db.GetResourceById(resourceId);
if (!resourceResult.IsSuccessful)
{
logger.Error($"获取资源时出错: {resourceResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {resourceResult.Error.Message}");
}
if (!resourceResult.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = resourceResult.Value.Value;
// 权限检查:管理员可以删除所有资源,普通用户只能删除自己的用户资源
if (!User.IsInRole("Admin"))
{
if (resource.ResourcePurpose == Resource.ResourcePurposes.Template)
return Forbid("普通用户不能删除模板资源");
if (resource.UserID != user.ID)
return Forbid("只能删除自己的资源");
}
var deleteResult = db.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}");
}
}
}

View File

@@ -150,6 +150,191 @@ public class Board
} }
} }
/// <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>
public class Resource
{
/// <summary>
/// 资源的唯一标识符
/// </summary>
[PrimaryKey, Identity]
public int ID { get; set; }
/// <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 string ResourcePurpose { get; set; }
/// <summary>
/// 资源名称(包含文件扩展名)
/// </summary>
[NotNull]
public required string ResourceName { get; set; }
/// <summary>
/// 资源的二进制数据
/// </summary>
[NotNull]
public required byte[] Data { get; set; }
/// <summary>
/// 资源创建/上传时间
/// </summary>
[NotNull]
public DateTime UploadTime { get; set; } = DateTime.Now;
/// <summary>
/// 资源的MIME类型
/// </summary>
[NotNull]
public string MimeType { get; set; } = "application/octet-stream";
/// <summary>
/// 资源类型枚举
/// </summary>
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 static class ResourcePurposes
{
/// <summary>
/// 模板资源,通常由管理员上传,供用户参考
/// </summary>
public const string Template = "template";
/// <summary>
/// 用户上传的资源
/// </summary>
public const string User = "user";
}
}
/// <summary> /// <summary>
/// 应用程序数据连接类,用于与数据库交互 /// 应用程序数据连接类,用于与数据库交互
/// </summary> /// </summary>
@@ -197,6 +382,8 @@ public class AppDataConnection : DataConnection
logger.Info("正在创建数据库表..."); logger.Info("正在创建数据库表...");
this.CreateTable<User>(); this.CreateTable<User>();
this.CreateTable<Board>(); this.CreateTable<Board>();
this.CreateTable<Exam>();
this.CreateTable<Resource>();
logger.Info("数据库表创建完成"); logger.Info("数据库表创建完成");
} }
@@ -208,6 +395,8 @@ public class AppDataConnection : DataConnection
logger.Warn("正在删除所有数据库表..."); logger.Warn("正在删除所有数据库表...");
this.DropTable<User>(); this.DropTable<User>();
this.DropTable<Board>(); this.DropTable<Board>();
this.DropTable<Exam>();
this.DropTable<Resource>();
logger.Warn("所有数据库表已删除"); logger.Warn("所有数据库表已删除");
} }
@@ -526,6 +715,31 @@ public class AppDataConnection : DataConnection
return new(boards[0]); return new(boards[0]);
} }
/// <summary>
/// 根据用户名获取实验板信息
/// </summary>
/// <param name="userName">用户名</param>
/// <returns>包含实验板信息的结果,如果未找到则返回空</returns>
public Result<Optional<Board>> GetBoardByUserName(string userName)
{
var boards = this.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>
/// 获取所有实验板信息 /// 获取所有实验板信息
/// </summary> /// </summary>
@@ -635,4 +849,418 @@ public class AppDataConnection : DataConnection
/// FPGA 板子表 /// FPGA 板子表
/// </summary> /// </summary>
public ITable<Board> BoardTable => this.GetTable<Board>(); 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>
/// <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 = this.ExamTable.Where(e => e.ID == 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);
}
this.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 += this.ExamTable.Where(e => e.ID == id).Set(e => e.Name, name).Update();
}
if (description != null)
{
result += this.ExamTable.Where(e => e.ID == 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 += this.ExamTable.Where(e => e.ID == id).Set(e => e.Tags, tagsString).Update();
}
if (difficulty.HasValue)
{
result += this.ExamTable.Where(e => e.ID == id).Set(e => e.Difficulty, Math.Max(1, Math.Min(5, difficulty.Value))).Update();
}
if (isVisibleToUsers.HasValue)
{
result += this.ExamTable.Where(e => e.ID == id).Set(e => e.IsVisibleToUsers, isVisibleToUsers.Value).Update();
}
// 更新时间
this.ExamTable.Where(e => e.ID == 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>
/// <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, string resourcePurpose, string resourceName, byte[] data, string? examId = null, string? mimeType = null)
{
try
{
// 验证用户是否存在
var user = this.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 = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault();
if (exam == null)
{
logger.Error($"实验不存在: {examId}");
return new(new Exception($"实验不存在: {examId}"));
}
}
// 验证资源用途
if (resourcePurpose != Resource.ResourcePurposes.Template && resourcePurpose != Resource.ResourcePurposes.User)
{
logger.Error($"无效的资源用途: {resourcePurpose}");
return new(new Exception($"无效的资源用途: {resourcePurpose}"));
}
// 如果未指定MIME类型根据文件扩展名自动确定
if (string.IsNullOrEmpty(mimeType))
{
var extension = Path.GetExtension(resourceName).ToLowerInvariant();
mimeType = GetMimeTypeFromExtension(extension, resourceName);
}
var resource = new Resource
{
UserID = userId,
ExamID = examId,
ResourceType = resourceType,
ResourcePurpose = resourcePurpose,
ResourceName = resourceName,
Data = data,
MimeType = mimeType,
UploadTime = DateTime.Now
};
var insertedId = this.InsertWithIdentity(resource);
resource.ID = Convert.ToInt32(insertedId);
logger.Info($"新资源已添加: {userId}/{resourceType}/{resourcePurpose}/{resourceName} ({data.Length} bytes)" +
(examId != null ? $" [实验: {examId}]" : "") + $" [ID: {resource.ID}]");
return new(resource);
}
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<(int ID, string Name)[]> GetResourceList(string resourceType, string? examId = null, string? resourcePurpose = null, Guid? userId = null)
{
try
{
var query = this.ResourceTable.Where(r => r.ResourceType == resourceType);
if (examId != null)
{
query = query.Where(r => r.ExamID == examId);
}
if (resourcePurpose != null)
{
query = query.Where(r => r.ResourcePurpose == resourcePurpose);
}
if (userId != null)
{
query = query.Where(r => r.UserID == userId);
}
var resources = query
.Select(r => new { r.ID, r.ResourceName })
.ToArray();
var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray();
logger.Info($"获取资源列表: {resourceType}" +
(examId != null ? $"/{examId}" : "") +
(resourcePurpose != null ? $"/{resourcePurpose}" : "") +
(userId != null ? $"/{userId}" : "") +
$",共 {result.Length} 个资源");
return new(result);
}
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, string? resourcePurpose = null, Guid? userId = null)
{
try
{
var query = this.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.ResourcePurpose == 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 != null ? $" [用途: {resourcePurpose}]" : "") +
(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 Result<Optional<Resource>> GetResourceById(int resourceId)
{
try
{
var resource = this.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
if (resource == null)
{
logger.Info($"未找到资源: {resourceId}");
return new(Optional<Resource>.None);
}
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
return new(resource);
}
catch (Exception ex)
{
logger.Error($"获取资源时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 删除资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>删除的记录数</returns>
public Result<int> DeleteResource(int resourceId)
{
try
{
var result = this.ResourceTable.Where(r => r.ID == resourceId).Delete();
logger.Info($"资源已删除: {resourceId},删除记录数: {result}");
return new(result);
}
catch (Exception ex)
{
logger.Error($"删除资源时出错: {ex.Message}");
return new(ex);
}
}
/// <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",
".sbit" => "application/octet-stream",
".bit" => "application/octet-stream",
".bin" => "application/octet-stream",
".json" => "application/json",
".zip" => "application/zip",
".md" => "text/markdown",
_ => "application/octet-stream"
};
}
/// <summary>
/// 获取所有实验信息
/// </summary>
/// <returns>所有实验的数组</returns>
public Exam[] GetAllExams()
{
var exams = this.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 = this.ExamTable.Where(exam => exam.ID == 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]);
}
/// <summary>
/// 根据文件扩展名获取比特流MIME类型
/// </summary>
/// <param name="extension">文件扩展名</param>
/// <returns>MIME类型</returns>
private string GetBitstreamMimeType(string extension)
{
return extension.ToLowerInvariant() switch
{
".bit" => "application/octet-stream",
".sbit" => "application/octet-stream",
".bin" => "application/octet-stream",
".mcs" => "application/octet-stream",
".hex" => "text/plain",
_ => "application/octet-stream"
};
}
} }

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

@@ -0,0 +1,196 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
using DotNext;
using System.Collections.Concurrent;
using TypedSignalR.Client;
using Tapper;
namespace server.Hubs.JtagHub;
[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 static ConcurrentDictionary<string, int> FreqTable = new();
private static ConcurrentDictionary<string, CancellationTokenSource> CancellationTokenSourceTable = new();
private readonly IHubContext<JtagHub, IJtagReceiver> _hubContext;
public JtagHub(IHubContext<JtagHub, IJtagReceiver> hubContext)
{
_hubContext = hubContext;
}
private Optional<Peripherals.JtagClient.Jtag> GetJtagClient(string userName)
{
try
{
using var db = new Database.AppDataConnection();
var board = db.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)
{
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()
{
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

@@ -23,7 +23,7 @@ public static class MsgBus
/// <returns>无</returns> /// <returns>无</returns>
public async static void Init() public async static void Init()
{ {
if (!Arp.IsAdministrator()) if (!ArpClient.IsAdministrator())
{ {
logger.Error($"非管理员运行ARP无法更新请用管理员权限运行"); logger.Error($"非管理员运行ARP无法更新请用管理员权限运行");
// throw new Exception($"非管理员运行ARP无法更新请用管理员权限运行"); // throw new Exception($"非管理员运行ARP无法更新请用管理员权限运行");

View File

@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using DotNext; using DotNext;
using Peripherals.PowerClient; using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.CameraClient; namespace Peripherals.CameraClient;
@@ -19,7 +20,7 @@ class Camera
{ {
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000; readonly int timeout = 500;
readonly int taskID; readonly int taskID;
readonly int port; readonly int port;
readonly string address; readonly string address;
@@ -43,7 +44,7 @@ class Camera
/// <param name="address">摄像头设备IP地址</param> /// <param name="address">摄像头设备IP地址</param>
/// <param name="port">摄像头设备端口</param> /// <param name="port">摄像头设备端口</param>
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
public Camera(string address, int port, int timeout = 2000) public Camera(string address, int port, int timeout = 500)
{ {
if (timeout < 0) if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout)); throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
@@ -225,6 +226,7 @@ class Camera
this.taskID, // taskID this.taskID, // taskID
FrameAddr, FrameAddr,
(int)_currentFrameLength, // 使用当前分辨率的动态大小 (int)_currentFrameLength, // 使用当前分辨率的动态大小
BurstType.ExtendBurst,
this.timeout); this.timeout);
if (!result.IsSuccessful) if (!result.IsSuccessful)
@@ -462,6 +464,20 @@ class Camera
); );
} }
/// <summary>
/// 配置为960x540分辨率
/// </summary>
/// <returns>配置结果</returns>
public async ValueTask<Result<bool>> ConfigureResolution960x540()
{
return await ConfigureResolution(
hStart: 0, vStart: 0,
dvpHo: 960, dvpVo: 540,
hts: 1700, vts: 1500,
hOffset: 16, vOffset: 4
);
}
/// <summary> /// <summary>
/// 配置为320x240分辨率 /// 配置为320x240分辨率
/// </summary> /// </summary>
@@ -543,6 +559,9 @@ class Camera
case "640x480": case "640x480":
result = await ConfigureResolution640x480(); result = await ConfigureResolution640x480();
break; break;
case "960x540":
result = await ConfigureResolution960x540();
break;
case "1280x720": case "1280x720":
result = await ConfigureResolution1280x720(); result = await ConfigureResolution1280x720();
break; break;

View File

@@ -119,12 +119,18 @@ public class DebuggerClient
/// <summary> /// <summary>
/// 设置信号捕获模式 /// 设置信号捕获模式
/// </summary> /// </summary>
/// <param name="wireNum">要设置的线</param>
/// <param name="mode">要设置的捕获模式</param> /// <param name="mode">要设置的捕获模式</param>
/// <returns>操作结果成功返回true失败返回错误信息</returns> /// <returns>操作结果成功返回true失败返回错误信息</returns>
public async ValueTask<Result<bool>> SetMode(CaptureMode mode) 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); UInt32 data = ((UInt32)mode);
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Mode, data, this.timeout); var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Mode + wireNum, data, this.timeout);
if (!ret.IsSuccessful) if (!ret.IsSuccessful)
{ {
logger.Error($"Failed to set mode: {ret.Error}"); logger.Error($"Failed to set mode: {ret.Error}");
@@ -175,7 +181,7 @@ public class DebuggerClient
logger.Error("ReadAddr returned invalid data for flag"); logger.Error("ReadAddr returned invalid data for flag");
return new(new Exception("Failed to read flag")); return new(new Exception("Failed to read flag"));
} }
return ret.Value.Options.Data[0]; return ret.Value.Options.Data[3];
} }
/// <summary> /// <summary>
@@ -201,30 +207,26 @@ public class DebuggerClient
/// <summary> /// <summary>
/// 从指定偏移地址读取捕获的数据 /// 从指定偏移地址读取捕获的数据
/// </summary> /// </summary>
/// <param name="offset">数据读取的偏移地址</param> /// <param name="portNum">Port数量</param>
/// <returns>操作结果,成功返回32KB的捕获数据,失败返回错误信息</returns> /// <returns>操作结果,成功返回捕获数据,失败返回错误信息</returns>
public async ValueTask<Result<byte[]>> ReadData(UInt16 offset) public async ValueTask<Result<byte[]>> ReadData(UInt32 portNum)
{ {
var captureData = new byte[1024 * 32]; var captureData = new byte[1024 * 4 * portNum];
{ {
var ret = await UDPClientPool.ReadAddr4BytesAsync(this.ep, this.taskID, this.captureDataAddr + offset, 512, this.timeout); var ret = await UDPClientPool.ReadAddr4Bytes(this.ep, this.taskID, this.captureDataAddr, captureData.Length / 4, this.timeout);
if (!ret.IsSuccessful) if (!ret.IsSuccessful)
{ {
logger.Error($"Failed to read data: {ret.Error}"); logger.Error($"Failed to read data: {ret.Error}");
return new(ret.Error); return new(ret.Error);
} }
Buffer.BlockCopy(ret.Value, 0, captureData, 0, 512 * 4); if (ret.Value.Length != captureData.Length)
}
{ {
var ret = await UDPClientPool.ReadAddr4BytesAsync(this.ep, this.taskID, this.captureDataAddr + offset + 512, 512, this.timeout); logger.Error($"Receive capture data length should be {captureData.Length} instead of {ret.Value.Length}");
if (!ret.IsSuccessful) return new(new Exception($"Receive capture data length should be {captureData.Length} instead of {ret.Value.Length}"));
{
logger.Error($"Failed to read data: {ret.Error}");
return new(ret.Error);
} }
Buffer.BlockCopy(ret.Value, 0, captureData, 512 * 4, 512 * 4); Buffer.BlockCopy(ret.Value, 0, captureData, 0, captureData.Length);
} }
return captureData; return captureData;

View File

@@ -0,0 +1,118 @@
using System.Net;
using DotNext;
using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.HdmiInClient;
static class HdmiInAddr
{
public const UInt32 BASE = 0xA000_0000;
public const UInt32 HdmiIn_CTRL = BASE + 0x0; //[0]: rstn, 0 is reset.
public const UInt32 HdmiIn_READFIFO = BASE + 0x1;
}
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;
// 动态分辨率参数
private UInt16 _currentWidth = 960;
private UInt16 _currentHeight = 540;
private UInt32 _currentFrameLength = 960 * 540 * 2 / 4; // RGB565格式2字节/像素按4字节对齐
/// <summary>
/// 初始化HDMI输入客户端
/// </summary>
/// <param name="address">HDMI输入设备IP地址</param>
/// <param name="port">HDMI输入设备端口</param>
/// <param name="timeout">超时时间(毫秒)</param>
public HdmiIn(string address, int port, 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.timeout = timeout;
}
public async ValueTask<Result<bool>> EnableTrans(bool isEnable)
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, HdmiInAddr.HdmiIn_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.HdmiIn_READFIFO,
(int)_currentFrameLength, // 使用当前分辨率的动态大小
BurstType.FixedBurst,
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;
}
/// <summary>
/// 获取当前分辨率
/// </summary>
/// <returns>当前分辨率(宽度, 高度)</returns>
public (int Width, int Height) GetCurrentResolution()
{
return (_currentWidth, _currentHeight);
}
/// <summary>
/// 获取当前帧长度
/// </summary>
/// <returns>当前帧长度</returns>
public UInt32 GetCurrentFrameLength()
{
return _currentFrameLength;
}
}

View File

@@ -386,7 +386,10 @@ public class Jtag
readonly int timeout; readonly int timeout;
readonly int port; readonly int port;
readonly string address; /// <summary>
/// Jtag控制器IP地址
/// </summary>
public readonly string address;
private IPEndPoint ep; private IPEndPoint ep;
/// <summary> /// <summary>
@@ -436,7 +439,7 @@ public class Jtag
if (retPackLen != 4) if (retPackLen != 4)
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes")); 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 async ValueTask<Result<bool>> WriteFIFO
@@ -609,13 +612,10 @@ public class Jtag
if (ret.Value) if (ret.Value)
{ {
var array = new UInt32[UInt32Num]; var array = new UInt32[UInt32Num];
for (int i = 0; i < UInt32Num; i++) var retData = await UDPClientPool.ReadAddr4Bytes(ep, 0, JtagAddr.READ_DATA, (int)UInt32Num);
{
var retData = await ReadFIFO(JtagAddr.READ_DATA);
if (!retData.IsSuccessful) if (!retData.IsSuccessful)
return new(new Exception("Read FIFO failed when Load DR")); return new(new Exception("Read FIFO failed when Load DR"));
array[i] = retData.Value; Buffer.BlockCopy(retData.Value, 0, array, 0, (int)UInt32Num * 4);
}
return array; return array;
} }
else else
@@ -785,7 +785,7 @@ public class Jtag
{ {
var paser = new BsdlParser.Parser(); var paser = new BsdlParser.Parser();
var portNum = paser.GetBoundaryRegsNum().Value; var portNum = paser.GetBoundaryRegsNum().Value;
logger.Debug($"Get boundar scan registers number: {portNum}"); logger.Debug($"Get boundary scan registers number: {portNum}");
// Clear Data // Clear Data
MsgBus.UDPServer.ClearUDPData(this.address, 0); MsgBus.UDPServer.ClearUDPData(this.address, 0);

View File

@@ -2,12 +2,15 @@ using System.Collections;
using System.Net; using System.Net;
using Common; using Common;
using DotNext; using DotNext;
using WebProtocol;
namespace Peripherals.LogicAnalyzerClient; namespace Peripherals.LogicAnalyzerClient;
static class AnalyzerAddr static class AnalyzerAddr
{ {
const UInt32 BASE = 0x9000_0000; const UInt32 BASE = 0x9000_0000;
const UInt32 DMA1_BASE = 0x7000_0000;
const UInt32 DDR_BASE = 0x0000_0000;
/// <summary> /// <summary>
/// 0x0000_0000 R/W [ 0] capture on: 置1开始等待捕获0停止捕获。捕获到信号后该位自动清零。 <br/> /// 0x0000_0000 R/W [ 0] capture on: 置1开始等待捕获0停止捕获。捕获到信号后该位自动清零。 <br/>
@@ -44,22 +47,37 @@ static class AnalyzerAddr
/// 111 SOME NUMBER <br/> /// 111 SOME NUMBER <br/>
/// </summary> /// </summary>
public static readonly UInt32[] SIGNAL_TRIG_MODE = { public static readonly UInt32[] SIGNAL_TRIG_MODE = {
BASE + 0x0000_0010, BASE + 0x0000_0010, BASE + 0x0000_0011,
BASE + 0x0000_0011, BASE + 0x0000_0012, BASE + 0x0000_0013,
BASE + 0x0000_0012, BASE + 0x0000_0014, BASE + 0x0000_0015,
BASE + 0x0000_0013, BASE + 0x0000_0016, BASE + 0x0000_0017,
BASE + 0x0000_0014, BASE + 0x0000_0018, BASE + 0x0000_0019,
BASE + 0x0000_0015, BASE + 0x0000_001A, BASE + 0x0000_001B,
BASE + 0x0000_0016, BASE + 0x0000_001C, BASE + 0x0000_001D,
BASE + 0x0000_0017, 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 DMA1_START_WRITE_ADDR = DMA1_BASE + 0x0000_0012;
public const UInt32 DMA1_END_WRITE_ADDR = DMA1_BASE + 0x0000_0013;
public const UInt32 DMA1_CAPTURE_CTRL_ADDR = DMA1_BASE + 0x0000_0014;
public const UInt32 STORE_OFFSET_ADDR = DDR_BASE + 0x0010_0000;
/// <summary> /// <summary>
/// 0x0100_0000 - 0x0100_03FF 只读 32位波形存储得到的32位数据中低八位最先捕获高八位最后捕获。<br/> /// 0x0100_0000 - 0x0100_03FF 只读 32位波形存储得到的32位数据中低八位最先捕获高八位最后捕获。<br/>
/// 共1024个地址每个地址存储4组深度为4096。<br/> /// 共1024个地址每个地址存储4组深度为4096。<br/>
/// </summary> /// </summary>
public const UInt32 CAPTURE_DATA_ADDR = BASE + 0x0100_0000;
public const Int32 CAPTURE_DATA_LENGTH = 1024; public const Int32 CAPTURE_DATA_LENGTH = 1024;
public const Int32 CAPTURE_DATA_PRELOAD = 512;
} }
/// <summary> /// <summary>
@@ -190,6 +208,37 @@ public enum SignalValue : byte
SomeNumber = 0b111 // SOME NUMBER 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> /// <summary>
/// FPGA逻辑分析仪客户端用于控制FPGA上的逻辑分析仪模块进行信号捕获和分析 /// FPGA逻辑分析仪客户端用于控制FPGA上的逻辑分析仪模块进行信号捕获和分析
/// </summary> /// </summary>
@@ -234,8 +283,21 @@ public class Analyzer
// 构造寄存器值 // 构造寄存器值
UInt32 value = 0; UInt32 value = 0;
if (captureOn) value |= 1 << 0; if (captureOn) value |= 1 << 0;
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA1_CAPTURE_CTRL_ADDR, value, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set DMA1_CAPTURE_CTRL_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DMA1_CAPTURE_CTRL_ADDR returned false");
return new(new Exception("Failed to set DMA1_CAPTURE_CTRL_ADDR"));
}
}
if (force) value |= 1 << 8; if (force) value |= 1 << 8;
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, value, this.timeout); var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, value, this.timeout);
if (!ret.IsSuccessful) if (!ret.IsSuccessful)
{ {
@@ -247,6 +309,7 @@ public class Analyzer
logger.Error("WriteAddr to CAPTURE_MODE returned false"); logger.Error("WriteAddr to CAPTURE_MODE returned false");
return new(new Exception("Failed to set capture mode")); return new(new Exception("Failed to set capture mode"));
} }
}
return true; return true;
} }
@@ -306,7 +369,7 @@ public class Analyzer
return new(new ArgumentException($"Signal index must be 0~{AnalyzerAddr.SIGNAL_TRIG_MODE.Length}")); return new(new ArgumentException($"Signal index must be 0~{AnalyzerAddr.SIGNAL_TRIG_MODE.Length}"));
// 计算模式值: [2:0] 信号值, [5:3] 操作符 // 计算模式值: [2:0] 信号值, [5:3] 操作符
UInt32 mode = ((UInt32)op << 3) | (UInt32) val; UInt32 mode = ((UInt32)op << 3) | (UInt32)val;
var addr = AnalyzerAddr.SIGNAL_TRIG_MODE[signalIndex]; var addr = AnalyzerAddr.SIGNAL_TRIG_MODE[signalIndex];
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, addr, mode, this.timeout); var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, addr, mode, this.timeout);
@@ -323,17 +386,97 @@ public class Analyzer
return true; 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>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetCaptureParams(int capture_length, int pre_capture_length, AnalyzerChannelDiv channel_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.DMA1_START_WRITE_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set DMA1_START_WRITE_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DMA1_START_WRITE_ADDR returned false");
return new(new Exception("Failed to set DMA1_START_WRITE_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA1_END_WRITE_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR + (UInt32)(capture_length - 1), this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set DMA1_END_WRITE_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DMA1_END_WRITE_ADDR returned false");
return new(new Exception("Failed to set DMA1_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"));
}
}
return true;
}
/// <summary> /// <summary>
/// 读取捕获的波形数据 /// 读取捕获的波形数据
/// </summary> /// </summary>
/// <returns>操作结果成功返回byte[],否则返回异常信息</returns> /// <returns>操作结果成功返回byte[],否则返回异常信息</returns>
public async ValueTask<Result<byte[]>> ReadCaptureData() public async ValueTask<Result<byte[]>> ReadCaptureData(int capture_length = 2048 * 32)
{ {
var ret = await UDPClientPool.ReadAddr4BytesAsync( var ret = await UDPClientPool.ReadAddr4BytesAsync(
this.ep, this.ep,
this.taskID, this.taskID,
AnalyzerAddr.CAPTURE_DATA_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR,
AnalyzerAddr.CAPTURE_DATA_LENGTH, capture_length,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout this.timeout
); );
if (!ret.IsSuccessful) if (!ret.IsSuccessful)
@@ -342,7 +485,7 @@ public class Analyzer
return new(ret.Error); return new(ret.Error);
} }
var data = ret.Value; var data = ret.Value;
if (data == null || data.Length != AnalyzerAddr.CAPTURE_DATA_LENGTH * 4) if (data == null || data.Length != capture_length * 4)
{ {
logger.Error($"Capture data length mismatch: {data?.Length}"); logger.Error($"Capture data length mismatch: {data?.Length}");
return new(new Exception("Capture data length mismatch")); return new(new Exception("Capture data length mismatch"));

View File

@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using Common; using Common;
using DotNext; using DotNext;
using WebProtocol;
namespace Peripherals.OscilloscopeClient; namespace Peripherals.OscilloscopeClient;
@@ -319,6 +320,7 @@ class Oscilloscope
this.taskID, this.taskID,
OscilloscopeAddr.RD_DATA_ADDR, OscilloscopeAddr.RD_DATA_ADDR,
(int)OscilloscopeAddr.RD_DATA_LENGTH / 32, (int)OscilloscopeAddr.RD_DATA_LENGTH / 32,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout this.timeout
); );
if (!ret.IsSuccessful) if (!ret.IsSuccessful)

View File

@@ -1109,6 +1109,7 @@ public class HttpVideoStreamService : BackgroundService
return new List<(int, int, string)> return new List<(int, int, string)>
{ {
(640, 480, "640x480 (VGA)"), (640, 480, "640x480 (VGA)"),
(960, 540, "960x540 (qHD)"),
(1280, 720, "1280x720 (HD)"), (1280, 720, "1280x720 (HD)"),
(1280, 960, "1280x960 (SXGA)"), (1280, 960, "1280x960 (SXGA)"),
(1920, 1080, "1920x1080 (Full HD)") (1920, 1080, "1920x1080 (Full HD)")

View File

@@ -336,7 +336,7 @@ public class UDPClientPool
$"Device {address} receive data is {retData.Length} bytes instead of 4 bytes")); $"Device {address} receive data is {retData.Length} bytes instead of 4 bytes"));
// Check result // 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; if (Common.Number.BitsCheck(retCode, result, resultMask)) return true;
} }
catch (Exception error) catch (Exception error)
@@ -433,11 +433,12 @@ public class UDPClientPool
/// <param name="endPoint">IP端点IP地址与端口</param> /// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param> /// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param> /// <param name="devAddr">设备地址</param>
/// <param name="burstType">突发类型</param>
/// <param name="dataLength">要读取的数据长度4字节</param> /// <param name="dataLength">要读取的数据长度4字节</param>
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
/// <returns>读取结果,包含接收到的字节数组</returns> /// <returns>读取结果,包含接收到的字节数组</returns>
public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync( public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync(
IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, int timeout = 1000) IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, BurstType burstType, int timeout = 1000)
{ {
var pkgList = new List<SendAddrPackage>(); var pkgList = new List<SendAddrPackage>();
var resultData = new List<byte>(); var resultData = new List<byte>();
@@ -460,7 +461,7 @@ public class UDPClientPool
var opts = new SendAddrPackOptions var opts = new SendAddrPackOptions
{ {
BurstType = BurstType.FixedBurst, BurstType = burstType,
CommandID = Convert.ToByte(taskID), CommandID = Convert.ToByte(taskID),
IsWrite = false, IsWrite = false,
BurstLength = (byte)(currentSegmentSize - 1), BurstLength = (byte)(currentSegmentSize - 1),

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,14 @@ const isDarkMode = ref(
window.matchMedia("(prefers-color-scheme: dark)").matches, window.matchMedia("(prefers-color-scheme: dark)").matches,
); );
// Navbar显示状态管理
const showNavbar = ref(true);
// 切换Navbar显示状态
const toggleNavbar = () => {
showNavbar.value = !showNavbar.value;
};
// 初始化主题设置 // 初始化主题设置
onMounted(() => { onMounted(() => {
// 应用初始主题 // 应用初始主题
@@ -47,6 +55,12 @@ provide("theme", {
toggleTheme, toggleTheme,
}); });
// 提供Navbar控制给子组件
provide("navbar", {
showNavbar,
toggleNavbar,
});
const currentRoutePath = computed(() => { const currentRoutePath = computed(() => {
return router.currentRoute.value.path; return router.currentRoute.value.path;
}); });
@@ -56,8 +70,8 @@ useAlertProvider();
<template> <template>
<div> <div>
<header class="relative"> <header class="relative" :class="{ 'navbar-hidden': !showNavbar }">
<Navbar /> <Navbar v-show="showNavbar" />
<Dialog /> <Dialog />
<Alert /> <Alert />
</header> </header>
@@ -79,4 +93,25 @@ useAlertProvider();
<style scoped> <style scoped>
/* 特定于App.vue的样式 */ /* 特定于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> </style>

View File

@@ -0,0 +1,118 @@
/* THIS (.ts) FILE IS GENERATED BY TypedSignalR.Client.TypeScript */
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
import type { IJtagHub, IJtagReceiver } from './server.Hubs.JtagHub';
// components
export type Disposable = {
dispose(): void;
}
export type HubProxyFactory<T> = {
createHubProxy(connection: HubConnection): T;
}
export type ReceiverRegister<T> = {
register(connection: HubConnection, receiver: T): Disposable;
}
type ReceiverMethod = {
methodName: string,
method: (...args: any[]) => void
}
class ReceiverMethodSubscription implements Disposable {
public constructor(
private connection: HubConnection,
private receiverMethod: ReceiverMethod[]) {
}
public readonly dispose = () => {
for (const it of this.receiverMethod) {
this.connection.off(it.methodName, it.method);
}
}
}
// API
export type HubProxyFactoryProvider = {
(hubType: "IJtagHub"): HubProxyFactory<IJtagHub>;
}
export const getHubProxyFactory = ((hubType: string) => {
if(hubType === "IJtagHub") {
return IJtagHub_HubProxyFactory.Instance;
}
}) as HubProxyFactoryProvider;
export type ReceiverRegisterProvider = {
(receiverType: "IJtagReceiver"): ReceiverRegister<IJtagReceiver>;
}
export const getReceiverRegister = ((receiverType: string) => {
if(receiverType === "IJtagReceiver") {
return IJtagReceiver_Binder.Instance;
}
}) as ReceiverRegisterProvider;
// HubProxy
class IJtagHub_HubProxyFactory implements HubProxyFactory<IJtagHub> {
public static Instance = new IJtagHub_HubProxyFactory();
private constructor() {
}
public readonly createHubProxy = (connection: HubConnection): IJtagHub => {
return new IJtagHub_HubProxy(connection);
}
}
class IJtagHub_HubProxy implements IJtagHub {
public constructor(private connection: HubConnection) {
}
public readonly setBoundaryScanFreq = async (freq: number): Promise<boolean> => {
return await this.connection.invoke("SetBoundaryScanFreq", freq);
}
public readonly startBoundaryScan = async (freq: number): Promise<boolean> => {
return await this.connection.invoke("StartBoundaryScan", freq);
}
public readonly stopBoundaryScan = async (): Promise<boolean> => {
return await this.connection.invoke("StopBoundaryScan");
}
}
// Receiver
class IJtagReceiver_Binder implements ReceiverRegister<IJtagReceiver> {
public static Instance = new IJtagReceiver_Binder();
private constructor() {
}
public readonly register = (connection: HubConnection, receiver: IJtagReceiver): Disposable => {
const __onReceiveBoundaryScanData = (...args: [Partial<Record<string, boolean>>]) => receiver.onReceiveBoundaryScanData(...args);
connection.on("OnReceiveBoundaryScanData", __onReceiveBoundaryScanData);
const methodList: ReceiverMethod[] = [
{ methodName: "OnReceiveBoundaryScanData", method: __onReceiveBoundaryScanData }
]
return new ReceiverMethodSubscription(connection, methodList);
}
}

View File

@@ -0,0 +1,31 @@
/* THIS (.ts) FILE IS GENERATED BY TypedSignalR.Client.TypeScript */
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
import type { IStreamResult, Subject } from '@microsoft/signalr';
export type IJtagHub = {
/**
* @param freq Transpiled from int
* @returns Transpiled from System.Threading.Tasks.Task<bool>
*/
setBoundaryScanFreq(freq: number): Promise<boolean>;
/**
* @param freq Transpiled from int
* @returns Transpiled from System.Threading.Tasks.Task<bool>
*/
startBoundaryScan(freq: number): Promise<boolean>;
/**
* @returns Transpiled from System.Threading.Tasks.Task<bool>
*/
stopBoundaryScan(): Promise<boolean>;
}
export type IJtagReceiver = {
/**
* @param msg Transpiled from System.Collections.Generic.Dictionary<string, bool>
* @returns Transpiled from System.Threading.Tasks.Task
*/
onReceiveBoundaryScanData(msg: Partial<Record<string, boolean>>): Promise<void>;
}

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="fixed left-1/2 top-30 z-999 -translate-x-1/2"> <div class="fixed left-1/2 top-30 z-[9999] -translate-x-1/2">
<transition <transition
name="alert" name="alert"
enter-active-class="alert-enter-active" enter-active-class="alert-enter-active"

View File

@@ -119,6 +119,7 @@
componentManager.prepareComponentProps( componentManager.prepareComponentProps(
component.attrs || {}, component.attrs || {},
component.id, component.id,
props.examId,
) )
" "
@update:bindKey=" @update:bindKey="
@@ -175,9 +176,7 @@ import {
ref, ref,
reactive, reactive,
onMounted, onMounted,
onUnmounted,
computed, computed,
watch,
provide, provide,
} from "vue"; } from "vue";
import { useEventListener } from "@vueuse/core"; import { useEventListener } from "@vueuse/core";
@@ -188,7 +187,6 @@ import { useAlertStore } from "@/components/Alert";
// 导入 diagram 管理器 // 导入 diagram 管理器
import { import {
loadDiagramData, loadDiagramData,
saveDiagramData,
updatePartPosition, updatePartPosition,
updatePartAttribute, updatePartAttribute,
parseConnectionPin, parseConnectionPin,
@@ -217,6 +215,7 @@ const emit = defineEmits(["toggle-doc-panel", "open-components"]);
// 定义组件接受的属性 // 定义组件接受的属性
const props = defineProps<{ const props = defineProps<{
showDocPanel?: boolean; // 添加属性接收文档面板的显示状态 showDocPanel?: boolean; // 添加属性接收文档面板的显示状态
examId?: string; // 新增examId属性
}>(); }>();
// 获取componentManager实例 // 获取componentManager实例
@@ -606,14 +605,13 @@ function onComponentDrag(e: MouseEvent) {
// 停止拖拽组件 // 停止拖拽组件
function stopComponentDrag() { function stopComponentDrag() {
// 如果有组件被拖拽,保存当前状态 // 如果有组件被拖拽,仅清除拖拽状态(不保存)
if (draggingComponentId.value) { if (draggingComponentId.value) {
draggingComponentId.value = null; draggingComponentId.value = null;
} }
isComponentDragEventActive.value = false; isComponentDragEventActive.value = false;
// 移除自动保存功能 - 不再自动保存到localStorage
saveDiagramData(diagramData.value);
} }
// 更新组件属性 // 更新组件属性
@@ -977,7 +975,8 @@ function exportDiagram() {
onMounted(async () => { onMounted(async () => {
// 加载图表数据 // 加载图表数据
try { try {
diagramData.value = await loadDiagramData(); // 传入examId参数让diagramManager处理动态加载
diagramData.value = await loadDiagramData(props.examId);
// 预加载所有组件模块 // 预加载所有组件模块
const componentTypes = new Set<string>(); const componentTypes = new Set<string>();

View File

@@ -1,7 +1,6 @@
import { ref, shallowRef, computed, reactive } from "vue"; import { ref, shallowRef, computed, reactive } from "vue";
import { createInjectionState } from "@vueuse/core"; import { createInjectionState } from "@vueuse/core";
import { import {
saveDiagramData,
type DiagramData, type DiagramData,
type DiagramPart, type DiagramPart,
} from "./diagramManager"; } from "./diagramManager";
@@ -302,7 +301,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
// 使用 updateDiagramDataDirectly 避免触发加载状态 // 使用 updateDiagramDataDirectly 避免触发加载状态
canvasInstance.updateDiagramDataDirectly(currentData); canvasInstance.updateDiagramDataDirectly(currentData);
saveDiagramData(currentData); // 移除自动保存功能
console.log("组件添加完成:", newComponent); console.log("组件添加完成:", newComponent);
@@ -431,7 +430,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
"=== 更新图表数据完成,新组件数量:", "=== 更新图表数据完成,新组件数量:",
currentData.parts.length, currentData.parts.length,
); );
saveDiagramData(currentData); // 移除自动保存功能
return { success: true, message: `已添加 ${templateData.name} 模板` }; return { success: true, message: `已添加 ${templateData.name} 模板` };
} else { } else {
@@ -504,7 +503,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
canvasInstance.updateDiagramDataDirectly(currentData); canvasInstance.updateDiagramDataDirectly(currentData);
saveDiagramData(currentData); // 移除自动保存功能
} }
/** /**
@@ -763,11 +762,15 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
function prepareComponentProps( function prepareComponentProps(
attrs: Record<string, any>, attrs: Record<string, any>,
componentId?: string, componentId?: string,
examId?: string,
): Record<string, any> { ): Record<string, any> {
const result: Record<string, any> = { ...attrs }; const result: Record<string, any> = { ...attrs };
if (componentId) { if (componentId) {
result.componentId = componentId; result.componentId = componentId;
} }
if (examId) {
result.examId = examId;
}
return result; return result;
} }

View File

@@ -26,6 +26,8 @@ export interface DiagramPart {
// 连接类型定义 - 使用元组类型表示四元素数组 // 连接类型定义 - 使用元组类型表示四元素数组
export type ConnectionArray = [string, string, number, string[]]; export type ConnectionArray = [string, string, number, string[]];
import { AuthManager } from '@/utils/AuthManager';
// 解析连接字符串为组件ID和引脚ID // 解析连接字符串为组件ID和引脚ID
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } { export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
const [componentId, pinId] = connectionPin.split(':'); const [componentId, pinId] = connectionPin.split(':');
@@ -80,22 +82,62 @@ export interface WireItem {
showLabel: boolean; showLabel: boolean;
} }
// 从本地存储加载图表数据 // 从本地存储或动态API加载图表数据
export async function loadDiagramData(): Promise<DiagramData> { export async function loadDiagramData(examId?: string): Promise<DiagramData> {
try { try {
// 先尝试从本地存储加载 // 如果提供了examId优先从API加载实验的diagram
const savedData = localStorage.getItem('diagramData'); if (examId) {
if (savedData) { try {
return JSON.parse(savedData); const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 获取diagram类型的资源列表
const resources = await resourceClient.getResourceList(examId, 'canvas', 'template');
if (resources && resources.length > 0) {
// 获取第一个diagram资源
const diagramResource = resources[0];
// 使用动态API获取资源文件内容
const response = await resourceClient.getResourceById(diagramResource.id);
if (response && response.data) {
const text = await response.data.text();
const data = JSON.parse(text);
// 验证数据格式
const validation = validateDiagramData(data);
if (validation.isValid) {
console.log('成功从API加载实验diagram:', examId);
return data;
} else {
console.warn('API返回的diagram数据格式无效:', validation.errors);
}
}
} else {
console.log('未找到实验diagram资源使用默认加载方式');
}
} catch (error) {
console.warn('从API加载实验diagram失败使用默认加载方式:', error);
}
} }
// 如果本地存储没有,从文件加载 // 如果没有examId或API加载失败尝试从静态文件加载不再使用本地存储
// 从静态文件加载(作为备选方案)
const response = await fetch('/src/components/diagram.json'); const response = await fetch('/src/components/diagram.json');
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to load diagram.json: ${response.statusText}`); throw new Error(`Failed to load diagram.json: ${response.statusText}`);
} }
const data = await response.json(); const data = await response.json();
// 验证静态文件数据
const validation = validateDiagramData(data);
if (validation.isValid) {
return data; return data;
} else {
console.warn('静态diagram文件数据格式无效:', validation.errors);
throw new Error('所有diagram数据源都无效');
}
} catch (error) { } catch (error) {
console.error('Error loading diagram data:', error); console.error('Error loading diagram data:', error);
// 返回空的默认数据结构 // 返回空的默认数据结构
@@ -114,13 +156,10 @@ export function createEmptyDiagram(): DiagramData {
}; };
} }
// 保存图表数据本地存储 // 保存图表数据(已禁用本地存储
export function saveDiagramData(data: DiagramData): void { export function saveDiagramData(data: DiagramData): void {
try { // 本地存储功能已禁用 - 不再保存到localStorage
localStorage.setItem('diagramData', JSON.stringify(data)); console.debug('saveDiagramData called but localStorage saving is disabled');
} catch (error) {
console.error('Error saving diagram data:', error);
}
} }
// 更新组件位置 // 更新组件位置

View File

@@ -9,6 +9,7 @@ import {
SignalOperator, SignalOperator,
SignalTriggerConfig, SignalTriggerConfig,
SignalValue, SignalValue,
AnalyzerChannelDiv,
} from "@/APIClient"; } from "@/APIClient";
import { AuthManager } from "@/utils/AuthManager"; import { AuthManager } from "@/utils/AuthManager";
import { useAlertStore } from "@/components/Alert"; import { useAlertStore } from "@/components/Alert";
@@ -65,6 +66,39 @@ const signalValues = [
{ value: SignalValue.SomeNumber, label: "#" }, { value: SignalValue.SomeNumber, label: "#" },
]; ];
// 通道组选项
const channelDivOptions = [
{ value: 1, label: "1通道", description: "启用1个通道 (CH0)" },
{ value: 2, label: "2通道", description: "启用2个通道 (CH0-CH1)" },
{ value: 4, label: "4通道", description: "启用4个通道 (CH0-CH3)" },
{ value: 8, label: "8通道", description: "启用8个通道 (CH0-CH7)" },
{ value: 16, label: "16通道", description: "启用16个通道 (CH0-CH15)" },
{ value: 32, label: "32通道", description: "启用32个通道 (CH0-CH31)" },
];
// 捕获深度选项
const captureLengthOptions = [
{ value: 256, label: "256" },
{ value: 512, label: "512" },
{ value: 1024, label: "1K" },
{ value: 2048, label: "2K" },
{ value: 4096, label: "4K" },
{ value: 8192, label: "8K" },
{ value: 16384, label: "16K" },
{ value: 32768, label: "32K" },
];
// 预捕获深度选项
const preCaptureLengthOptions = [
{ value: 0, label: "0" },
{ value: 16, label: "16" },
{ value: 32, label: "32" },
{ value: 64, label: "64" },
{ value: 128, label: "128" },
{ value: 256, label: "256" },
{ value: 512, label: "512" },
];
// 默认颜色数组 // 默认颜色数组
const defaultColors = [ const defaultColors = [
"#FF5733", "#FF5733",
@@ -78,7 +112,7 @@ const defaultColors = [
]; ];
// 添加逻辑分析仪频率常量 // 添加逻辑分析仪频率常量
const LOGIC_ANALYZER_FREQUENCY = 5_000_000; // 5MHz const LOGIC_ANALYZER_FREQUENCY = 125_000_000; // 125MHz
const SAMPLE_PERIOD_NS = 1_000_000_000 / LOGIC_ANALYZER_FREQUENCY; // 采样周期,单位:纳秒 const SAMPLE_PERIOD_NS = 1_000_000_000 / LOGIC_ANALYZER_FREQUENCY; // 采样周期,单位:纳秒
const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
@@ -91,22 +125,25 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
// 触发设置相关状态 // 触发设置相关状态
const currentGlobalMode = ref<GlobalCaptureMode>(GlobalCaptureMode.AND); const currentGlobalMode = ref<GlobalCaptureMode>(GlobalCaptureMode.AND);
const currentChannelDiv = ref<number>(8); // 默认启用8个通道
const captureLength = ref<number>(1024); // 捕获深度默认1024
const preCaptureLength = ref<number>(0); // 预捕获深度默认0
const isApplying = ref(false); const isApplying = ref(false);
const isCapturing = ref(false); // 添加捕获状态标识 const isCapturing = ref(false); // 添加捕获状态标识
// 通道配置 // 通道配置
const channels = reactive<Channel[]>( const channels = reactive<Channel[]>(
Array.from({ length: 8 }, (_, index) => ({ Array.from({ length: 32 }, (_, index) => ({
enabled: false, enabled: index < 8, // 默认启用前8个通道
label: `CH${index}`, label: `CH${index}`,
color: defaultColors[index], color: defaultColors[index % defaultColors.length], // 使用模运算避免数组越界
})), })),
); );
// 8个信号通道的配置 // 32个信号通道的配置
const signalConfigs = reactive<SignalTriggerConfig[]>( const signalConfigs = reactive<SignalTriggerConfig[]>(
Array.from( Array.from(
{ length: 8 }, { length: 32 },
(_, index) => (_, index) =>
new SignalTriggerConfig({ new SignalTriggerConfig({
signalIndex: index, signalIndex: index,
@@ -131,101 +168,52 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
channels.filter((channel) => channel.enabled), channels.filter((channel) => channel.enabled),
); );
const enableAllChannels = () => { // 转换通道数字到枚举值
channels.forEach((channel) => { const getChannelDivEnum = (channelCount: number): AnalyzerChannelDiv => {
channel.enabled = true; switch (channelCount) {
}); case 1: return AnalyzerChannelDiv.ONE;
case 2: return AnalyzerChannelDiv.TWO;
case 4: return AnalyzerChannelDiv.FOUR;
case 8: return AnalyzerChannelDiv.EIGHT;
case 16: return AnalyzerChannelDiv.XVI;
case 32: return AnalyzerChannelDiv.XXXII;
default: return AnalyzerChannelDiv.EIGHT;
}
}; };
const disableAllChannels = () => { // 设置通道组
const setChannelDiv = (channelCount: number) => {
// 验证通道数量是否有效
if (!channelDivOptions.find(option => option.value === channelCount)) {
console.error(`无效的通道组设置: ${channelCount}`);
return;
}
currentChannelDiv.value = channelCount;
// 禁用所有通道
channels.forEach((channel) => { channels.forEach((channel) => {
channel.enabled = false; channel.enabled = false;
}); });
};
const setGlobalMode = async (mode: GlobalCaptureMode) => { // 启用指定数量的通道从CH0开始
// 检查是否有其他操作正在进行 for (let i = 0; i < channelCount && i < channels.length; i++) {
if (operationMutex.isLocked()) { channels[i].enabled = true;
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
return;
} }
const release = await operationMutex.acquire(); const option = channelDivOptions.find(opt => opt.value === channelCount);
try { alert?.success(`已设置为${option?.label}`, 2000);
const client = AuthManager.createAuthenticatedLogicAnalyzerClient(); };
const success = await client.setGlobalTrigMode(mode);
if (success) { const setGlobalMode = (mode: GlobalCaptureMode) => {
currentGlobalMode.value = mode; currentGlobalMode.value = mode;
alert?.success( const modeOption = globalModes.find((m) => m.value === mode);
`全局触发模式已设置为 ${globalModes.find((m) => m.value === mode)?.label}`, alert?.info(`全局触发模式已设置为 ${modeOption?.label}`, 2000);
3000,
);
} else {
throw new Error("设置失败");
}
} catch (error) {
console.error("设置全局触发模式失败:", error);
alert?.error("设置全局触发模式失败", 3000);
} finally {
release();
}
};
const applyConfiguration = async () => {
// 检查是否有其他操作正在进行
if (operationMutex.isLocked()) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
return;
}
const release = await operationMutex.acquire();
isApplying.value = true;
try {
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
// 准备配置数据 - 只包含启用的通道
const enabledSignals = signalConfigs.filter(
(signal, index) => channels[index].enabled,
);
const config = new CaptureConfig({
globalMode: currentGlobalMode.value,
signalConfigs: enabledSignals,
});
// 发送配置
const success = await client.configureCapture(config);
if (success) {
const enabledChannelCount = channels.filter(
(ch) => ch.enabled,
).length;
alert?.success(
`配置已成功应用,启用了 ${enabledChannelCount} 个通道和触发条件`,
3000,
);
} else {
throw new Error("应用配置失败");
}
} catch (error) {
console.error("应用配置失败:", error);
alert?.error("应用配置失败,请检查设备连接", 3000);
} finally {
isApplying.value = false;
release();
}
}; };
const resetConfiguration = () => { const resetConfiguration = () => {
currentGlobalMode.value = GlobalCaptureMode.AND; currentGlobalMode.value = GlobalCaptureMode.AND;
currentChannelDiv.value = 8; // 重置为默认的8通道
channels.forEach((channel, index) => { setChannelDiv(8); // 重置为默认的8通道
channel.enabled = false;
channel.label = `CH${index}`;
channel.color = defaultColors[index];
});
signalConfigs.forEach((signal) => { signalConfigs.forEach((signal) => {
signal.operator = SignalOperator.Equal; signal.operator = SignalOperator.Equal;
@@ -243,28 +231,120 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
const getCaptureData = async () => { const getCaptureData = async () => {
try { try {
const client = AuthManager.createAuthenticatedLogicAnalyzerClient(); const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
// 3. 获取捕获数据 // 获取捕获数据,使用当前设置的捕获长度
const base64Data = await client.getCaptureData(); const base64Data = await client.getCaptureData(captureLength.value);
// 4. 将base64数据转换为bytes // 将base64数据转换为bytes
const binaryString = atob(base64Data); const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length); const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) { for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i); bytes[i] = binaryString.charCodeAt(i);
} }
// 5. 解析数据为8个通道的数字信号 // 根据当前通道数量解析数据
const sampleCount = bytes.length; const channelCount = currentChannelDiv.value;
const timeStepNs = SAMPLE_PERIOD_NS; // 每个采样点间隔200ns (1/5MHz) const timeStepNs = SAMPLE_PERIOD_NS;
// 创建时间轴(转换为合适的单位) let sampleCount: number;
const x = Array.from( let x: number[];
let y: number[][];
if (channelCount === 1) {
// 1通道每个字节包含8个时间单位的数据
sampleCount = bytes.length * 8;
// 创建时间轴
x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建通道数据数组
y = Array.from(
{ length: 1 },
() => new Array(sampleCount),
);
// 解析数据每个字节的8个位对应8个时间单位
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
const byte = bytes[byteIndex];
for (let bitIndex = 0; bitIndex < 8; bitIndex++) {
const timeIndex = byteIndex * 8 + bitIndex;
y[0][timeIndex] = (byte >> bitIndex) & 1;
}
}
} else if (channelCount === 2) {
// 2通道每个字节包含4个时间单位的数据
sampleCount = bytes.length * 4;
// 创建时间轴
x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建通道数据数组
y = Array.from(
{ length: 2 },
() => new Array(sampleCount),
);
// 解析数据每个字节的8个位对应4个时间单位的2通道数据
// 位分布:[T3_CH1, T3_CH0, T2_CH1, T2_CH0, T1_CH1, T1_CH0, T0_CH1, T0_CH0]
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
const byte = bytes[byteIndex];
for (let timeUnit = 0; timeUnit < 4; timeUnit++) {
const timeIndex = byteIndex * 4 + timeUnit;
const bitOffset = timeUnit * 2;
y[0][timeIndex] = (byte >> bitOffset) & 1; // CH0
y[1][timeIndex] = (byte >> (bitOffset + 1)) & 1; // CH1
}
}
} else if (channelCount === 4) {
// 4通道每个字节包含2个时间单位的数据
sampleCount = bytes.length * 2;
// 创建时间轴
x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建通道数据数组
y = Array.from(
{ length: 4 },
() => new Array(sampleCount),
);
// 解析数据每个字节的8个位对应2个时间单位的4通道数据
// 位分布:[T1_CH3, T1_CH2, T1_CH1, T1_CH0, T0_CH3, T0_CH2, T0_CH1, T0_CH0]
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
const byte = bytes[byteIndex];
// 处理第一个时间单位低4位
const timeIndex1 = byteIndex * 2;
for (let channel = 0; channel < 4; channel++) {
y[channel][timeIndex1] = (byte >> channel) & 1;
}
// 处理第二个时间单位高4位
const timeIndex2 = byteIndex * 2 + 1;
for (let channel = 0; channel < 4; channel++) {
y[channel][timeIndex2] = (byte >> (channel + 4)) & 1;
}
}
} else if (channelCount === 8) {
// 8通道每个字节包含1个时间单位的8个通道数据
sampleCount = bytes.length;
// 创建时间轴
x = Array.from(
{ length: sampleCount }, { length: sampleCount },
(_, i) => (i * timeStepNs) / 1000, (_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒 ); // 转换为微秒
// 创建8个通道的数据 // 创建8个通道的数据
const y: number[][] = Array.from( y = Array.from(
{ length: 8 }, { length: 8 },
() => new Array(sampleCount), () => new Array(sampleCount),
); );
@@ -277,17 +357,97 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
y[channel][i] = (byte >> channel) & 1; y[channel][i] = (byte >> channel) & 1;
} }
} }
} else if (channelCount === 16) {
// 16通道每2个字节包含1个时间单位的16个通道数据
sampleCount = bytes.length / 2;
// 6. 设置逻辑数据 // 创建时间轴
x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建16个通道的数据
y = Array.from(
{ length: 16 },
() => new Array(sampleCount),
);
// 解析数据每2个字节为一个时间单位
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
const byteIndex = timeIndex * 2;
const byte1 = bytes[byteIndex]; // [7:0]
const byte2 = bytes[byteIndex + 1]; // [15:8]
// 处理低8位通道 [7:0]
for (let channel = 0; channel < 8; channel++) {
y[channel][timeIndex] = (byte1 >> channel) & 1;
}
// 处理高8位通道 [15:8]
for (let channel = 0; channel < 8; channel++) {
y[channel + 8][timeIndex] = (byte2 >> channel) & 1;
}
}
} else if (channelCount === 32) {
// 32通道每4个字节包含1个时间单位的32个通道数据
sampleCount = bytes.length / 4;
// 创建时间轴
x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建32个通道的数据
y = Array.from(
{ length: 32 },
() => new Array(sampleCount),
);
// 解析数据每4个字节为一个时间单位
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
const byteIndex = timeIndex * 4;
const byte1 = bytes[byteIndex]; // [7:0]
const byte2 = bytes[byteIndex + 1]; // [15:8]
const byte3 = bytes[byteIndex + 2]; // [23:16]
const byte4 = bytes[byteIndex + 3]; // [31:24]
// 处理 [7:0]
for (let channel = 0; channel < 8; channel++) {
y[channel][timeIndex] = (byte1 >> channel) & 1;
}
// 处理 [15:8]
for (let channel = 0; channel < 8; channel++) {
y[channel + 8][timeIndex] = (byte2 >> channel) & 1;
}
// 处理 [23:16]
for (let channel = 0; channel < 8; channel++) {
y[channel + 16][timeIndex] = (byte3 >> channel) & 1;
}
// 处理 [31:24]
for (let channel = 0; channel < 8; channel++) {
y[channel + 24][timeIndex] = (byte4 >> channel) & 1;
}
}
} else {
throw new Error(`不支持的通道数量: ${channelCount}`);
}
// 设置逻辑数据
const logicData: LogicDataType = { const logicData: LogicDataType = {
x, x,
y, y,
xUnit: "us", // 改为微秒单位 xUnit: "us", // 微秒单位
}; };
setLogicData(logicData); setLogicData(logicData);
} catch (error) { } catch (error) {
console.error("获取捕获数据失败:", error); console.error("获取捕获数据失败:", error);
alert?.error("获取捕获数据失败", 3000);
} }
}; };
@@ -303,7 +463,45 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
try { try {
const client = AuthManager.createAuthenticatedLogicAnalyzerClient(); const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
// 1. 设置捕获模式为开始捕获 // 1. 先应用配置
alert?.info("正在应用配置...", 2000);
// 准备配置数据 - 包含所有32个通道未启用的通道设置为默认值
const allSignals = signalConfigs.map((signal, index) => {
if (channels[index].enabled) {
// 启用的通道使用用户配置的触发条件
return signal;
} else {
// 未启用的通道设置为默认触发条件
return new SignalTriggerConfig({
signalIndex: index,
operator: SignalOperator.Equal,
value: SignalValue.NotCare,
});
}
});
const config = new CaptureConfig({
globalMode: currentGlobalMode.value,
channelDiv: getChannelDivEnum(currentChannelDiv.value),
captureLength: captureLength.value,
preCaptureLength: preCaptureLength.value,
signalConfigs: allSignals,
});
// 发送配置
const configSuccess = await client.configureCapture(config);
if (!configSuccess) {
throw new Error("配置应用失败");
}
const enabledChannelCount = channels.filter((ch) => ch.enabled).length;
alert?.success(
`配置已应用,启用了 ${enabledChannelCount} 个通道,捕获深度: ${captureLength.value}`,
2000,
);
// 2. 设置捕获模式为开始捕获
const captureStarted = await client.setCaptureMode(true, false); const captureStarted = await client.setCaptureMode(true, false);
if (!captureStarted) { if (!captureStarted) {
throw new Error("无法启动捕获"); throw new Error("无法启动捕获");
@@ -311,7 +509,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
alert?.info("开始捕获信号...", 2000); alert?.info("开始捕获信号...", 2000);
// 2. 轮询捕获状态 // 3. 轮询捕获状态
let captureCompleted = false; let captureCompleted = false;
while (isCapturing.value) { while (isCapturing.value) {
const status = await client.getCaptureStatus(); const status = await client.getCaptureStatus();
@@ -390,8 +588,11 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
}; };
const forceCapture = async () => { const forceCapture = async () => {
// 设置捕获状态为false这会使轮询停止 // 检查是否正在捕获
isCapturing.value = false; if (!isCapturing.value) {
alert.warn("当前没有正在进行的捕获操作", 2000);
return;
}
const release = await operationMutex.acquire(); const release = await operationMutex.acquire();
try { try {
@@ -404,7 +605,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
} }
await getCaptureData(); await getCaptureData();
alert.success(`捕获完成!`, 3000); alert.success(`强制捕获完成!`, 3000);
} catch (error) { } catch (error) {
console.error("强制捕获失败:", error); console.error("强制捕获失败:", error);
alert.error( alert.error(
@@ -487,7 +688,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
}); });
// 设置逻辑数据 // 设置逻辑数据
enableAllChannels(); setChannelDiv(8);
setLogicData({ x, y, xUnit: "us" }); // 改为微秒单位 setLogicData({ x, y, xUnit: "us" }); // 改为微秒单位
alert?.success("测试数据生成成功", 2000); alert?.success("测试数据生成成功", 2000);
@@ -499,6 +700,9 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
// 触发设置状态 // 触发设置状态
currentGlobalMode, currentGlobalMode,
currentChannelDiv, // 导出当前通道组状态
captureLength, // 导出捕获深度
preCaptureLength, // 导出预捕获深度
isApplying, isApplying,
isCapturing, // 导出捕获状态 isCapturing, // 导出捕获状态
isOperationInProgress, // 导出操作进行状态 isOperationInProgress, // 导出操作进行状态
@@ -512,12 +716,13 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
globalModes, globalModes,
operators, operators,
signalValues, signalValues,
channelDivOptions, // 导出通道组选项
captureLengthOptions, // 导出捕获深度选项
preCaptureLengthOptions, // 导出预捕获深度选项
// 触发设置方法 // 触发设置方法
enableAllChannels, setChannelDiv, // 导出设置通道组方法
disableAllChannels,
setGlobalMode, setGlobalMode,
applyConfiguration,
resetConfiguration, resetConfiguration,
setLogicData, setLogicData,
startCapture, startCapture,

View File

@@ -21,60 +21,7 @@
<h3 class="text-xl font-semibold text-slate-600 mb-2"> <h3 class="text-xl font-semibold text-slate-600 mb-2">
暂无逻辑分析数据 暂无逻辑分析数据
</h3> </h3>
<p class="text-sm text-slate-500">点击下方按钮生成测试数据用于观察</p>
</div> </div>
<!-- <button
class="group relative px-8 py-3 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 ease-in-out focus:outline-none focus:ring-4 focus:ring-blue-300 active:scale-95"
@click="analyzer.generateTestData"
>
<span class="flex items-center gap-2">
<RefreshCcw
class="w-5 h-5 group-hover:rotate-180 transition-transform duration-300"
/>
生成测试数据
</span>
<div
class="absolute inset-0 bg-white opacity-0 group-hover:opacity-20 rounded-lg transition-opacity duration-200"
></div>
</button> -->
<button
class="group relative px-8 py-3 bg-gradient-to-r text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 ease-in-out focus:outline-none focus:ring-4 active:scale-95"
:class="{
'from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 focus:ring-blue-300':
!analyzer.isCapturing.value,
'from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 focus:ring-red-300':
analyzer.isCapturing.value,
}"
@click="
analyzer.isCapturing.value
? analyzer.stopCapture()
: analyzer.startCapture()
"
>
<span class="flex items-center gap-2">
<template v-if="analyzer.isCapturing.value">
<Square class="w-5 h-5" />
停止捕获
</template>
<template v-else>
<Play class="w-5 h-5" />
开始捕获
</template>
</span>
</button>
<!-- 强制捕获按钮 - 只在正在捕获时显示 -->
<button
v-if="analyzer.isCapturing.value"
class="group relative px-8 py-3 bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 ease-in-out focus:outline-none focus:ring-4 focus:ring-orange-300 active:scale-95"
@click="analyzer.forceCapture()"
>
<span class="flex items-center gap-2">
<Square class="w-5 h-5" />
强制捕获
</span>
</button>
</div> </div>
</div> </div>
</template> </template>
@@ -82,7 +29,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, shallowRef } from "vue"; import { computed, shallowRef } from "vue";
import VChart from "vue-echarts"; import VChart from "vue-echarts";
import { RefreshCcw, Play, Square } from "lucide-vue-next";
// Echarts // Echarts
import { use } from "echarts/core"; import { use } from "echarts/core";

View File

@@ -1,38 +1,19 @@
<template> <template>
<div class="space-y-6"> <div class="space-y-6">
<!-- 通道状态概览 -->
<div class="stats stats-horizontal bg-base-100 shadow flex justify-between">
<div class="stat">
<div class="stat-title">总通道数</div>
<div class="stat-value text-primary">8</div>
<div class="stat-desc">逻辑分析仪通道</div>
</div>
<div class="stat">
<div class="stat-title">启用通道</div>
<div class="stat-value text-success">{{ enabledChannelCount }}</div>
<div class="stat-desc">当前激活通道</div>
</div>
<div class="stat">
<div class="stat-title">采样率</div>
<div class="stat-value text-info">5MHz</div>
<div class="stat-desc">最大采样频率</div>
</div>
</div>
<!-- 通道配置 --> <!-- 通道配置 -->
<div class="form-control"> <div class="form-control">
<!-- 全局触发模式选择 --> <!-- 全局触发模式选择和通道组配置 -->
<div class="flex flex-row justify-between my-4 mx-2"> <div class="flex flex-col lg:flex-row justify-between gap-4 my-4 mx-2">
<div class="flex flex-row gap-4"> <!-- 左侧全局触发模式和通道组选择 -->
<div class="flex flex-col lg:flex-row gap-4">
<div class="flex flex-row gap-2 items-center">
<label class="label"> <label class="label">
<span class="label-text text-sm">全局触发逻辑</span> <span class="label-text text-sm">全局触发逻辑</span>
</label> </label>
<select <select
v-model="currentGlobalMode" v-model="currentGlobalMode"
@change="setGlobalMode(currentGlobalMode)" @change="setGlobalMode(currentGlobalMode)"
class="select select-sm select-bordered w-full" class="select select-sm select-bordered"
> >
<option <option
v-for="mode in globalModes" v-for="mode in globalModes"
@@ -44,89 +25,105 @@
</select> </select>
</div> </div>
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-2 items-center">
<button @click="toggleAllChannels" class="btn btn-primary btn-sm"> <label class="label">
{{ enabledChannelCount > 0 ? "全部禁用" : "全部启用" }} <span class="label-text text-sm">通道组</span>
</button> </label>
<button <select
@click="applyConfiguration" v-model="currentChannelDiv"
:disabled="isApplying" @change="setChannelDiv(currentChannelDiv)"
class="btn btn-primary btn-sm" class="select select-sm select-bordered"
> >
<span <option
v-if="isApplying" v-for="option in channelDivOptions"
class="loading loading-spinner loading-sm" :key="option.value"
></span> :value="option.value"
应用配置 >
</button> {{ option.label }}
</option>
</select>
</div>
<div class="flex flex-row gap-2 items-center">
<label class="label">
<span class="label-text text-sm">捕获深度</span>
</label>
<select
v-model="captureLength"
class="select select-sm select-bordered"
>
<option
v-for="option in captureLengthOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
<div class="flex flex-row gap-2 items-center">
<label class="label">
<span class="label-text text-sm">预捕获深度</span>
</label>
<select
v-model="preCaptureLength"
class="select select-sm select-bordered"
>
<option
v-for="option in preCaptureLengthOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
</div>
<!-- 右侧操作按钮 -->
<div class="flex flex-row gap-2">
<button @click="resetConfiguration" class="btn btn-outline btn-sm"> <button @click="resetConfiguration" class="btn btn-outline btn-sm">
重置 重置配置
</button> </button>
</div> </div>
</div> </div>
<!-- 通道列表 --> <!-- 通道列表 -->
<div class="space-y-2"> <div class="space-y-2">
<!-- 表头 - 小屏幕单列时显示 --> <!-- 表头 -->
<div <div
class="flex items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium lg:hidden" class="flex items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium"
> >
<span class="w-16">通道</span> <span class="w-16">通道</span>
<span class="w-20">启用/触发</span>
<span class="w-32">标签</span> <span class="w-32">标签</span>
<span class="w-16">颜色</span> <span class="w-16">颜色</span>
<span class="w-32">触发操作</span> <span class="w-32">触发操作</span>
<span class="w-32">触发值</span> <span class="w-32">触发值</span>
</div> </div>
<!-- 通道配置网格 --> <!-- 通道配置网格 - 根据当前通道组动态显示 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
<!-- 左列 (CH0-CH3) -->
<div class="space-y-2">
<!-- 左列表头 - 大屏幕时显示 -->
<div <div
class="hidden lg:flex items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium" v-for="(channel, index) in channels.filter(ch => ch.enabled)"
>
<span class="w-16">通道</span>
<span class="w-20">启用</span>
<span class="w-32">标签</span>
<span class="w-16">颜色</span>
<span class="w-32">触发操作</span>
<span class="w-32">触发值</span>
</div>
<!-- 左列通道 (0-3) -->
<div
v-for="(channel, index) in channels.slice(0, 4)"
:key="index" :key="index"
class="flex items-center gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors" class="flex items-center gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
> >
<!-- 通道编号和颜色指示 --> <!-- 通道编号和颜色指示 -->
<div class="flex items-center gap-2 w-16"> <div class="flex items-center gap-2 w-16">
<span class="font-mono font-medium">CH{{ index }}</span> <span class="font-mono font-medium">CH{{ channels.indexOf(channel) }}</span>
<div <div
class="w-3 h-3 rounded-full border-2 border-white shadow-sm" class="w-3 h-3 rounded-full border-2 border-white shadow-sm"
:style="{ backgroundColor: channel.color }" :style="{ backgroundColor: channel.color }"
></div> ></div>
</div> </div>
<!-- 通道启用开关同时控制触发 -->
<div class="form-control w-20">
<input
type="checkbox"
v-model="channel.enabled"
class="toggle toggle-sm toggle-primary"
/>
</div>
<!-- 通道标签 --> <!-- 通道标签 -->
<div class="form-control w-32"> <div class="form-control w-32">
<input <input
type="text" type="text"
v-model="channel.label" v-model="channel.label"
:placeholder="`通道 ${index}`" :placeholder="`通道 ${channels.indexOf(channel)}`"
class="input input-sm input-bordered w-full" class="input input-sm input-bordered w-full"
:disabled="!channel.enabled"
/> />
</div> </div>
@@ -136,15 +133,13 @@
type="color" type="color"
v-model="channel.color" v-model="channel.color"
class="w-8 h-8 rounded border-2 border-base-300 cursor-pointer" class="w-8 h-8 rounded border-2 border-base-300 cursor-pointer"
:disabled="!channel.enabled"
/> />
</div> </div>
<!-- 触发操作符选择 --> <!-- 触发操作符选择 -->
<select <select
v-model="signalConfigs[index].operator" v-model="signalConfigs[channels.indexOf(channel)].operator"
class="select select-sm select-bordered w-32" class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
> >
<option <option
v-for="op in operators" v-for="op in operators"
@@ -157,9 +152,8 @@
<!-- 触发信号值选择 --> <!-- 触发信号值选择 -->
<select <select
v-model="signalConfigs[index].value" v-model="signalConfigs[channels.indexOf(channel)].value"
class="select select-sm select-bordered w-32" class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
> >
<option <option
v-for="val in signalValues" v-for="val in signalValues"
@@ -172,174 +166,10 @@
</div> </div>
</div> </div>
<!-- 右列 (CH4-CH7) - 仅在大屏幕显 --> <!-- 当没有启用通道时的提 -->
<div class="hidden lg:block space-y-2"> <div v-if="enabledChannelCount === 0" class="text-center py-8 text-base-content/60">
<!-- 右列表头 --> <p class="text-lg font-medium">未启用任何通道</p>
<div <p class="text-sm">请选择通道组来配置逻辑分析仪</p>
class="flex items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium"
>
<span class="w-16">通道</span>
<span class="w-20">启用/触发</span>
<span class="w-32">标签</span>
<span class="w-16">颜色</span>
<span class="w-32">触发操作</span>
<span class="w-32">触发值</span>
</div>
<!-- 右列通道 (4-7) -->
<div
v-for="(channel, index) in channels.slice(4, 8)"
:key="index + 4"
class="flex items-center gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
>
<!-- 通道编号和颜色指示 -->
<div class="flex items-center gap-2 w-16">
<span class="font-mono font-medium">CH{{ index + 4 }}</span>
<div
class="w-3 h-3 rounded-full border-2 border-white shadow-sm"
:style="{ backgroundColor: channel.color }"
></div>
</div>
<!-- 通道启用开关同时控制触发 -->
<div class="form-control w-20">
<input
type="checkbox"
v-model="channel.enabled"
class="toggle toggle-sm toggle-primary"
/>
</div>
<!-- 通道标签 -->
<div class="form-control w-32">
<input
type="text"
v-model="channel.label"
:placeholder="`通道 ${index + 4}`"
class="input input-sm input-bordered w-full"
:disabled="!channel.enabled"
/>
</div>
<!-- 颜色选择 -->
<div class="form-control w-16">
<input
type="color"
v-model="channel.color"
class="w-8 h-8 rounded border-2 border-base-300 cursor-pointer"
:disabled="!channel.enabled"
/>
</div>
<!-- 触发操作符选择 -->
<select
v-model="signalConfigs[index + 4].operator"
class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
>
<option
v-for="op in operators"
:key="op.value"
:value="op.value"
>
{{ op.label }}
</option>
</select>
<!-- 触发信号值选择 -->
<select
v-model="signalConfigs[index + 4].value"
class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
>
<option
v-for="val in signalValues"
:key="val.value"
:value="val.value"
>
{{ val.label }}
</option>
</select>
</div>
</div>
<!-- 小屏幕时继续显示 CH4-CH7 -->
<div class="lg:hidden space-y-2">
<div
v-for="(channel, index) in channels.slice(4, 8)"
:key="index + 4"
class="flex items-center gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
>
<!-- 通道编号和颜色指示 -->
<div class="flex items-center gap-2 w-16">
<span class="font-mono font-medium">CH{{ index + 4 }}</span>
<div
class="w-3 h-3 rounded-full border-2 border-white shadow-sm"
:style="{ backgroundColor: channel.color }"
></div>
</div>
<!-- 通道启用开关同时控制触发 -->
<div class="form-control w-20">
<input
type="checkbox"
v-model="channel.enabled"
class="toggle toggle-sm toggle-primary"
/>
</div>
<!-- 通道标签 -->
<div class="form-control w-32">
<input
type="text"
v-model="channel.label"
:placeholder="`通道 ${index + 4}`"
class="input input-sm input-bordered w-full"
:disabled="!channel.enabled"
/>
</div>
<!-- 颜色选择 -->
<div class="form-control w-16">
<input
type="color"
v-model="channel.color"
class="w-8 h-8 rounded border-2 border-base-300 cursor-pointer"
:disabled="!channel.enabled"
/>
</div>
<!-- 触发操作符选择 -->
<select
v-model="signalConfigs[index + 4].operator"
class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
>
<option
v-for="op in operators"
:key="op.value"
:value="op.value"
>
{{ op.label }}
</option>
</select>
<!-- 触发信号值选择 -->
<select
v-model="signalConfigs[index + 4].value"
class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
>
<option
v-for="val in signalValues"
:key="val.value"
:value="val.value"
>
{{ val.label }}
</option>
</select>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -352,6 +182,9 @@ import { useLogicAnalyzerState } from "./LogicAnalyzerManager";
const { const {
currentGlobalMode, currentGlobalMode,
currentChannelDiv,
captureLength,
preCaptureLength,
isApplying, isApplying,
channels, channels,
signalConfigs, signalConfigs,
@@ -359,18 +192,11 @@ const {
globalModes, globalModes,
operators, operators,
signalValues, signalValues,
enableAllChannels, channelDivOptions,
disableAllChannels, captureLengthOptions,
preCaptureLengthOptions,
setChannelDiv,
setGlobalMode, setGlobalMode,
applyConfiguration,
resetConfiguration, resetConfiguration,
} = useRequiredInjection(useLogicAnalyzerState); } = useRequiredInjection(useLogicAnalyzerState);
const toggleAllChannels = () => {
if (enabledChannelCount.value > 0) {
disableAllChannels();
} else {
enableAllChannels();
}
};
</script> </script>

View File

@@ -6,6 +6,7 @@ import hljs from 'highlight.js';
import 'highlight.js/styles/github.css'; // 亮色主题 import 'highlight.js/styles/github.css'; // 亮色主题
// 导入主题存储 // 导入主题存储
import { useThemeStore } from '@/stores/theme'; import { useThemeStore } from '@/stores/theme';
import { AuthManager } from '@/utils/AuthManager';
const props = defineProps({ const props = defineProps({
content: { content: {
@@ -15,6 +16,10 @@ const props = defineProps({
removeFirstH1: { removeFirstH1: {
type: Boolean, type: Boolean,
default: false default: false
},
examId: {
type: String,
default: ''
} }
}); });
@@ -23,6 +28,42 @@ const themeStore = useThemeStore();
// 使用 isDarkTheme 函数来检查当前是否为暗色主题 // 使用 isDarkTheme 函数来检查当前是否为暗色主题
const isDarkMode = computed(() => themeStore.isDarkTheme()); const isDarkMode = computed(() => themeStore.isDarkTheme());
// 图片资源缓存
const imageResourceCache = ref<Map<string, string>>(new Map());
// 获取图片资源ID的函数
async function getImageResourceId(examId: string, imagePath: string): Promise<string | null> {
try {
const client = AuthManager.createAuthenticatedResourceClient();
const resources = await client.getResourceList(examId, 'images', 'template');
// 查找匹配的图片资源
const imageResource = resources.find(r => r.name === imagePath || r.name.endsWith(imagePath));
return imageResource ? imageResource.id.toString() : null;
} catch (error) {
console.error('获取图片资源ID失败:', error);
return null;
}
}
// 通过资源ID获取图片数据URL
async function getImageDataUrl(resourceId: string): Promise<string | null> {
try {
const client = AuthManager.createAuthenticatedResourceClient();
const response = await client.getResourceById(parseInt(resourceId));
if (response && response.data) {
return URL.createObjectURL(response.data);
}
return null;
} catch (error) {
console.error('获取图片数据失败:', error);
return null;
}
}
// 监听主题变化 // 监听主题变化
watch(() => themeStore.currentTheme, () => { watch(() => themeStore.currentTheme, () => {
// 主题变化时更新代码高亮样式 // 主题变化时更新代码高亮样式
@@ -54,6 +95,27 @@ const renderedContent = computed(() => {
// 创建自定义渲染器 // 创建自定义渲染器
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
// 重写图片渲染方法,处理相对路径
renderer.image = (href, title, text) => {
let src = href;
console.log(`原始图片路径: ${href}, examId: ${props.examId}`);
// 如果是相对路径且有实验ID需要通过动态API获取
if (props.examId && href && href.startsWith('./')) {
// 对于相对路径的图片我们需要先获取图片资源ID然后通过动态API获取
// 暂时保留原始路径,在后处理中进行替换
src = href;
console.log(`保留原始路径用于后处理: ${src}`);
}
const titleAttr = title ? ` title="${title}"` : '';
const altAttr = text ? ` alt="${text}"` : '';
const dataOriginal = href && href.startsWith('./') ? ` data-original-src="${href}"` : '';
console.log(`最终渲染的HTML: <img src="${src}"${titleAttr}${altAttr}${dataOriginal} />`);
return `<img src="${src}"${titleAttr}${altAttr}${dataOriginal} />`;
};
// 重写代码块渲染方法,添加语言信息 // 重写代码块渲染方法,添加语言信息
renderer.code = (code, incomingLanguage) => { renderer.code = (code, incomingLanguage) => {
// 确保语言参数是字符串 // 确保语言参数是字符串
@@ -67,14 +129,60 @@ const renderedContent = computed(() => {
return `<pre class="hljs" data-language="${validLanguage}"><code class="language-${validLanguage}">${highlightedCode}</code></pre>`; return `<pre class="hljs" data-language="${validLanguage}"><code class="language-${validLanguage}">${highlightedCode}</code></pre>`;
}; };
// 设置 marked 选项 // 设置 marked 选项并解析内容
marked.use({ let html = marked.parse(processedContent, {
renderer: renderer, renderer: renderer,
gfm: true, gfm: true,
breaks: true breaks: true
}); }) as string;
return marked(processedContent); // 后处理HTML异步处理图片
if (props.examId) {
// 查找所有需要处理的图片
const imgMatches = Array.from(html.matchAll(/(<img[^>]+data-original-src=["'])\.\/([^"']+)(["'][^>]*>)/g));
// 异步处理每个图片
imgMatches.forEach(async (match) => {
const [fullMatch, prefix, path, suffix] = match;
const imagePath = path.replace('images/', '');
// 检查缓存
if (imageResourceCache.value.has(imagePath)) {
const cachedUrl = imageResourceCache.value.get(imagePath)!;
html = html.replace(fullMatch, `${prefix}${cachedUrl}${suffix.replace(' data-original-src="./'+path+'"', '')}`);
return;
}
try {
// 获取图片资源ID
const resourceId = await getImageResourceId(props.examId, imagePath);
if (resourceId) {
// 获取图片数据URL
const dataUrl = await getImageDataUrl(resourceId);
if (dataUrl) {
// 缓存URL
imageResourceCache.value.set(imagePath, dataUrl);
// 更新HTML中的图片src
const updatedHtml = html.replace(fullMatch, `${prefix}${dataUrl}${suffix.replace(' data-original-src="./'+path+'"', '')}`);
// 触发重新渲染
setTimeout(() => {
const imgElements = document.querySelectorAll(`img[data-original-src="./${path}"]`);
imgElements.forEach(img => {
(img as HTMLImageElement).src = dataUrl;
img.removeAttribute('data-original-src');
});
}, 0);
}
}
} catch (error) {
console.error(`处理图片 ${imagePath} 失败:`, error);
}
});
}
return html;
}); });
// 页面挂载后,确保应用正确的主题样式 // 页面挂载后,确保应用正确的主题样式

View File

@@ -31,6 +31,12 @@
工程界面 工程界面
</router-link> </router-link>
</li> </li>
<li class="my-1 hover:translate-x-1 transition-all duration-300">
<router-link to="/exam" class="text-base font-medium">
<FileText class="icon" />
实验列表
</router-link>
</li>
<li class="my-1 hover:translate-x-1 transition-all duration-300"> <li class="my-1 hover:translate-x-1 transition-all duration-300">
<router-link to="/test" class="text-base font-medium"> <router-link to="/test" class="text-base font-medium">
<FlaskConical class="icon" /> <FlaskConical class="icon" />

View File

@@ -1,11 +0,0 @@
<template>
<button class="btn transition-transform duration-150 ease-in-out hover:scale-120">
Button A
</button>
</template>
<script lang="ts" setup></script>
<style scoped lang="postcss">
@import "@/assets/main.css";
</style>

View File

@@ -1,162 +0,0 @@
<template>
<div
class="card card-dash shadow-xl h-screen"
:class="[sidebar.isClose ? 'w-31' : 'w-80']"
>
<div
class="card-body flex relative transition-all duration-500 ease-in-out"
>
<!-- Avatar and Name -->
<div class="relative" :class="sidebar.isClose ? 'h-50' : 'h-20'">
<!-- Img -->
<div
class="avatar h-10 fixed top-10"
:class="sidebar.isClose ? 'left-10' : 'left-7'"
>
<div class="rounded-full">
<img src="../assets/user.svg" alt="User" />
</div>
</div>
<!-- Text -->
<Transition>
<div v-if="!sidebar.isClose" class="mx-5 grow fixed left-20 top-11">
<label class="text-2xl">用户名</label>
</div>
</Transition>
</div>
<!-- Toggle Button -->
<button
class="btn btn-square rounded-lg p-2 m-3 fixed"
:class="sidebar.isClose ? 'left-7 top-23' : 'left-60 top-7'"
@click="sidebar.toggleSidebar"
>
<svg
t="1741694970690"
:class="sidebar.isClose ? 'rotate-0' : 'rotate-540'"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4546"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="200"
height="200"
>
<path
d="M803.758 514.017c-0.001-0.311-0.013-0.622-0.018-0.933-0.162-23.974-9.386-47.811-27.743-65.903-0.084-0.082-0.172-0.157-0.256-0.239-0.154-0.154-0.296-0.315-0.451-0.468L417.861 94.096c-37.685-37.153-99.034-37.476-136.331-0.718-37.297 36.758-36.979 97.231 0.707 134.384l290.361 286.257-290.362 286.257c-37.685 37.153-38.004 97.625-0.707 134.383 37.297 36.758 98.646 36.435 136.331-0.718l357.43-352.378c0.155-0.153 0.297-0.314 0.451-0.468 0.084-0.082 0.172-0.157 0.256-0.239 18.354-18.089 27.578-41.922 27.743-65.892 0.004-0.315 0.017-0.631 0.018-0.947z"
:fill="theme.isLightTheme() ? '#828282' : '#C0C3C8'"
p-id="4547"
></path>
</svg>
</button>
<div class="divider"></div>
<ul class="menu h-full w-full">
<li v-for="item in props.items" class="text-xl my-1">
<a @click="router.push(item.page)">
<svg
t="1741694797806"
class="icon h-[1.5em] w-[1.5em] opacity-50 mx-1"
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>
<Transition>
<p class="break-keep" v-if="!sidebar.isClose">{{ item.text }}</p>
</Transition>
</a>
</li>
</ul>
<div class="divider"></div>
<ul class="menu w-full">
<li>
<a @click="theme.toggleTheme" class="text-xl">
<ThemeControlButton />
<Transition>
<p v-if="!sidebar.isClose" class="break-keep">改变主题</p>
</Transition>
<Transition>
<ThemeControlToggle v-if="!sidebar.isClose" />
</Transition>
</a>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import iconMenu from "../assets/menu.svg";
import "../router";
import { useThemeStore } from "@/stores/theme";
import { useSidebarStore } from "@/stores/sidebar";
import ThemeControlButton from "./ThemeControlButton.vue";
import ThemeControlToggle from "./ThemeControlToggle.vue";
import router from "../router";
const theme = useThemeStore();
const sidebar = useSidebarStore();
type Item = {
id: number;
icon: string;
text: string;
page: string;
};
interface Props {
items?: Array<Item>;
}
const props = withDefaults(defineProps<Props>(), {
items: () => [
{ id: 1, icon: iconMenu, text: "btn1", page: "/" },
{ id: 2, icon: iconMenu, text: "btn2", page: "/" },
{ id: 3, icon: iconMenu, text: "btn3", page: "/" },
{ id: 4, icon: iconMenu, text: "btn4", page: "/" },
],
});
</script>
<style scoped lang="postcss">
@reference "../assets/main.css";
* {
@apply transition-all duration-500 ease-in-out;
}
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

View File

@@ -31,8 +31,20 @@
<!-- 标题覆盖层 --> <!-- 标题覆盖层 -->
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent"> <div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent">
<div class="flex flex-col gap-2">
<h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3> <h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3>
<p class="text-sm opacity-80 truncate">{{ tutorial.description }}</p> <p class="text-sm opacity-80 truncate">{{ tutorial.description }}</p>
<!-- 标签显示 -->
<div v-if="tutorial.tags && tutorial.tags.length > 0" class="flex flex-wrap gap-1">
<span
v-for="tag in tutorial.tags.slice(0, 3)"
:key="tag"
class="badge badge-outline badge-xs text-xs"
>
{{ tag }}
</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -54,6 +66,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { AuthManager } from '@/utils/AuthManager';
import type { ExamSummary } from '@/APIClient';
// 接口定义 // 接口定义
interface Tutorial { interface Tutorial {
@@ -61,7 +75,7 @@ interface Tutorial {
title: string; title: string;
description: string; description: string;
thumbnail?: string; thumbnail?: string;
docPath: string; tags: string[];
} }
// Props // Props
@@ -81,87 +95,93 @@ let autoRotationTimer: number | null = null;
// 处理卡片点击 // 处理卡片点击
const handleCardClick = (index: number, tutorialId: string) => { const handleCardClick = (index: number, tutorialId: string) => {
if (index === currentIndex.value) { if (index === currentIndex.value) {
goToTutorial(tutorialId); goToExam(tutorialId);
} else { } else {
setActiveCard(index); setActiveCard(index);
} }
}; };
// 从 public/doc 目录加载例程信息 // 从数据库加载实验数据
onMounted(async () => { onMounted(async () => {
try { try {
// 尝试从API获取教程目录 console.log('正在从数据库加载实验数据...');
let tutorialIds: string[] = [];
try { // 创建认证客户端
const response = await fetch('/api/tutorial'); const client = AuthManager.createAuthenticatedExamClient();
if (response.ok) {
const data = await response.json(); // 获取实验列表
tutorialIds = data.tutorials || []; const examList: ExamSummary[] = await client.getExamList();
}
} catch (error) { // 筛选可见的实验并转换为Tutorial格式
console.warn('无法从API获取教程目录使用默认值:', error); const visibleExams = examList
.filter(exam => exam.isVisibleToUsers)
.slice(0, 6); // 限制轮播显示最多6个实验
if (visibleExams.length === 0) {
console.warn('没有找到可见的实验');
return;
} }
// 如果API调用失败或返回空列表使用默认值 // 转换数据格式并获取封面图片
if (tutorialIds.length === 0) { const tutorialPromises = visibleExams.map(async (exam) => {
console.log('使用默认教程列表'); let thumbnail: string | undefined;
tutorialIds = ['01', '02', '03', '04', '05', '06', '11', '12', '13']; // 默认例程
} else {
console.log('使用API获取的教程列表:', tutorialIds);
}
// 为每个例程创建对象并尝试获取文档标题
const tutorialPromises = tutorialIds.map(async (id) => {
// 尝试读取doc.md获取标题
let title = `例程 ${id}`;
let description = "点击加载此例程";
let thumbnail = `/doc/${id}/cover.png`; // 默认使用第一张图片作为缩略图
try { try {
// 尝试读取文档内容获取标题 // 获取实验的封面资源(模板资源)
const response = await fetch(`/doc/${id}/doc.md`); const resourceClient = AuthManager.createAuthenticatedResourceClient();
if (response.ok) { const resourceList = await resourceClient.getResourceList(exam.id, 'cover', 'template');
const text = await response.text(); if (resourceList && resourceList.length > 0) {
// 从Markdown提取标题 // 使用第一个封面资源
const titleMatch = text.match(/^#\s+(.+)$/m); const coverResource = resourceList[0];
if (titleMatch && titleMatch[1]) { const fileResponse = await resourceClient.getResourceById(coverResource.id);
title = titleMatch[1].trim(); // 创建Blob URL作为缩略图
} thumbnail = URL.createObjectURL(fileResponse.data);
// 提取第一段作为描述
const descMatch = text.match(/\n\n([^#\n][^\n]+)/);
if (descMatch && descMatch[1]) {
description = descMatch[1].substring(0, 100).trim();
if (description.length === 100) description += '...';
}
} }
} catch (error) { } catch (error) {
console.warn(`无法读取例程${id}文档内容:`, error); console.warn(`无法获取实验${exam.id}封面图片:`, error);
} }
return { return {
id, id: exam.id,
title, title: exam.name,
description, description: '点击查看实验详情',
thumbnail, thumbnail,
docPath: `/doc/${id}/doc.md` tags: exam.tags || []
}; };
}); });
tutorials.value = await Promise.all(tutorialPromises); tutorials.value = await Promise.all(tutorialPromises);
console.log('成功加载实验数据:', tutorials.value.length, '个实验');
// 启动自动旋转 // 启动自动旋转
startAutoRotation(); startAutoRotation();
} catch (error) { } catch (error) {
console.error('加载例程失败:', error); console.error('加载实验数据失败:', error);
// 如果加载失败,显示默认的占位内容
tutorials.value = [{
id: 'placeholder',
title: '实验数据加载中...',
description: '请稍后或刷新页面重试',
thumbnail: undefined,
tags: []
}];
} }
}); });
// 在组件销毁时清除计时器 // 在组件销毁时清除计时器和Blob URLs
onUnmounted(() => { onUnmounted(() => {
if (autoRotationTimer) { if (autoRotationTimer) {
clearInterval(autoRotationTimer); clearInterval(autoRotationTimer);
} }
// 清理创建的Blob URLs
tutorials.value.forEach(tutorial => {
if (tutorial.thumbnail && tutorial.thumbnail.startsWith('blob:')) {
URL.revokeObjectURL(tutorial.thumbnail);
}
});
}); });
// 鼠标滚轮处理 // 鼠标滚轮处理
@@ -210,12 +230,12 @@ const resumeAutoRotation = () => {
} }
}; };
// 前往例程 // 前往实验
const goToTutorial = (tutorialId: string) => { const goToExam = (examId: string) => {
// 跳转到工程页面,并通过 query 参数传递文档路径 // 跳转到实验列表页面并传递examId参数页面将自动打开对应的实验详情模态框
router.push({ router.push({
path: '/project', path: '/exam',
query: { tutorial: tutorialId } query: { examId: examId }
}); });
}; };

View File

@@ -1,21 +1,64 @@
<template> <template>
<div class="flex flex-col bg-base-100 justify-center items-center"> <div class="flex flex-col bg-base-100 justify-center items-center gap-4">
<!-- Title --> <!-- Title -->
<h1 class="font-bold text-2xl">上传比特流文件</h1> <h1 class="font-bold text-2xl">比特流文件</h1>
<!-- 示例比特流下载区域 (仅在有examId时显示) -->
<div v-if="examId && availableBitstreams.length > 0" class="w-full">
<fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">示例比特流文件</legend>
<div class="space-y-2">
<div v-for="bitstream in availableBitstreams" :key="bitstream.id" class="flex items-center justify-between p-2 border-2 border-base-300 rounded-lg">
<span class="text-sm">{{ bitstream.name }}</span>
<div class="flex gap-2">
<button
@click="downloadExampleBitstream(bitstream)"
class="btn btn-sm btn-secondary"
:disabled="isDownloading || isProgramming"
>
<div v-if="isDownloading">
<span class="loading loading-spinner loading-xs"></span>
下载中...
</div>
<div v-else>
下载示例
</div>
</button>
<button
@click="programExampleBitstream(bitstream)"
class="btn btn-sm btn-primary"
:disabled="isDownloading || isProgramming || !uploadEvent"
>
<div v-if="isProgramming">
<span class="loading loading-spinner loading-xs"></span>
烧录中...
</div>
<div v-else>
直接烧录
</div>
</button>
</div>
</div>
</div>
</fieldset>
</div>
<!-- 分割线 -->
<div v-if="examId && availableBitstreams.length > 0" class="divider"></div>
<!-- Input File --> <!-- Input File -->
<fieldset class="fieldset w-full"> <fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">选择或拖拽上传文件</legend> <legend class="fieldset-legend text-sm">上传自定义比特流文件</legend>
<input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" /> <input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" />
<label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label> <label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
</fieldset> </fieldset>
<!-- Upload Button --> <!-- Upload Button -->
<div class="card-actions w-full"> <div class="card-actions w-full">
<button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading"> <button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading || isProgramming">
<div v-if="isUploading"> <div v-if="isUploading">
<span class="loading loading-spinner"></span> <span class="loading loading-spinner"></span>
下载... 上传...
</div> </div>
<div v-else> <div v-else>
{{ buttonText }} {{ buttonText }}
@@ -27,17 +70,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, useTemplateRef, onMounted } from "vue"; import { computed, ref, useTemplateRef, onMounted } from "vue";
import { AuthManager } from "@/utils/AuthManager"; // Adjust the path based on your project structure
import { useDialogStore } from "@/stores/dialog"; import { useDialogStore } from "@/stores/dialog";
import { isNull, isUndefined } from "lodash"; import { isNull, isUndefined } from "lodash";
interface Props { interface Props {
uploadEvent?: (file: File) => Promise<boolean>; uploadEvent?: (file: File, examId: string) => Promise<number | null>;
downloadEvent?: () => Promise<boolean>; downloadEvent?: (bitstreamId: number) => Promise<boolean>;
maxMemory?: number; maxMemory?: number;
examId?: string; // 新增examId属性
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
maxMemory: 4, maxMemory: 4,
examId: '',
}); });
const emits = defineEmits<{ const emits = defineEmits<{
@@ -47,6 +93,10 @@ const emits = defineEmits<{
const dialog = useDialogStore(); const dialog = useDialogStore();
const isUploading = ref(false); const isUploading = ref(false);
const isDownloading = ref(false);
const isProgramming = ref(false);
const availableBitstreams = ref<{id: number, name: string}[]>([]);
const buttonText = computed(() => { const buttonText = computed(() => {
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载"; return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
}); });
@@ -56,14 +106,97 @@ const bitstream = defineModel("bitstreamFile", {
type: File, type: File,
default: undefined, default: undefined,
}); });
onMounted(() => {
// 初始化时加载示例比特流
onMounted(async () => {
if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) { if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
let fileList = new DataTransfer(); let fileList = new DataTransfer();
fileList.items.add(bitstream.value); fileList.items.add(bitstream.value);
fileInput.value.files = fileList.files; fileInput.value.files = fileList.files;
} }
await loadAvailableBitstreams();
}); });
// 加载可用的比特流文件列表
async function loadAvailableBitstreams() {
console.log('加载可用比特流文件examId:', props.examId);
if (!props.examId) {
availableBitstreams.value = [];
return;
}
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用新的ResourceClient API获取比特流模板资源列表
const resources = await resourceClient.getResourceList(props.examId, 'bitstream', 'template');
availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || [];
} catch (error) {
console.error('加载比特流列表失败:', error);
availableBitstreams.value = [];
}
}
// 下载示例比特流
async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
if (isDownloading.value) return;
isDownloading.value = true;
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用新的ResourceClient API获取资源文件
const response = await resourceClient.getResourceById(bitstream.id);
if (response && response.data) {
// 创建下载链接
const url = URL.createObjectURL(response.data);
const link = document.createElement('a');
link.href = url;
link.download = response.fileName || bitstream.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
dialog.info("示例比特流下载成功");
} else {
dialog.error("下载失败:响应数据为空");
}
} catch (error) {
console.error('下载示例比特流失败:', error);
dialog.error("下载示例比特流失败");
} finally {
isDownloading.value = false;
}
}
// 直接烧录示例比特流
async function programExampleBitstream(bitstream: {id: number, name: string}) {
if (isProgramming.value) return;
isProgramming.value = true;
try {
const resourceClient = AuthManager.createAuthenticatedResourceClient();
if (props.downloadEvent) {
const downloadSuccess = await props.downloadEvent(bitstream.id);
if (downloadSuccess) {
dialog.info("示例比特流烧录成功");
} else {
dialog.error("烧录失败");
}
} else {
dialog.info("示例比特流props.downloadEvent未定义 无法烧录");
}
} catch (error) {
console.error('烧录示例比特流失败:', error);
dialog.error("烧录示例比特流失败");
} finally {
isProgramming.value = false;
}
}
function handleFileChange(event: Event): void { function handleFileChange(event: Event): void {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const file = target.files?.[0]; // 获取选中的第一个文件 const file = target.files?.[0]; // 获取选中的第一个文件
@@ -85,6 +218,7 @@ function checkFile(file: File): boolean {
} }
async function handleClick(event: Event): Promise<void> { async function handleClick(event: Event): Promise<void> {
console.log("上传按钮被点击");
if (isNull(bitstream.value) || isUndefined(bitstream.value)) { if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
dialog.error(`未选择文件`); dialog.error(`未选择文件`);
return; return;
@@ -97,19 +231,21 @@ async function handleClick(event: Event): Promise<void> {
} }
isUploading.value = true; isUploading.value = true;
let uploadedBitstreamId: number | null = null;
try { try {
const ret = await props.uploadEvent(bitstream.value); console.log("开始上传比特流文件:", bitstream.value.name);
const bitstreamId = await props.uploadEvent(bitstream.value, props.examId || '');
console.log("上传结果ID:", bitstreamId);
if (isUndefined(props.downloadEvent)) { if (isUndefined(props.downloadEvent)) {
if (ret) { console.log("上传成功,下载未定义");
dialog.info("上传成功");
emits("finishedUpload", bitstream.value);
} else dialog.error("上传失败");
return;
}
if (!ret) {
isUploading.value = false; isUploading.value = false;
return; return;
} }
if (bitstreamId === null || bitstreamId === undefined) {
isUploading.value = false;
return;
}
uploadedBitstreamId = bitstreamId;
} catch (e) { } catch (e) {
dialog.error("上传失败"); dialog.error("上传失败");
console.error(e); console.error(e);
@@ -118,9 +254,14 @@ async function handleClick(event: Event): Promise<void> {
// Download // Download
try { try {
const ret = await props.downloadEvent(); console.log("开始下载比特流ID:", uploadedBitstreamId);
if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) {
dialog.error("uploadedBitstreamId is null or undefined");
} else {
const ret = await props.downloadEvent(uploadedBitstreamId);
if (ret) dialog.info("下载成功"); if (ret) dialog.info("下载成功");
else dialog.error("下载失败"); else dialog.error("下载失败");
}
} catch (e) { } catch (e) {
dialog.error("下载失败"); dialog.error("下载失败");
console.error(e); console.error(e);

View File

@@ -2,12 +2,12 @@
<div <div
class="w-full" class="w-full"
:class="{ :class="{
'h-48': !analyzer.logicData.value, 'h-48': !props.data,
'h-150': analyzer.logicData.value, 'h-150': props.data,
}" }"
> >
<v-chart <v-chart
v-if="analyzer.logicData.value" v-if="props.data"
class="w-full h-full" class="w-full h-full"
:option="option" :option="option"
autoresize autoresize
@@ -17,17 +17,20 @@
v-else v-else
class="w-full h-full flex flex-col gap-6 items-center justify-center" class="w-full h-full flex flex-col gap-6 items-center justify-center"
> >
<template v-if="hasSlot">
<slot />
</template>
<template v-else>
<div class="text-center"> <div class="text-center">
<h3 class="text-xl font-semibold text-slate-600 mb-2"> <h3 class="text-xl font-semibold text-slate-600 mb-2">暂无数据</h3>
暂无逻辑分析数据
</h3>
</div> </div>
</template>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, shallowRef } from "vue"; import { computed, shallowRef, useSlots } from "vue";
import VChart from "vue-echarts"; import VChart from "vue-echarts";
// Echarts // Echarts
@@ -39,6 +42,7 @@ import {
DataZoomComponent, DataZoomComponent,
AxisPointerComponent, AxisPointerComponent,
ToolboxComponent, ToolboxComponent,
MarkLineComponent,
} from "echarts/components"; } from "echarts/components";
import { CanvasRenderer } from "echarts/renderers"; import { CanvasRenderer } from "echarts/renderers";
import type { ComposeOption } from "echarts/core"; import type { ComposeOption } from "echarts/core";
@@ -48,15 +52,15 @@ import type {
TooltipComponentOption, TooltipComponentOption,
GridComponentOption, GridComponentOption,
DataZoomComponentOption, DataZoomComponentOption,
MarkLineComponentOption,
} from "echarts/components"; } from "echarts/components";
import type { import type {
ToolboxComponentOption, ToolboxComponentOption,
XAXisOption, XAXisOption,
YAXisOption, YAXisOption,
} from "echarts/types/dist/shared"; } from "echarts/types/dist/shared";
import { useRequiredInjection } from "@/utils/Common";
import { isUndefined } from "lodash"; import { isUndefined } from "lodash";
import { useWaveformManager } from "./WaveformManager"; import type { LogicDataType } from ".";
use([ use([
TooltipComponent, TooltipComponent,
@@ -66,6 +70,7 @@ use([
DataZoomComponent, DataZoomComponent,
LineChart, LineChart,
CanvasRenderer, CanvasRenderer,
MarkLineComponent,
]); ]);
type EChartsOption = ComposeOption< type EChartsOption = ComposeOption<
@@ -75,9 +80,15 @@ type EChartsOption = ComposeOption<
| GridComponentOption | GridComponentOption
| DataZoomComponentOption | DataZoomComponentOption
| LineSeriesOption | LineSeriesOption
| MarkLineComponentOption
>; >;
const analyzer = useRequiredInjection(useWaveformManager); const props = defineProps<{
data?: LogicDataType;
}>();
const slots = useSlots();
const hasSlot = computed(() => !!slots.default && slots.default().length > 0);
// 添加更新选项来减少重绘 // 添加更新选项来减少重绘
const updateOptions = shallowRef({ const updateOptions = shallowRef({
@@ -87,11 +98,13 @@ const updateOptions = shallowRef({
}); });
const option = computed((): EChartsOption => { const option = computed((): EChartsOption => {
if (isUndefined(analyzer.logicData.value)) return {}; if (isUndefined(props.data)) return {};
// 只获取启用的通道使用y数据结构 // 只获取启用的通道使用y数据结构
const enabledChannels = analyzer.logicData.value.y.filter(channel => channel.enabled); const enabledChannels = props.data.y.filter(
const enabledChannelIndices = analyzer.logicData.value.y (channel) => channel.enabled,
);
const enabledChannelIndices = props.data.y
.map((channel, index) => (channel.enabled ? index : -1)) .map((channel, index) => (channel.enabled ? index : -1))
.filter((index) => index !== -1); .filter((index) => index !== -1);
@@ -117,12 +130,12 @@ const option = computed((): EChartsOption => {
const xAxis: XAXisOption[] = [ const xAxis: XAXisOption[] = [
{ {
type: "category", type: "category",
boundaryGap: false, boundaryGap: true,
data: analyzer.logicData.value.x.map((x) => x.toFixed(3)), data: props.data.x.map((x) => x.toFixed(3)),
axisLabel: { axisLabel: {
formatter: (value: string) => formatter: (value: string) =>
analyzer.logicData.value props.data
? `${value}${analyzer.logicData.value.xUnit}` ? `${value}${props.data.xUnit}`
: `${value}`, : `${value}`,
}, },
}, },
@@ -149,8 +162,9 @@ const option = computed((): EChartsOption => {
// 创建系列数据 // 创建系列数据
const series: LineSeriesOption[] = []; const series: LineSeriesOption[] = [];
enabledChannelIndices.forEach((originalIndex: number, displayIndex: number) => { enabledChannelIndices.forEach(
const channel = analyzer.logicData.value!.y[originalIndex]; (originalIndex: number, displayIndex: number) => {
const channel = props.data!.y[originalIndex];
if (channel.type === "logic") { if (channel.type === "logic") {
// logic类型原样显示 // logic类型原样显示
series.push({ series.push({
@@ -174,52 +188,126 @@ const option = computed((): EChartsOption => {
animation: false, animation: false,
}); });
} else if (channel.type === "number") { } else if (channel.type === "number") {
// number类型VCD仿真样式两个线条1和0变化时有斜率过渡点无areaStyle
const values = channel.value; const values = channel.value;
const xArr = analyzer.logicData.value!.x; const xArr = props.data!.x;
// 构造带过渡的点序列 // 构造带过渡的点序列
function buildVcdLine(valArr: number[], high: number, low: number) { function buildVcdLine(valArr: number[], high: number, low: number) {
const points: {x: number, y: number}[] = []; const points: { x: number; y: number }[] = [];
let lastValue = high; let lastValue = high;
points.push({x: xArr[0], y: lastValue}); points.push({ x: xArr[0], y: lastValue });
for (let i = 1; i < valArr.length; i++) { for (let i = 1; i < valArr.length; i++) {
const v = valArr[i] !== valArr[i-1] ? (lastValue === high ? low : high) : lastValue; const v =
points.push({x: xArr[i], y: v}); valArr[i] !== valArr[i - 1]
? lastValue === high
? low
: high
: lastValue;
points.push({ x: xArr[i], y: v });
lastValue = v; lastValue = v;
} }
// 返回y数组x由category轴控制 return points.map((p) => p.y);
return points.map(p => p.y);
} }
// 计算变化点区间
function buildMarkLines(valArr: number[], yBase: number) {
const markLines: any[] = [];
let lastValue = valArr[0];
let lastIdx = 0;
// 格式化函数
function formatValue(val: number) {
if (channel.base === "hex")
return "0x" + val.toString(16).toUpperCase();
if (channel.base === "bin") return "0b" + val.toString(2);
return val.toString();
}
for (let i = 1; i <= valArr.length; i++) {
if (i === valArr.length || valArr[i] !== lastValue) {
markLines.push([
{
xAxis: lastIdx,
yAxis: yBase,
label: {
formatter: formatValue(lastValue),
position: "insideMiddle",
color: channel.color,
fontSize: 18,
opacity: 1,
},
lineStyle: {
opacity: 0,
},
},
{
xAxis: i - 1,
yAxis: yBase,
lineStyle: {
opacity: 0,
},
},
]);
lastValue = valArr[i];
lastIdx = i;
}
}
return markLines;
}
// 1线条 // 1线条
series.push({ series.push({
name: channel.name + "_1", name: channel.name + "_1",
type: "line", type: "line",
data: buildVcdLine(values, displayIndex * channelSpacing + 1, displayIndex * channelSpacing), data: buildVcdLine(
step: false, // 关闭step允许斜率 values,
lineStyle: { displayIndex * channelSpacing + 1,
width: 2, displayIndex * channelSpacing,
color: channel.color, ),
},
symbol: "none",
sampling: "lttb",
animation: false,
});
// 0线条
series.push({
name: channel.name + "_0",
type: "line",
data: buildVcdLine(values, displayIndex * channelSpacing, displayIndex * channelSpacing + 1),
step: false, step: false,
lineStyle: { lineStyle: {
width: 2, width: 2,
color: channel.color, color: channel.color,
}, },
areaStyle: {
opacity: 0.3,
origin: displayIndex * channelSpacing + 0.5,
color: channel.color,
},
symbol: "none",
sampling: "lttb",
animation: false,
// 添加markLine
markLine: {
data: buildMarkLines(values, displayIndex * channelSpacing + 0.5),
emphasis: {
disabled: true,
},
},
});
// 0线条
series.push({
name: channel.name + "_0",
type: "line",
data: buildVcdLine(
values,
displayIndex * channelSpacing,
displayIndex * channelSpacing + 1,
),
step: false,
lineStyle: {
width: 2,
color: channel.color,
},
areaStyle: {
opacity: 0.3,
origin: displayIndex * channelSpacing + 0.5,
color: channel.color,
},
symbol: "none", symbol: "none",
sampling: "lttb", sampling: "lttb",
animation: false, animation: false,
}); });
} }
}); },
);
return { return {
animation: false, animation: false,
@@ -234,11 +322,12 @@ const option = computed((): EChartsOption => {
}, },
formatter: (params: any) => { formatter: (params: any) => {
if (Array.isArray(params) && params.length > 0) { if (Array.isArray(params) && params.length > 0) {
const timeValue = analyzer.logicData.value!.x[params[0].dataIndex]; const timeValue = props.data!.x[params[0].dataIndex];
const dataIndex = params[0].dataIndex; const dataIndex = params[0].dataIndex;
let tooltip = `Time: ${timeValue.toFixed(3)}${analyzer.logicData.value!.xUnit}<br/>`; let tooltip = `Time: ${timeValue.toFixed(3)}${props.data!.xUnit}<br/>`;
enabledChannelIndices.forEach((originalIndex: number, displayIndex: number) => { enabledChannelIndices.forEach(
const channel = analyzer.logicData.value!.y[originalIndex]; (originalIndex: number, displayIndex: number) => {
const channel = props.data!.y[originalIndex];
if (channel.type === "logic") { if (channel.type === "logic") {
const channelName = channel.name; const channelName = channel.name;
const originalValue = channel.value[dataIndex]; const originalValue = channel.value[dataIndex];
@@ -248,7 +337,8 @@ const option = computed((): EChartsOption => {
const originalValue = channel.value[dataIndex]; const originalValue = channel.value[dataIndex];
tooltip += `${channelName}: ${originalValue}<br/>`; tooltip += `${channelName}: ${originalValue}<br/>`;
} }
}); },
);
return tooltip; return tooltip;
} }
return ""; return "";

View File

@@ -1,73 +0,0 @@
import { createInjectionState } from "@vueuse/core";
import { shallowRef } from "vue";
export type LogicDataType = {
x: number[];
y: {
enabled: boolean;
type: "logic" | "number";
name: string;
color: string;
value: number[];
base: "bin" | "dec" | "hex";
}[];
xUnit: "s" | "ms" | "us" | "ns";
};
// 生成4路测试数据的函数
export function generateTestData(): LogicDataType {
// 生成时间轴数据 (0-100ns每1ns一个采样点)
const timePoints = Array.from({ length: 101 }, (_, i) => i);
return {
x: timePoints,
y: [
{
enabled: true,
type: "logic",
name: "CLK",
color: "#ff0000",
value: timePoints.map((t) => t % 2), // 时钟信号每1ns翻转
base: "bin",
},
{
enabled: true,
type: "logic",
name: "RESET",
color: "#00ff00",
value: timePoints.map((t) => (t < 10 ? 1 : 0)), // 复位信号前10ns为高电平
base: "bin",
},
{
enabled: true,
type: "number",
name: "DATA",
color: "#0000ff",
value: timePoints.map((t) => Math.floor(t / 4) % 16), // 计数器每4ns递增
base: "hex",
},
{
enabled: true,
type: "logic",
name: "ENABLE",
color: "#ff8800",
value: timePoints.map((t) => (t >= 20 && t < 80 ? 1 : 0)), // 使能信号20-80ns为高电平
base: "bin",
},
],
xUnit: "ns",
};
}
const [useProvideWaveformManager, useWaveformManager] = createInjectionState(
() => {
const logicData = shallowRef<LogicDataType>();
return {
logicData,
generateTestData,
};
},
);
export { useProvideWaveformManager, useWaveformManager };

View File

@@ -1,5 +1,61 @@
import WaveformDisplay from "./WaveformDisplay.vue"; import WaveformDisplay from "./WaveformDisplay.vue";
export { export type LogicDataType = {
WaveformDisplay x: number[];
y: {
enabled: boolean;
type: "logic" | "number";
name: string;
color: string;
value: number[];
base: "bin" | "dec" | "hex";
}[];
xUnit: "s" | "ms" | "us" | "ns";
}; };
// 生成4路测试数据的函数
export function generateTestData(): LogicDataType {
// 生成时间轴数据 (0-100ns每1ns一个采样点)
const timePoints = Array.from({ length: 101 }, (_, i) => i);
return {
x: timePoints,
y: [
{
enabled: true,
type: "logic",
name: "CLK",
color: "#ff0000",
value: timePoints.map((t) => t % 2), // 时钟信号每1ns翻转
base: "bin",
},
{
enabled: true,
type: "logic",
name: "RESET",
color: "#00ff00",
value: timePoints.map((t) => (t < 10 ? 1 : 0)), // 复位信号前10ns为高电平
base: "bin",
},
{
enabled: true,
type: "number",
name: "DATA",
color: "#0000ff",
value: timePoints.map((t) => Math.floor(t / 4) % 16), // 计数器每4ns递增
base: "hex",
},
{
enabled: true,
type: "logic",
name: "ENABLE",
color: "#ff8800",
value: timePoints.map((t) => (t >= 20 && t < 80 ? 1 : 0)), // 使能信号20-80ns为高电平
base: "bin",
},
],
xUnit: "ns",
};
}
export { WaveformDisplay };

View File

@@ -27,7 +27,7 @@
to="#ComponentCapabilities" to="#ComponentCapabilities"
v-if="selectecComponentID === props.componentId" v-if="selectecComponentID === props.componentId"
> >
<MotherBoardCaps :jtagFreq="jtagFreq" @change-jtag-freq="changeJtagFreq" /> <MotherBoardCaps :jtagFreq="jtagFreq" :exam-id="examId" @change-jtag-freq="changeJtagFreq" />
</Teleport> </Teleport>
</template> </template>
@@ -41,6 +41,7 @@ import { toNumber } from "lodash";
export interface MotherBoardProps { export interface MotherBoardProps {
size: number; size: number;
componentId?: string; componentId?: string;
examId?: string; // 新增examId属性
} }
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -8,20 +8,35 @@
<p> <p>
IDCode: 0x{{ jtagIDCode.toString(16).padStart(8, "0").toUpperCase() }} IDCode: 0x{{ jtagIDCode.toString(16).padStart(8, "0").toUpperCase() }}
</p> </p>
<button class="btn btn-circle w-6 h-6" :disabled="isGettingIDCode" :onclick="getIDCode"> <button
<RefreshCcwIcon class="icon" :class="{ 'animate-spin': isGettingIDCode }" /> class="btn btn-circle w-6 h-6"
:disabled="isGettingIDCode"
:onclick="getIDCode"
>
<RefreshCcwIcon
class="icon"
:class="{ 'animate-spin': isGettingIDCode }"
/>
</button> </button>
</div> </div>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<UploadCard class="bg-base-200" :upload-event="eqps.jtagUploadBitstream" <UploadCard
:download-event="eqps.jtagDownloadBitstream" :bitstream-file="eqps.jtagBitstream" :exam-id="props.examId"
@update:bitstream-file="handleBitstreamChange"> :upload-event="eqps.jtagUploadBitstream"
:download-event="handleDownloadBitstream"
:bitstream-file="eqps.jtagBitstream"
@update:bitstream-file="handleBitstreamChange"
>
</UploadCard> </UploadCard>
<div class="divider"></div> <div class="divider"></div>
<div class="w-full"> <div class="w-full">
<legend class="fieldset-legend text-sm mb-0.3">Jtag运行频率</legend> <legend class="fieldset-legend text-sm mb-0.3">Jtag运行频率</legend>
<select class="select w-full" @change="handleSelectJtagSpeed" :value="props.jtagFreq"> <select
class="select w-full"
@change="handleSelectJtagSpeed"
:value="props.jtagFreq"
>
<option v-for="option in selectJtagSpeedOptions" :value="option.id"> <option v-for="option in selectJtagSpeedOptions" :value="option.id">
{{ option.text }} {{ option.text }}
</option> </option>
@@ -30,27 +45,38 @@
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<fieldset class="fieldset w-70"> <fieldset class="fieldset w-70">
<legend class="fieldset-legend text-sm">边界扫描刷新率 / Hz</legend> <legend class="fieldset-legend text-sm">边界扫描刷新率 / Hz</legend>
<input type="number" class="input validator" required placeholder="Type a number between 1 to 1000" min="1" <input
max="1000" v-model="jtagBoundaryScanFreq" title="Type a number between 1 to 1000" /> type="number"
class="input validator"
required
placeholder="Type a number between 1 to 1000"
min="1"
max="1000"
v-model="jtagBoundaryScanFreq"
title="Type a number between 1 to 1000"
/>
<p class="validator-hint">输入一个1 ~ 1000的数</p> <p class="validator-hint">输入一个1 ~ 1000的数</p>
</fieldset> </fieldset>
<button class="btn btn-primary grow mx-4" :class="eqps.enableJtagBoundaryScan ? '' : 'btn-soft'" <button
:onclick="toggleJtagBoundaryScan"> class="btn btn-primary grow mx-4"
:class="eqps.enableJtagBoundaryScan ? '' : 'btn-soft'"
:onclick="toggleJtagBoundaryScan"
>
{{ eqps.enableJtagBoundaryScan ? "关闭边界扫描" : "启动边界扫描" }} {{ eqps.enableJtagBoundaryScan ? "关闭边界扫描" : "启动边界扫描" }}
</button> </button>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<h1 class="font-bold text-center text-2xl">外设</h1> <h1 class="font-bold text-center text-2xl">外设</h1>
<div class="flex flex-row justify-around"> <div class="flex flex-row justify-center">
<div class="flex flex-row"> <div class="flex flex-row">
<input type="checkbox" class="checkbox" :checked="eqps.enableMatrixKey" <input
@change="handleMatrixkeyCheckboxChange" /> type="checkbox"
class="checkbox"
:checked="eqps.enableMatrixKey"
@change="handleMatrixkeyCheckboxChange"
/>
<p class="mx-2">启用矩阵键盘</p> <p class="mx-2">启用矩阵键盘</p>
</div> </div>
<div class="flex flex-row">
<input type="checkbox" class="checkbox" :checked="eqps.enablePower" @change="handlePowerCheckboxChange" />
<p class="mx-2">启用电源</p>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -65,6 +91,7 @@ import { RefreshCcwIcon } from "lucide-vue-next";
interface CapsProps { interface CapsProps {
jtagFreq?: string; jtagFreq?: string;
examId?: string; // 新增examId属性
} }
const emits = defineEmits<{ const emits = defineEmits<{
@@ -101,6 +128,11 @@ function handleBitstreamChange(file: File | undefined) {
eqps.jtagBitstream = file; eqps.jtagBitstream = file;
} }
async function handleDownloadBitstream(bitstreamId: number): Promise<boolean> {
console.log("开始下载比特流ID:", bitstreamId);
return await eqps.jtagDownloadBitstream(bitstreamId);
}
function handleSelectJtagSpeed(event: Event) { function handleSelectJtagSpeed(event: Event) {
const target = event.target as HTMLSelectElement; const target = event.target as HTMLSelectElement;
eqps.jtagSetSpeed(target.selectedIndex); eqps.jtagSetSpeed(target.selectedIndex);
@@ -120,18 +152,8 @@ async function handleMatrixkeyCheckboxChange(event: Event) {
} }
} }
async function handlePowerCheckboxChange(event: Event) {
const target = event.target as HTMLInputElement;
const ret = await eqps.powerSetOnOff(target.checked);
if (target.checked) {
eqps.enablePower = ret;
} else {
eqps.enablePower = !ret;
}
}
async function toggleJtagBoundaryScan() { async function toggleJtagBoundaryScan() {
eqps.enableJtagBoundaryScan = !eqps.enableJtagBoundaryScan; eqps.jtagBoundaryScanSetOnOff(!eqps.enableJtagBoundaryScan);
} }
const isGettingIDCode = ref(false); const isGettingIDCode = ref(false);

View File

@@ -4,6 +4,7 @@ import AuthView from "../views/AuthView.vue";
import ProjectView from "../views/Project/Index.vue"; import ProjectView from "../views/Project/Index.vue";
import TestView from "../views/TestView.vue"; import TestView from "../views/TestView.vue";
import UserView from "@/views/User/Index.vue"; import UserView from "@/views/User/Index.vue";
import ExamView from "@/views/ExamView.vue";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@@ -13,6 +14,7 @@ const router = createRouter({
{ path: "/project", name: "project", component: ProjectView }, { path: "/project", name: "project", component: ProjectView },
{ path: "/test", name: "test", component: TestView }, { path: "/test", name: "test", component: TestView },
{ path: "/user", name: "user", component: UserView }, { path: "/user", name: "user", component: UserView },
{ path: "/exam", name: "exam", component: ExamView },
], ],
}); });

View File

@@ -1,26 +1,25 @@
import { ref, computed, reactive } from 'vue' import { ref, computed, reactive } from "vue";
import { defineStore } from 'pinia' import { defineStore } from "pinia";
import { isBoolean } from 'lodash'; import { isBoolean } from "lodash";
// 约束电平状态类型 // 约束电平状态类型
export type ConstraintLevel = 'high' | 'low' | 'undefined'; export type ConstraintLevel = "high" | "low" | "undefined";
export const useConstraintsStore = defineStore('constraints', () => {
export const useConstraintsStore = defineStore("constraints", () => {
// 约束状态存储 // 约束状态存储
const constraintStates = reactive<Record<string, ConstraintLevel>>({}); const constraintStates = reactive<Record<string, ConstraintLevel>>({});
// 约束颜色映射 // 约束颜色映射
const constraintColors = { const constraintColors = {
high: '#ff3333', // 高电平为红色 high: "#ff3333", // 高电平为红色
low: '#3333ff', // 低电平为蓝色 low: "#3333ff", // 低电平为蓝色
undefined: '#999999' // 未定义为灰色 undefined: "#999999", // 未定义为灰色
}; };
// 获取约束状态 // 获取约束状态
function getConstraintState(constraint: string): ConstraintLevel { function getConstraintState(constraint: string): ConstraintLevel {
if (!constraint) return 'undefined'; if (!constraint) return "undefined";
return constraintStates[constraint] || 'undefined'; return constraintStates[constraint] || "undefined";
} }
// 设置约束状态 // 设置约束状态
@@ -30,7 +29,9 @@ export const useConstraintsStore = defineStore('constraints', () => {
} }
// 批量设置约束状态 // 批量设置约束状态
function batchSetConstraintStates(states: Record<string, ConstraintLevel> | Record<string, boolean>) { function batchSetConstraintStates(
states: Record<string, ConstraintLevel> | Partial<Record<string, boolean>>,
) {
// 收集发生变化的约束 // 收集发生变化的约束
const changedConstraints: [string, ConstraintLevel][] = []; const changedConstraints: [string, ConstraintLevel][] = [];
@@ -38,6 +39,8 @@ export const useConstraintsStore = defineStore('constraints', () => {
Object.entries(states).forEach(([constraint, level]) => { Object.entries(states).forEach(([constraint, level]) => {
if (isBoolean(level)) { if (isBoolean(level)) {
level = level ? "high" : "low"; level = level ? "high" : "low";
} else {
level = "low";
} }
if (constraintStates[constraint] !== level) { if (constraintStates[constraint] !== level) {
@@ -48,7 +51,7 @@ export const useConstraintsStore = defineStore('constraints', () => {
// 通知所有变化 // 通知所有变化
changedConstraints.forEach(([constraint, level]) => { changedConstraints.forEach(([constraint, level]) => {
stateChangeCallbacks.forEach(callback => callback(constraint, level)); stateChangeCallbacks.forEach((callback) => callback(constraint, level));
}); });
} }
@@ -60,7 +63,7 @@ export const useConstraintsStore = defineStore('constraints', () => {
// 清除所有约束状态 // 清除所有约束状态
function clearAllConstraintStates() { function clearAllConstraintStates() {
Object.keys(constraintStates).forEach(key => { Object.keys(constraintStates).forEach((key) => {
delete constraintStates[key]; delete constraintStates[key];
}); });
} }
@@ -71,9 +74,14 @@ export const useConstraintsStore = defineStore('constraints', () => {
} }
// 注册约束状态变化回调 // 注册约束状态变化回调
const stateChangeCallbacks: ((constraint: string, level: ConstraintLevel) => void)[] = []; const stateChangeCallbacks: ((
constraint: string,
level: ConstraintLevel,
) => void)[] = [];
function onConstraintStateChange(callback: (constraint: string, level: ConstraintLevel) => void) { function onConstraintStateChange(
callback: (constraint: string, level: ConstraintLevel) => void,
) {
stateChangeCallbacks.push(callback); stateChangeCallbacks.push(callback);
return () => { return () => {
const index = stateChangeCallbacks.indexOf(callback); const index = stateChangeCallbacks.indexOf(callback);
@@ -86,7 +94,7 @@ export const useConstraintsStore = defineStore('constraints', () => {
// 触发约束变化 // 触发约束变化
function notifyConstraintChange(constraint: string, level: ConstraintLevel) { function notifyConstraintChange(constraint: string, level: ConstraintLevel) {
setConstraintState(constraint, level); setConstraintState(constraint, level);
stateChangeCallbacks.forEach(callback => callback(constraint, level)); stateChangeCallbacks.forEach((callback) => callback(constraint, level));
} }
return { return {
@@ -98,6 +106,5 @@ export const useConstraintsStore = defineStore('constraints', () => {
getAllConstraintStates, getAllConstraintStates,
onConstraintStateChange, onConstraintStateChange,
notifyConstraintChange, notifyConstraintChange,
} };
}) });

View File

@@ -1,15 +1,18 @@
import { ref, reactive, watchPostEffect } from "vue"; import { ref, reactive, watchPostEffect, onMounted, onUnmounted } from "vue";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
import { isString, toNumber } from "lodash"; import { isString, toNumber, isUndefined, type Dictionary } from "lodash";
import z from "zod"; import z from "zod";
import { isNumber } from "mathjs"; import { isNumber } from "mathjs";
import { JtagClient, MatrixKeyClient, PowerClient } from "@/APIClient";
import { Mutex, withTimeout } from "async-mutex"; import { Mutex, withTimeout } from "async-mutex";
import { useConstraintsStore } from "@/stores/constraints"; import { useConstraintsStore } from "@/stores/constraints";
import { useDialogStore } from "./dialog"; import { useDialogStore } from "./dialog";
import { toFileParameterOrUndefined } from "@/utils/Common"; import { toFileParameterOrUndefined } from "@/utils/Common";
import { AuthManager } from "@/utils/AuthManager"; import { AuthManager } from "@/utils/AuthManager";
import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr";
import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client";
import type { ResourceInfo } from "@/APIClient";
import type { IJtagHub } from "@/TypedSignalR.Client/server.Hubs.JtagHub";
export const useEquipments = defineStore("equipments", () => { export const useEquipments = defineStore("equipments", () => {
// Global Stores // Global Stores
@@ -22,13 +25,39 @@ export const useEquipments = defineStore("equipments", () => {
// Jtag // Jtag
const jtagBitstream = ref<File>(); const jtagBitstream = ref<File>();
const jtagBoundaryScanFreq = ref(100); const jtagBoundaryScanFreq = ref(100);
const jtagBoundaryScanErrorCount = ref(0); // 边界扫描连续错误计数 const jtagUserBitstreams = ref<ResourceInfo[]>([]);
const maxJtagBoundaryScanErrors = 5; // 最大允许连续错误次数
const jtagClientMutex = withTimeout( const jtagClientMutex = withTimeout(
new Mutex(), new Mutex(),
1000, 1000,
new Error("JtagClient Mutex Timeout!"), new Error("JtagClient Mutex Timeout!"),
); );
const jtagHubConnection = ref<HubConnection>();
const jtagHubProxy = ref<IJtagHub>();
onMounted(async () => {
// 每次挂载都重新创建连接
jtagHubConnection.value =
AuthManager.createAuthenticatedJtagHubConnection();
jtagHubProxy.value = getHubProxyFactory("IJtagHub").createHubProxy(
jtagHubConnection.value,
);
getReceiverRegister("IJtagReceiver").register(jtagHubConnection.value, {
onReceiveBoundaryScanData: async (msg) => {
constrainsts.batchSetConstraintStates(msg);
},
});
await jtagHubConnection.value.start();
});
onUnmounted(() => {
// 断开连接,清理资源
if (jtagHubConnection.value) {
jtagHubConnection.value.stop();
jtagHubConnection.value = undefined;
jtagHubProxy.value = undefined;
}
});
// Matrix Key // Matrix Key
const matrixKeyStates = reactive(new Array<boolean>(16).fill(false)); const matrixKeyStates = reactive(new Array<boolean>(16).fill(false));
@@ -50,41 +79,6 @@ export const useEquipments = defineStore("equipments", () => {
const enableMatrixKey = ref(false); const enableMatrixKey = ref(false);
const enablePower = ref(false); const enablePower = ref(false);
// Watch
watchPostEffect(async () => {
if (true === enableJtagBoundaryScan.value) {
// 重新启用时重置错误计数器
jtagBoundaryScanErrorCount.value = 0;
jtagBoundaryScan();
}
});
// Parse and Set
function setAddr(address: string | undefined): boolean {
if (isString(address) && z.string().ip("4").safeParse(address).success) {
boardAddr.value = address;
return true;
}
return false;
}
function setPort(port: string | number | undefined): boolean {
if (isString(port) && port.length != 0) {
const portNumber = toNumber(port);
if (z.number().nonnegative().max(65535).safeParse(portNumber).success) {
boardPort.value = portNumber;
return true;
}
} else if (isNumber(port)) {
if (z.number().nonnegative().max(65535).safeParse(port).success) {
boardPort.value = port;
return true;
}
}
return false;
}
function setMatrixKey( function setMatrixKey(
keyNum: number | string | undefined, keyNum: number | string | undefined,
keyValue: boolean, keyValue: boolean,
@@ -105,64 +99,76 @@ export const useEquipments = defineStore("equipments", () => {
return false; return false;
} }
async function jtagBoundaryScan() { async function jtagBoundaryScanSetOnOff(enable: boolean) {
const release = await jtagClientMutex.acquire(); if (isUndefined(jtagHubProxy.value)) {
try { console.error("JtagHub Not Initialize...");
const jtagClient = AuthManager.createAuthenticatedJtagClient(); return;
const portStates = await jtagClient.boundaryScanLogicalPorts( }
boardAddr.value,
boardPort.value, if (enable) {
const ret = await jtagHubProxy.value.startBoundaryScan(
jtagBoundaryScanFreq.value,
); );
if (!ret) {
constrainsts.batchSetConstraintStates(portStates); console.error("Failed to start boundary scan");
return;
// 扫描成功,重置错误计数器
jtagBoundaryScanErrorCount.value = 0;
} catch (error) {
jtagBoundaryScanErrorCount.value++;
console.error(`边界扫描错误 (${jtagBoundaryScanErrorCount.value}/${maxJtagBoundaryScanErrors}):`, error);
// 如果错误次数超过最大允许次数,才停止扫描并显示错误
if (jtagBoundaryScanErrorCount.value >= maxJtagBoundaryScanErrors) {
dialog.error("边界扫描发生连续错误,已自动停止");
enableJtagBoundaryScan.value = false;
jtagBoundaryScanErrorCount.value = 0; // 重置错误计数器
} }
} finally { } else {
release(); const ret = await jtagHubProxy.value.stopBoundaryScan();
if (!ret) {
if (enableJtagBoundaryScan.value) console.error("Failed to stop boundary scan");
setTimeout(jtagBoundaryScan, 1000 / jtagBoundaryScanFreq.value); return;
} }
} }
enableJtagBoundaryScan.value = enable;
}
async function jtagUploadBitstream(bitstream: File): Promise<boolean> { async function jtagUploadBitstream(bitstream: File, examId?: string): Promise<number | null> {
try { try {
const jtagClient = AuthManager.createAuthenticatedJtagClient(); // 自动开启电源
const resp = await jtagClient.uploadBitstream( await powerSetOnOff(true);
boardAddr.value,
const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resp = await resourceClient.addResource(
'bitstream',
'user',
examId || null,
toFileParameterOrUndefined(bitstream), toFileParameterOrUndefined(bitstream),
); );
return resp;
// 如果上传成功,设置为当前选中的比特流
if (resp && resp.id !== undefined && resp.id !== null) {
return resp.id;
}
return null;
} catch (e) { } catch (e) {
dialog.error("上传错误"); dialog.error("上传错误");
console.error(e); console.error(e);
return false; return null;
} }
} }
async function jtagDownloadBitstream(): Promise<boolean> { async function jtagDownloadBitstream(bitstreamId?: number): Promise<boolean> {
if (bitstreamId === null || bitstreamId === undefined) {
dialog.error("请先选择要下载的比特流");
return false;
}
const release = await jtagClientMutex.acquire(); const release = await jtagClientMutex.acquire();
try { try {
// 自动开启电源
await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient(); const jtagClient = AuthManager.createAuthenticatedJtagClient();
const resp = await jtagClient.downloadBitstream( const resp = await jtagClient.downloadBitstream(
boardAddr.value, boardAddr.value,
boardPort.value, boardPort.value,
bitstreamId,
); );
return resp; return resp;
} catch (e) { } catch (e) {
dialog.error("上传错误"); dialog.error("下载错误");
console.error(e); console.error(e);
return false; return false;
} finally { } finally {
@@ -173,6 +179,9 @@ export const useEquipments = defineStore("equipments", () => {
async function jtagGetIDCode(isQuiet: boolean = false): Promise<number> { async function jtagGetIDCode(isQuiet: boolean = false): Promise<number> {
const release = await jtagClientMutex.acquire(); const release = await jtagClientMutex.acquire();
try { try {
// 自动开启电源
await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient(); const jtagClient = AuthManager.createAuthenticatedJtagClient();
const resp = await jtagClient.getDeviceIDCode( const resp = await jtagClient.getDeviceIDCode(
boardAddr.value, boardAddr.value,
@@ -190,6 +199,9 @@ export const useEquipments = defineStore("equipments", () => {
async function jtagSetSpeed(speed: number): Promise<boolean> { async function jtagSetSpeed(speed: number): Promise<boolean> {
const release = await jtagClientMutex.acquire(); const release = await jtagClientMutex.acquire();
try { try {
// 自动开启电源
await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient(); const jtagClient = AuthManager.createAuthenticatedJtagClient();
const resp = await jtagClient.setSpeed( const resp = await jtagClient.setSpeed(
boardAddr.value, boardAddr.value,
@@ -209,7 +221,8 @@ export const useEquipments = defineStore("equipments", () => {
const release = await matrixKeypadClientMutex.acquire(); const release = await matrixKeypadClientMutex.acquire();
console.log("set Key !!!!!!!!!!!!"); console.log("set Key !!!!!!!!!!!!");
try { try {
const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient(); const matrixKeypadClient =
AuthManager.createAuthenticatedMatrixKeyClient();
const resp = await matrixKeypadClient.setMatrixKeyStatus( const resp = await matrixKeypadClient.setMatrixKeyStatus(
boardAddr.value, boardAddr.value,
boardPort.value, boardPort.value,
@@ -228,7 +241,8 @@ export const useEquipments = defineStore("equipments", () => {
const release = await matrixKeypadClientMutex.acquire(); const release = await matrixKeypadClientMutex.acquire();
try { try {
if (enable) { if (enable) {
const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient(); const matrixKeypadClient =
AuthManager.createAuthenticatedMatrixKeyClient();
const resp = await matrixKeypadClient.enabelMatrixKey( const resp = await matrixKeypadClient.enabelMatrixKey(
boardAddr.value, boardAddr.value,
boardPort.value, boardPort.value,
@@ -236,7 +250,8 @@ export const useEquipments = defineStore("equipments", () => {
enableMatrixKey.value = resp; enableMatrixKey.value = resp;
return resp; return resp;
} else { } else {
const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient(); const matrixKeypadClient =
AuthManager.createAuthenticatedMatrixKeyClient();
const resp = await matrixKeypadClient.disableMatrixKey( const resp = await matrixKeypadClient.disableMatrixKey(
boardAddr.value, boardAddr.value,
boardPort.value, boardPort.value,
@@ -275,16 +290,14 @@ export const useEquipments = defineStore("equipments", () => {
return { return {
boardAddr, boardAddr,
boardPort, boardPort,
setAddr,
setPort,
setMatrixKey, setMatrixKey,
// Jtag // Jtag
enableJtagBoundaryScan, enableJtagBoundaryScan,
jtagBoundaryScanSetOnOff,
jtagBitstream, jtagBitstream,
jtagBoundaryScanFreq, jtagBoundaryScanFreq,
jtagBoundaryScanErrorCount, jtagUserBitstreams,
jtagClientMutex,
jtagUploadBitstream, jtagUploadBitstream,
jtagDownloadBitstream, jtagDownloadBitstream,
jtagGetIDCode, jtagGetIDCode,

View File

@@ -1,29 +0,0 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useSidebarStore = defineStore('sidebar', () => {
const isClose = ref(false);
function closeSidebar() {
isClose.value = true;
console.info("Close sidebar");
}
function openSidebar() {
isClose.value = false;
console.info("Open sidebar");
}
function toggleSidebar() {
if (isClose.value) {
openSidebar();
// themeSidebar.value = "card-dash sidebar-base sidebar-open"
} else {
closeSidebar();
// themeSidebar.value = "card-dash sidebar-base sidebar-close"
}
}
return { isClose, closeSidebar, openSidebar, toggleSidebar }
})

View File

@@ -12,7 +12,14 @@ import {
LogicAnalyzerClient, LogicAnalyzerClient,
NetConfigClient, NetConfigClient,
OscilloscopeApiClient, OscilloscopeApiClient,
DebuggerClient,
ExamClient,
ResourceClient,
} from "@/APIClient"; } from "@/APIClient";
import router from "@/router";
import { HubConnectionBuilder } from "@microsoft/signalr";
import axios, { type AxiosInstance } from "axios";
import { isNull } from "lodash";
// 支持的客户端类型联合类型 // 支持的客户端类型联合类型
type SupportedClient = type SupportedClient =
@@ -28,7 +35,10 @@ type SupportedClient =
| LogicAnalyzerClient | LogicAnalyzerClient
| UDPClient | UDPClient
| NetConfigClient | NetConfigClient
| OscilloscopeApiClient; | OscilloscopeApiClient
| DebuggerClient
| ExamClient
| ResourceClient;
export class AuthManager { export class AuthManager {
// 存储token到localStorage // 存储token到localStorage
@@ -106,13 +116,27 @@ export class AuthManager {
}; };
} }
// 私有方法创建带认证的Axios实例
private static createAuthenticatedAxiosInstance(): AxiosInstance | null {
const token = AuthManager.getToken();
if (!token) return null;
const instance = axios.create();
instance.interceptors.request.use((config) => {
config.headers = config.headers || {};
(config.headers as any)["Authorization"] = `Bearer ${token}`;
return config;
});
return instance;
}
// 通用的创建已认证客户端的方法(使用泛型) // 通用的创建已认证客户端的方法(使用泛型)
public static createAuthenticatedClient<T extends SupportedClient>( public static createAuthenticatedClient<T extends SupportedClient>(
ClientClass: new (baseUrl?: string, http?: any) => T, ClientClass: new (baseUrl?: string, instance?: AxiosInstance) => T,
): T { ): T {
const customHttp = AuthManager.createAuthenticatedHttp(); const axiosInstance = AuthManager.createAuthenticatedAxiosInstance();
return customHttp return axiosInstance
? new ClientClass(undefined, customHttp) ? new ClientClass(undefined, axiosInstance)
: new ClientClass(); : new ClientClass();
} }
@@ -169,6 +193,33 @@ export class AuthManager {
return AuthManager.createAuthenticatedClient(OscilloscopeApiClient); return AuthManager.createAuthenticatedClient(OscilloscopeApiClient);
} }
public static createAuthenticatedDebuggerClient(): DebuggerClient {
return AuthManager.createAuthenticatedClient(DebuggerClient);
}
public static createAuthenticatedExamClient(): ExamClient {
return AuthManager.createAuthenticatedClient(ExamClient);
}
public static createAuthenticatedResourceClient(): ResourceClient {
return AuthManager.createAuthenticatedClient(ResourceClient);
}
public static createAuthenticatedJtagHubConnection() {
const token = this.getToken();
if (isNull(token)) {
router.push("/login");
throw Error("Token Null!");
}
return new HubConnectionBuilder()
.withUrl("http://127.0.0.1:5000/hubs/JtagHub", {
accessTokenFactory: () => token,
})
.withAutomaticReconnect()
.build();
}
// 登录函数 // 登录函数
public static async login( public static async login(
username: string, username: string,

1069
src/views/ExamView.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -25,26 +25,37 @@
</h1> </h1>
<p class="py-6 text-lg opacity-80 leading-relaxed"> <p class="py-6 text-lg opacity-80 leading-relaxed">
Prototype and simulate electronic circuits in your browser with our 在浏览器中进行FPGA原型设计和电路仿真使用现代直观的界面创建测试和分享您的FPGA设计体验从基础学习到高级项目的完整开发流程
modern, intuitive interface. Create, test, and share your FPGA
designs seamlessly.
</p> </p>
<div class="flex flex-wrap gap-4 actions-container"> <div class="flex flex-col sm:flex-row gap-4 actions-container">
<router-link <router-link
to="/project" to="/project"
class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1" class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1 flex-1 sm:flex-none"
> >
<BookOpen class="h-5 w-5 mr-2" /> <BookOpen class="h-5 w-5 mr-2" />
进入工程界面 进入工程界面
</router-link> </router-link>
<router-link
to="/exam"
class="btn btn-secondary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1 flex-1 sm:flex-none"
>
<GraduationCap class="h-5 w-5 mr-2" />
实验列表
</router-link>
</div> </div>
<div <div
class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md" class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md"
> >
<div class="space-y-2">
<p class="text-sm"> <p class="text-sm">
<span class="font-semibold text-primary">提示</span> <span class="font-semibold text-primary">工程界面</span>
您可以在工程界面中创建编辑和测试您的FPGA项目使用我们简洁直观的界面轻松进行硬件设计 自由创建和编辑FPGA项目使用可视化画布进行电路设计和仿真测试
</p> </p>
<p class="text-sm">
<span class="font-semibold text-secondary">实验列表</span>
浏览结构化的学习实验从基础概念到高级应用的系统性学习路径
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -55,7 +66,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import "@/router"; import "@/router";
import TutorialCarousel from "@/components/TutorialCarousel.vue"; import TutorialCarousel from "@/components/TutorialCarousel.vue";
import { BookOpen } from "lucide-vue-next"; import { BookOpen, GraduationCap } from "lucide-vue-next";
</script> </script>
<style scoped lang="postcss"> <style scoped lang="postcss">

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="h-full flex flex-col gap-7"> <div class="h-full flex flex-col gap-7">
<div class="tabs tabs-box flex-shrink-0 shadow-xl mx-5"> <div class="tabs tabs-lift flex-shrink-0 mx-5">
<label class="tab"> <label class="tab">
<input <input
type="radio" type="radio"
@@ -103,15 +103,11 @@ import { isNull, toNumber } from "lodash";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import Debugger from "./Debugger.vue"; import Debugger from "./Debugger.vue";
import { useProvideLogicAnalyzer } from "@/components/LogicAnalyzer"; import { useProvideLogicAnalyzer } from "@/components/LogicAnalyzer";
import { useProvideWaveformManager } from "@/components/WaveformDisplay/WaveformManager";
import { useProvideOscilloscope } from "@/components/Oscilloscope/OscilloscopeManager"; import { useProvideOscilloscope } from "@/components/Oscilloscope/OscilloscopeManager";
const analyzer = useProvideLogicAnalyzer(); const analyzer = useProvideLogicAnalyzer();
const waveformManager = useProvideWaveformManager();
const oscilloscopeManager = useProvideOscilloscope(); const oscilloscopeManager = useProvideOscilloscope();
waveformManager.logicData.value = waveformManager.generateTestData();
const checkID = useLocalStorage("checkID", 1); const checkID = useLocalStorage("checkID", 1);
// 定义事件 // 定义事件

View File

@@ -1,11 +1,527 @@
<template> <template>
<div> <div>
<div class="card"> <div class="card m-5 bg-base-200 shadow-2xl">
<WaveformDisplay /> <div class="card-body">
<h2 class="card-title flex justify-between items-center">
<div class="flex items-center gap-2">
<Zap class="w-5 h-5" />
调试器波形捕获
</div> </div>
<div class="flex items-center gap-2">
<button
class="btn btn-sm btn-primary"
@click="
() => {
handleDeleteData();
startCapture();
}
"
:disabled="!captureData"
>
重新捕获
</button>
<button
class="btn btn-sm btn-error"
@click="handleDeleteData"
:disabled="!captureData"
>
清空
</button>
</div>
</h2>
<WaveformDisplay :data="captureData">
<div class="text-center">
<h3 class="text-xl font-semibold text-slate-600 mb-2">
暂无逻辑分析数据
</h3>
<p class="text-sm text-slate-500">点击下方按钮开始捕获</p>
</div>
<button
class="group relative px-8 py-3 bg-gradient-to-r text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 ease-in-out focus:outline-none focus:ring-4 active:scale-95"
:class="{
'from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 focus:ring-blue-300':
!isCapturing,
'from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 focus:ring-red-300':
isCapturing,
}"
@click="isCapturing ? stopCapture() : startCapture()"
>
<span class="flex items-center gap-2">
<template v-if="isCapturing">
<Square class="w-5 h-5" />
停止捕获
</template>
<template v-else>
<Play class="w-5 h-5" />
开始捕获
</template>
</span>
</button>
</WaveformDisplay>
</div>
</div>
<!-- Debugger 通道配置 -->
<div class="card m-5 bg-base-200 shadow-2xl">
<div class="card-body">
<div class="flex justify-between">
<h2 class="card-title mb-4">调试器通道配置</h2>
<div class="flex items-center gap-2">
<button
class="btn btn-sm btn-primary"
@click="addChannel"
:disabled="!configInited"
>
添加通道
</button>
<button
class="btn btn-sm btn-secondary"
@click="showConfigDialog = true"
>
配置
</button>
</div>
</div>
<!-- 配置未初始化时 -->
<div
v-if="!configInited"
class="flex flex-col items-center justify-center py-10"
>
<div class="text-lg text-slate-500 mb-4">请先进行调试器基本配置</div>
<button class="btn btn-primary" @click="showConfigDialog = true">
配置调试器
</button>
</div>
<div v-if="configInited" class="overflow-x-auto flex flex-col gap-10">
<!-- 状态概览 -->
<div
class="stats stats-horizontal bg-base-100 shadow flex justify-between"
>
<div class="stat">
<div class="stat-title">启用端口数</div>
<div class="stat-value text-primary">
{{ config.totalPortNum }}
</div>
<div class="stat-desc">每端口最大32线</div>
</div>
<div class="stat">
<div class="stat-title">最大线宽数</div>
<div class="stat-value text-info">
{{ config.totalPortNum * 32 }}
</div>
<div class="stat-desc">启用端口数 × 32</div>
</div>
<div class="stat">
<div class="stat-title">已用线宽数</div>
<div class="stat-value text-success">
{{ channels.reduce((sum, ch) => sum + ch.width, 0) }}
</div>
<div class="stat-desc">所有通道线宽总和</div>
</div>
<div class="stat">
<div class="stat-title">采样深度</div>
<div class="stat-value text-warning">
{{ config.captureDepth }}
</div>
<div class="stat-desc">每通道采样点数</div>
</div>
<div class="stat">
<div class="stat-title">时钟频率</div>
<div class="stat-value text-accent">{{ config.clkFreq }} MHz</div>
<div class="stat-desc">采样时钟</div>
</div>
</div>
<!-- 通道表格 -->
<div class="space-y-2">
<!-- 表头 -->
<div
class="grid grid-cols-7 justify-items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium"
>
<span>名称</span>
<span>显示</span>
<span>颜色</span>
<span>触发模式</span>
<span>数据位宽(起始:结尾)</span>
<span>父端口编号</span>
<span>操作</span>
</div>
<!-- 通道列表 -->
<div
v-for="(ch, idx) in channels"
:key="idx"
class="grid grid-cols-7 place-items-center gap-4 p-4 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
>
<input
v-model="ch.name"
class="input input-bordered w-full"
:placeholder="`通道${idx + 1}`"
/>
<input
type="checkbox"
v-model="ch.visible"
class="toggle toggle-primary"
/>
<input
type="color"
v-model="ch.color"
class="w-8 h-8 rounded border-2 border-base-300 cursor-pointer"
/>
<select
v-model="ch.trigger"
class="select select-bordered w-full"
>
<option
v-for="mode in triggerModes"
:key="mode.value"
:value="mode.value"
>
{{ mode.label }}
</option>
</select>
<input
v-model="ch.widthStr"
class="input input-bordered w-full"
placeholder="如0:7"
@change="parseWidthStr(idx)"
/>
<input
type="number"
min="0"
:max="config.totalPortNum - 1"
v-model.number="ch.parentPort"
class="input input-bordered w-full"
/>
<button class="btn btn-error" @click="removeChannel(idx)">
删除
</button>
</div>
<!-- 添加通道按钮 -->
<div class="flex justify-center mt-2">
<button class="btn btn-primary w-100" @click="addChannel">
添加通道
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 配置Dialog -->
<dialog v-if="showConfigDialog" class="modal modal-open">
<form
method="dialog"
class="modal-box max-w-fit"
@submit.prevent="onConfigSubmit"
>
<h3 class="font-bold text-lg mb-4">调试器基本配置</h3>
<div class="flex flex-col gap-4 w-80">
<BaseInputField
v-model="config.clkFreq"
label="时钟频率 (MHz)"
type="number"
min="1"
max="200"
:error="
config.clkFreq < 1 || config.clkFreq > 500 ? '范围1~200' : ''
"
required
/>
<BaseInputField
v-model="config.totalPortNum"
label="启用端口数"
type="number"
min="1"
max="16"
:error="
config.totalPortNum < 1 || config.totalPortNum > 16
? '范围1~16'
: ''
"
required
/>
<BaseInputField
v-model="config.captureDepth"
label="采样深度"
type="number"
min="1"
max="1048576"
:error="
config.captureDepth < 1 || config.captureDepth > 1048576
? '范围1~1048576'
: ''
"
required
/>
</div>
<div class="modal-action mt-6">
<button class="btn btn-primary" type="submit">确定</button>
<button class="btn" type="button" @click="showConfigDialog = false">
取消
</button>
</div>
</form>
</dialog>
</div> </div>
</template> </template>
<script setup lang="ts">
import WaveformDisplay from '@/components/WaveformDisplay/WaveformDisplay.vue';
<script setup lang="ts">
import { CaptureMode, ChannelConfig, DebuggerConfig } from "@/APIClient";
import { useAlertStore } from "@/components/Alert";
import BaseInputField from "@/components/InputField/BaseInputField.vue";
import type { LogicDataType } from "@/components/WaveformDisplay";
import WaveformDisplay from "@/components/WaveformDisplay/WaveformDisplay.vue";
import { AuthManager } from "@/utils/AuthManager";
import { useRequiredInjection } from "@/utils/Common";
import { useLocalStorage } from "@vueuse/core";
import axios, { type CancelTokenSource } from "axios";
import { isNull } from "lodash";
import { Play, Square, Zap } from "lucide-vue-next";
import { ref, reactive, computed } from "vue";
interface DebugChannel {
name: string;
visible: boolean;
color: string;
trigger: CaptureMode;
width: number;
widthStr: string;
start: number;
parentPort: number;
}
interface DebuggerSettings {
clkFreq: number;
totalPortNum: number;
captureDepth: number;
}
const triggerModes = [
{ value: CaptureMode.None, label: "x (无关)" },
{ value: CaptureMode.Logic0, label: "0 (低电平)" },
{ value: CaptureMode.Logic1, label: "1 (高电平)" },
{ value: CaptureMode.Rise, label: "↑ (上升沿)" },
{ value: CaptureMode.Fall, label: "↓ (下降沿)" },
];
// 基本配置
const config = reactive<DebuggerSettings>({
clkFreq: 50,
totalPortNum: 1,
captureDepth: 1024,
});
const configInited = ref(false);
const showConfigDialog = ref(false);
function onConfigSubmit() {
configInited.value = true;
showConfigDialog.value = false;
// 清空通道
channels.value = [];
}
// 通道配置
const channels = useLocalStorage<DebugChannel[]>("debugger-channels", []);
const captureData = ref<LogicDataType>();
const alert = useRequiredInjection(useAlertStore);
const isCapturing = ref(false);
const readCancelTokenSource = ref<CancelTokenSource | null>(null);
// 解析widthStr为start/width
function parseWidthStr(idx: number) {
const ch = channels.value[idx];
const match = /^(\d+)\s*:\s*(\d+)$/.exec(ch.widthStr);
if (isNull(match)) {
alert.error("格式错误,应为 起始位:宽度,如 0:7");
ch.widthStr = `${ch.start}:${ch.width}`;
return;
}
const min = Math.min(parseInt(match[1]), parseInt(match[2]));
const max = Math.max(parseInt(match[1]), parseInt(match[2]));
ch.start = min;
ch.width = max - min + 1;
}
function addChannel() {
if (!configInited.value) {
alert.error("请先配置调试器基本参数");
return;
}
if (channels.value.length >= config.totalPortNum * 32) {
alert.error("通道数已达最大线宽数");
return;
}
channels.value.push({
name: `CH${channels.value.length + 1}`,
visible: true,
color: "#00bcd4",
trigger: CaptureMode.None,
width: 1,
widthStr: "0:0",
start: 0,
parentPort: 0,
});
}
function removeChannel(idx: number) {
channels.value.splice(idx, 1);
}
function handleDeleteData() {
captureData.value = undefined;
}
function stopCapture() {
isCapturing.value = false;
if (readCancelTokenSource.value) {
readCancelTokenSource.value.cancel("用户手动停止捕获");
readCancelTokenSource.value = null;
}
}
async function startCapture() {
if (!configInited.value) {
alert.error("请先配置调试器基本参数");
return;
}
if (channels.value.length === 0) {
alert.error("请至少添加一个通道");
return;
}
// 校验通道参数
let usedWires = 0;
for (let i = 0; i < channels.value.length; i++) {
const ch = channels.value[i];
if (!ch.visible) continue;
if (!ch.name) {
alert.error(`通道 ${i + 1} 名称不能为空`);
return;
}
if (ch.width < 1 || ch.width > 32) {
alert.error(`通道 ${i + 1} 数据位宽必须在1到32之间`);
return;
}
if (ch.start < 0 || ch.start + ch.width > 32) {
alert.error(`通道 ${i + 1} 起始位+宽度不能超过32`);
return;
}
if (ch.parentPort < 0 || ch.parentPort >= config.totalPortNum) {
alert.error(`通道 ${i + 1} 父端口编号超出范围`);
return;
}
usedWires += ch.width;
}
if (usedWires > config.totalPortNum * 32) {
alert.error("所有通道线宽总和不能超过最大线宽数");
return;
}
isCapturing.value = true;
const client = AuthManager.createAuthenticatedDebuggerClient();
// 构造API配置
const channelConfigs = channels.value
.filter((ch) => ch.visible)
.map(
(ch) =>
new ChannelConfig({
name: ch.name,
color: ch.color,
wireWidth: ch.width,
wireStartIndex: ch.start,
parentPort: ch.parentPort,
mode: ch.trigger,
}),
);
const apiConfig = new DebuggerConfig({
clkFreq: config.clkFreq,
totalPortNum: config.totalPortNum,
captureDepth: config.captureDepth,
triggerNum: 0,
channelConfigs: channelConfigs,
});
try {
// 设置通道模式
let ret = await client.setChannelsMode(apiConfig);
if (!ret) {
alert.error("设置通道模式失败");
isCapturing.value = false;
return;
}
// 启动捕获
ret = await client.startTrigger();
if (!ret) {
alert.error("开始捕获失败,请检查连接");
isCapturing.value = false;
return;
}
// 读取数据
readCancelTokenSource.value = axios.CancelToken.source();
const readDataPromise = client
.readData(apiConfig, readCancelTokenSource.value.token)
.then((data) => {
const enabledChannels = channelConfigs;
const sampleCount = config.captureDepth;
// 解析数据
const y = data.map((cd, idx) => {
const ch = enabledChannels[idx];
const bin = atob(cd.data);
// UInt32数组
const arr = [];
for (let i = 0; i < bin.length; i += 4) {
arr.push(
bin.charCodeAt(i) |
(bin.charCodeAt(i + 1) << 8) |
(bin.charCodeAt(i + 2) << 16) |
(bin.charCodeAt(i + 3) << 24),
);
}
// 截取采样深度
return {
enabled: true,
type: ch.wireWidth === 1 ? ("logic" as const) : ("number" as const),
name: ch.name,
color: ch.color,
value: arr.slice(0, sampleCount),
base: ch.wireWidth === 1 ? ("bin" as const) : ("hex" as const),
};
});
const x: number[] = [];
for (let i = 0; i < sampleCount; i++) {
x.push(i * (1 / config.clkFreq)); // us
}
captureData.value = {
x,
y,
xUnit: "us",
};
})
.catch((error) => {
if (axios.isCancel(error)) {
alert.info("捕获已取消");
} else {
alert.error(`读取数据失败: ${error.message}`);
}
})
.finally(() => {
isCapturing.value = false;
readCancelTokenSource.value = null;
});
} catch (error: any) {
alert.error(`开始捕获失败: ${error.message}`);
isCapturing.value = false;
return;
}
}
</script> </script>

View File

@@ -29,6 +29,7 @@
<DiagramCanvas <DiagramCanvas
ref="diagramCanvas" ref="diagramCanvas"
:showDocPanel="showDocPanel" :showDocPanel="showDocPanel"
:exam-id="(route.query.examId as string) || ''"
@open-components="openComponentsMenu" @open-components="openComponentsMenu"
@toggle-doc-panel="toggleDocPanel" @toggle-doc-panel="toggleDocPanel"
/> />
@@ -36,13 +37,13 @@
<!-- 拖拽分割线 --> <!-- 拖拽分割线 -->
<SplitterResizeHandle <SplitterResizeHandle
id="splitter-group-h-resize-handle" id="splitter-group-h-resize-handle"
class="w-2 bg-base-100 hover:bg-primary hover:opacity-70 transition-colors" class="w-1 bg-base-300"
/> />
<!-- 右侧编辑区域 --> <!-- 右侧编辑区域 -->
<SplitterPanel <SplitterPanel
id="splitter-group-h-panel-properties" id="splitter-group-h-panel-properties"
:min-size="20" :min-size="20"
class="bg-base-200 h-full overflow-hidden flex flex-col" class="bg-base-100 h-full overflow-hidden flex flex-col"
> >
<div class="overflow-y-auto flex-1"> <div class="overflow-y-auto flex-1">
<!-- 使用条件渲染显示不同的面板 --> <!-- 使用条件渲染显示不同的面板 -->
@@ -59,7 +60,10 @@
v-show="showDocPanel" v-show="showDocPanel"
class="doc-panel overflow-y-auto h-full" class="doc-panel overflow-y-auto h-full"
> >
<MarkdownRenderer :content="documentContent" /> <MarkdownRenderer
:content="documentContent"
:examId="(route.query.examId as string) || ''"
/>
</div> </div>
</div> </div>
</SplitterPanel> </SplitterPanel>
@@ -70,7 +74,7 @@
<SplitterResizeHandle <SplitterResizeHandle
v-show="!isBottomBarFullscreen" v-show="!isBottomBarFullscreen"
id="splitter-group-v-resize-handle" id="splitter-group-v-resize-handle"
class="h-2 bg-base-100 hover:bg-primary hover:opacity-70 transition-colors" class="h-1 bg-base-300"
/> />
<!-- 功能底栏 --> <!-- 功能底栏 -->
@@ -100,11 +104,32 @@
@close="handleRequestBoardClose" @close="handleRequestBoardClose"
@success="handleRequestBoardSuccess" @success="handleRequestBoardSuccess"
/> />
<!-- Navbar切换浮动按钮 -->
<div
class="navbar-toggle-btn"
:class="{ 'with-navbar': navbarControl.showNavbar.value }"
>
<button
@click="navbarControl.toggleNavbar"
class="btn btn-circle btn-primary shadow-lg hover:shadow-xl transition-all duration-300"
:class="{ 'btn-outline': navbarControl.showNavbar.value }"
:title="navbarControl.showNavbar.value ? '隐藏顶部导航栏' : '显示顶部导航栏'"
>
<!-- 使用SVG图标表示菜单/关闭状态 -->
<svg v-if="navbarControl.showNavbar.value" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from "vue"; import { ref, onMounted, watch, inject, type Ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useLocalStorage } from '@vueuse/core'; // 添加VueUse导入 import { useLocalStorage } from '@vueuse/core'; // 添加VueUse导入
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui"; import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
@@ -115,7 +140,6 @@ import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
import BottomBar from "@/views/Project/BottomBar.vue"; import BottomBar from "@/views/Project/BottomBar.vue";
import RequestBoardDialog from "@/views/Project/RequestBoardDialog.vue"; import RequestBoardDialog from "@/views/Project/RequestBoardDialog.vue";
import { useProvideComponentManager } from "@/components/LabCanvas"; import { useProvideComponentManager } from "@/components/LabCanvas";
import type { DiagramData } from "@/components/LabCanvas";
import { useAlertStore } from "@/components/Alert"; import { useAlertStore } from "@/components/Alert";
import { AuthManager } from "@/utils/AuthManager"; import { AuthManager } from "@/utils/AuthManager";
import { useEquipments } from "@/stores/equipments"; import { useEquipments } from "@/stores/equipments";
@@ -133,6 +157,12 @@ const equipments = useEquipments();
const alert = useAlertStore(); const alert = useAlertStore();
// --- Navbar控制 ---
const navbarControl = inject('navbar') as {
showNavbar: Ref<boolean>;
toggleNavbar: () => void;
};
// --- 使用VueUse保存分栏状态 --- // --- 使用VueUse保存分栏状态 ---
// 左右分栏比例默认60% // 左右分栏比例默认60%
const horizontalSplitterSize = useLocalStorage('project-horizontal-splitter-size', 60); const horizontalSplitterSize = useLocalStorage('project-horizontal-splitter-size', 60);
@@ -182,29 +212,37 @@ async function toggleDocPanel() {
// 加载文档内容 // 加载文档内容
async function loadDocumentContent() { async function loadDocumentContent() {
try { try {
// 从路由参数中获取教程ID // 检查是否有实验ID参数
const tutorialId = (route.query.tutorial as string) || "02"; // 默认加载02例程 const examId = route.query.examId as string;
if (examId) {
// 如果有实验ID从API加载实验文档
console.log('加载实验文档:', examId);
const client = AuthManager.createAuthenticatedResourceClient();
// 构建文档路径 // 获取markdown类型的模板资源列表
let docPath = `/doc/${tutorialId}/doc.md`; const resources = await client.getResourceList(examId, 'doc', 'template');
// 检查当前路径是否包含下划线(例如 02_key 格式) if (resources && resources.length > 0) {
// 如果不包含,那么使用更新的命名格式 // 获取第一个markdown资源
if (!tutorialId.includes("_")) { const markdownResource = resources[0];
docPath = `/doc/${tutorialId}/doc.md`;
// 使用新的ResourceClient API获取资源文件内容
const response = await client.getResourceById(markdownResource.id);
if (!response || !response.data) {
throw new Error('获取markdown文件失败');
} }
// 获取文档内容 const content = await response.data.text();
const response = await fetch(docPath);
if (!response.ok) {
throw new Error(`Failed to load document: ${response.status}`);
}
// 更新文档内容,并替换图片路径 // 更新文档内容,暂时不处理图片路径由MarkdownRenderer处理
documentContent.value = (await response.text()).replace( documentContent.value = content;
/.\/images/gi, } else {
`/doc/${tutorialId}/images`, documentContent.value = "# 暂无实验文档\n\n该实验尚未提供文档内容。";
); }
} else {
documentContent.value = "# 无文档";
}
} catch (error) { } catch (error) {
console.error("加载文档失败:", error); console.error("加载文档失败:", error);
documentContent.value = "# 文档加载失败\n\n无法加载请求的文档。"; documentContent.value = "# 文档加载失败\n\n无法加载请求的文档。";
@@ -268,8 +306,8 @@ async function checkAndInitializeBoard() {
// 根据实验板信息更新equipment store // 根据实验板信息更新equipment store
function updateEquipmentFromBoard(board: Board) { function updateEquipmentFromBoard(board: Board) {
equipments.setAddr(board.ipAddr); equipments.boardAddr = board.ipAddr;
equipments.setPort(board.port); equipments.boardPort = board.port;
console.log(`实验板信息已更新到equipment store:`, { console.log(`实验板信息已更新到equipment store:`, {
address: board.ipAddr, address: board.ipAddr,
@@ -312,8 +350,8 @@ onMounted(async () => {
// 检查并初始化用户实验板 // 检查并初始化用户实验板
await checkAndInitializeBoard(); await checkAndInitializeBoard();
// 检查是否有例程参数,如果有则自动打开文档面板 // 检查是否有例程参数或实验ID参数,如果有则自动打开文档面板
if (route.query.tutorial) { if (route.query.tutorial || route.query.examId) {
showDocPanel.value = true; showDocPanel.value = true;
await loadDocumentContent(); await loadDocumentContent();
} }
@@ -344,7 +382,7 @@ onMounted(async () => {
} }
} }
/* 确保滚动行为仅在需要时出现 */ /* 确保整个页面禁止滚动 */
html, html,
body { body {
overflow: hidden; overflow: hidden;
@@ -376,7 +414,42 @@ body {
:deep(.markdown-content) { :deep(.markdown-content) {
padding: 1rem; padding: 1rem;
background-color: hsl(var(--b1)); background-color: hsl(var(--b1));
border-radius: 0.5rem; }
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
/* Navbar切换浮动按钮样式 */
.navbar-toggle-btn {
position: fixed;
top: 20px;
left: 20px;
z-index: 1000;
transition: all 0.3s ease-in-out;
}
/* 当Navbar显示时调整按钮位置 */
.navbar-toggle-btn.with-navbar {
top: 80px; /* 调整到Navbar下方 */
}
.navbar-toggle-btn button {
backdrop-filter: blur(10px);
background: rgba(var(--p), 0.9);
border: 2px solid rgba(var(--p), 0.3);
transition: all 0.3s ease-in-out;
}
.navbar-toggle-btn button:hover {
transform: scale(1.1);
background: rgba(var(--p), 1);
}
.navbar-toggle-btn button.btn-outline {
background: rgba(var(--b1), 0.9);
color: hsl(var(--p));
border: 2px solid rgba(var(--p), 0.5);
}
.navbar-toggle-btn button.btn-outline:hover {
background: rgba(var(--p), 0.1);
border: 2px solid rgba(var(--p), 0.8);
} }
</style> </style>

View File

@@ -9,8 +9,42 @@
逻辑信号分析 逻辑信号分析
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- 空闲状态只显示开始捕获按钮 -->
<button
v-if="!analyzer.isCapturing.value"
@click="analyzer.startCapture"
:disabled="analyzer.isApplying.value"
class="btn btn-sm btn-primary"
>
开始捕获
</button>
<!-- 捕获状态显示停止捕获和强制捕获按钮 -->
<button
v-if="analyzer.isCapturing.value"
@click="analyzer.stopCapture"
class="btn btn-sm btn-warning"
>
<span class="loading loading-spinner loading-sm"></span>
停止捕获
</button>
<button
v-if="analyzer.isCapturing.value"
@click="analyzer.forceCapture"
class="btn btn-sm btn-secondary"
>
强制捕获
</button>
<!-- 其他按钮保持不变 -->
<button
@click="analyzer.generateTestData"
class="btn btn-sm btn-info"
>
测试数据
</button>
<button class="btn btn-sm btn-error" @click="handleDeleteData"> <button class="btn btn-sm btn-error" @click="handleDeleteData">
清空 清空数据
</button> </button>
</div> </div>
</h2> </h2>
@@ -21,9 +55,45 @@
<!-- 触发设置 --> <!-- 触发设置 -->
<div class="card bg-base-200 shadow-xl mx-5"> <div class="card bg-base-200 shadow-xl mx-5">
<div class="card-body"> <div class="card-body">
<h2 class="card-title"> <h2 class="card-title flex justify-between items-center">
<div class="flex gap-8">
<div class="flex items-center gap-2">
<Settings class="w-5 h-5" /> <Settings class="w-5 h-5" />
触发设置 触发设置
</div>
<!-- 配置摘要 -->
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>{{ analyzer.enabledChannelCount.value }}/32 通道</span>
<span>捕获: {{ analyzer.captureLength.value }}</span>
<span>预捕获: {{ analyzer.preCaptureLength.value }}</span>
<span>{{ analyzer.globalModes.find(m => m.value === analyzer.currentGlobalMode.value)?.label || '未知' }}</span>
</div>
</div>
<div class="flex items-center gap-4">
<!-- 状态指示 -->
<div class="flex items-center gap-2 text-sm">
<span
v-if="analyzer.isCapturing.value"
class="flex items-center gap-1 text-warning"
>
<span class="loading loading-spinner loading-xs"></span>
捕获中
</span>
<span
v-else-if="analyzer.isApplying.value"
class="flex items-center gap-1 text-info"
>
<span class="loading loading-spinner loading-xs"></span>
配置中
</span>
<span
v-else
class="text-success"
>
就绪
</span>
</div>
</div>
</h2> </h2>
<TriggerSettings /> <TriggerSettings />
</div> </div>

View File

@@ -1,13 +1,13 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from "node:url";
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import vue from '@vitejs/plugin-vue' import vue from "@vitejs/plugin-vue";
import vueJsx from '@vitejs/plugin-vue-jsx' import vueJsx from "@vitejs/plugin-vue-jsx";
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from "vite-plugin-vue-devtools";
import tailwindcss from '@tailwindcss/postcss' import tailwindcss from "@tailwindcss/postcss";
import autoprefixer from 'autoprefixer' import autoprefixer from "autoprefixer";
import Components from 'unplugin-vue-components/vite' import Components from "unplugin-vue-components/vite";
import RekaResolver from 'reka-ui/resolver' import RekaResolver from "reka-ui/resolver";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
@@ -16,49 +16,44 @@ export default defineConfig({
template: { template: {
compilerOptions: { compilerOptions: {
// 将所有 wokwi- 开头的标签视为自定义元素 // 将所有 wokwi- 开头的标签视为自定义元素
isCustomElement: (tag) => tag.startsWith('wokwi-') isCustomElement: (tag) => tag.startsWith("wokwi-"),
} },
} },
}), }),
vueJsx(), vueJsx(),
vueDevTools(), vueDevTools(),
Components( Components({
{
dts: true, dts: true,
resolvers: [ resolvers: [
RekaResolver() RekaResolver(),
// RekaResolver({ // RekaResolver({
// prefix: '' // use the prefix option to add Prefix to the imported components // prefix: '' // use the prefix option to add Prefix to the imported components
// }) // })
], ],
} }),
)
], ],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) "@": fileURLToPath(new URL("./src", import.meta.url)),
}, },
}, },
css: { css: {
postcss: { postcss: {
plugins: [ plugins: [tailwindcss(), autoprefixer()],
tailwindcss(), },
autoprefixer()
]
}
}, },
build: { build: {
outDir: 'wwwroot', outDir: "wwwroot",
emptyOutDir: true, // also necessary emptyOutDir: true, // also necessary
}, },
server: { server: {
proxy: { proxy: {
"/swagger": { "/swagger": {
target: 'http://localhost:5000', target: "http://localhost:5000",
changeOrigin: true changeOrigin: true,
} },
}, },
port: 5173, port: 5173,
} },
}) });