Compare commits
177 Commits
refactor
...
2d77706013
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d77706013 | ||
|
|
c564844673 | ||
| 2adeca3b99 | |||
| bafd06162c | |||
| 8c404d4072 | |||
|
|
d27b5d7737 | ||
|
|
4df583e74b | ||
| 1ca9999f15 | |||
|
|
0cc35ce541 | ||
|
|
d7c02ee6c9 | ||
|
|
6b701658d1 | ||
| 2f1be8b0b7 | |||
| 82bc03b9fb | |||
| 3257a68407 | |||
|
|
6dfd275091 | ||
| 6c5250f9c2 | |||
| 912eb625f5 | |||
| 10e4a82e5b | |||
| ef267721fd | |||
|
|
1d35c36da6 | ||
| 3da0f284f3 | |||
| 23d4459406 | |||
| a4192659d1 | |||
| f200d90fc0 | |||
| 6c1bda50ce | |||
| 3535b94123 | |||
| 5da9d9f4e2 | |||
| e7c8d3fb9e | |||
| e872f24936 | |||
| d1c9710afe | |||
|
|
422aaa89d5 | ||
|
|
5103145d01 | ||
|
|
27c8ceb1db | ||
| d30712d0f6 | |||
| a56a65cc0d | |||
| 9c7bde206b | |||
|
|
1fa944f3c7 | ||
|
|
1492f16fdd | ||
| 042ca40998 | |||
| e4a1c34a6c | |||
|
|
ba79a2093b | ||
| 35bad4027d | |||
| 9af2d3e87e | |||
| e9ad1f0256 | |||
| 12cd35edff | |||
| 69c7cbf4d8 | |||
| 80b6dfb38d | |||
| 0f4386457d | |||
| 08a9be543e | |||
| 688fe05b1b | |||
| 2ff735e06a | |||
| 53eaac43e3 | |||
| f5dd474ba0 | |||
|
|
e4ead72d53 | ||
| fb13a5c484 | |||
| 1053d71d29 | |||
| 56dcbf5caa | |||
| dfe279bf37 | |||
| e3b769b24e | |||
|
|
8e19587a16 | ||
| d551cbe793 | |||
| 822091243e | |||
| bcee42d8c1 | |||
| 9165c2e5f4 | |||
| 8070e03496 | |||
| 43e3cce048 | |||
| bcdefb2779 | |||
| 519094b3a0 | |||
| 57cf82b48f | |||
| b08b86dbbe | |||
| 0cfbebf804 | |||
| 446da52515 | |||
|
|
c70cc46aa9 | ||
|
|
0f850c3ae7 | ||
| 99dc7b52cc | |||
| 0410d14d3a | |||
|
|
474151d412 | ||
|
|
4562be2d01 | ||
| ef76f3e9c7 | |||
|
|
a28ae9be97 | ||
| 9f25391540 | |||
|
|
938ee80979 | ||
|
|
4af7da6344 | ||
|
|
4b140ef683 | ||
| b139542c4c | |||
| be8fed995c | |||
| 49cbdc51d9 | |||
|
|
705e322e41 | ||
|
|
0f3754ce99 | ||
| 89cc2291c0 | |||
|
|
683d918d30 | ||
|
|
d901a440a7 | ||
| 6500c1ce2d | |||
| c9fc6961fa | |||
| 4d6c06a0e0 | |||
|
|
533e2561ab | ||
| e8a16fd446 | |||
| ca906489c2 | |||
|
|
2b1ee90af7 | ||
| 2894ee24be | |||
| 6068a10d67 | |||
|
|
c5f0e706a4 | ||
|
|
9b580be5e9 | ||
| 1273be7dee | |||
| 78737f6839 | |||
| e38770a496 | |||
| a76ee74656 | |||
|
|
f710a66c69 | ||
|
|
4e5dc91f10 | ||
| 8221f8e133 | |||
| bad64bdfbd | |||
|
|
c29c3652bc | ||
|
|
352ee1f4f2 | ||
| 32b126b93f | |||
| b913f58f13 | |||
|
|
0350ce8829 | ||
|
|
229e6e70ed | ||
| eebc5105a0 | |||
| 15c6eefe30 | |||
| 28af2df093 | |||
|
|
e25f08739a | ||
| f253a33c83 | |||
| 0fb0c4e395 | |||
| 44e357b887 | |||
| 50ffd491fe | |||
| e0619eb9a3 | |||
| da6386c6f0 | |||
| 8789d6f9ee | |||
| 546b9250fa | |||
| 3f2c772eeb | |||
| fae07d9eae | |||
| eedec80927 | |||
| b4bb563782 | |||
| d88c710606 | |||
| bdffba7576 | |||
|
|
d83bc250bd | ||
| 285d3e8585 | |||
|
|
8a1d6e52cb | ||
| 33a2dbf437 | |||
| 4a5709a783 | |||
| d6167ac286 | |||
| c6c3f1cc41 | |||
| 540f5c788d | |||
| 558a139593 | |||
| fad37ba922 | |||
| c7c8cbaeb8 | |||
| 15f9b68e7d | |||
| 48501d79e2 | |||
| bbad7388d8 | |||
| cbb3543c4a | |||
| 53027470fe | |||
| 2a766c3f6b | |||
| 497fa731ca | |||
| 443aea5e3e | |||
| 67bdec8570 | |||
| 1af3fa3a8f | |||
| dd7efe3c84 | |||
|
|
23236b22bd | ||
|
|
ef1a6c8208 | ||
| ff7f7b5a76 | |||
| da7b3f4a4b | |||
| a9ab5926ed | |||
| 2e084bfb58 | |||
| 221d598a6e | |||
| 287c416247 | |||
| e84a784517 | |||
| 178ac0de67 | |||
| bed0158a5f | |||
| 7ffb15c722 | |||
|
|
5ba71d220f | ||
|
|
4c14ada97b | ||
| 81f91b2b71 | |||
|
|
bbfe06822d | ||
|
|
d73166187a | ||
|
|
2eabb79d0f | ||
|
|
a865cfc950 | ||
| fa7c947351 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,6 +18,7 @@ coverage
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
DebuggerCmd.md
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
@@ -27,10 +28,10 @@ coverage
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Generated Files
|
||||
*.sqlite
|
||||
components.d.ts
|
||||
|
||||
10
.justfile
10
.justfile
@@ -15,11 +15,15 @@ clean:
|
||||
rm -rf "dist"
|
||||
rm -rf "wwwroot"
|
||||
|
||||
update:
|
||||
npm install
|
||||
dotnet restore ./server/server.csproj
|
||||
update: update-node update-dotnet
|
||||
git submodule update --init --remote --recursive
|
||||
|
||||
update-node:
|
||||
npm install
|
||||
|
||||
update-dotnet:
|
||||
dotnet restore ./server/server.csproj
|
||||
|
||||
# 生成Restful API到网页客户端
|
||||
gen-api:
|
||||
npm run gen-api
|
||||
|
||||
48
FPGAWebLabServer.sln
Normal file
48
FPGAWebLabServer.sln
Normal 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
|
||||
13
TODO.md
Normal file
13
TODO.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# TODO
|
||||
|
||||
1. 后端HTTP视频流
|
||||
|
||||
640*480, RGB565
|
||||
0x0000_0000 + 25800
|
||||
|
||||
|
||||
2. 信号发生器界面导入.dat文件
|
||||
3. 示波器后端交互、前端界面
|
||||
4. 逻辑分析仪后端交互、前端界面
|
||||
5. 前端重构
|
||||
6. 数据库 —— 用户登录、板卡资源分配、板卡IP地址分配
|
||||
56
components.d.ts
vendored
56
components.d.ts
vendored
@@ -1,56 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
BaseBoard: typeof import('./src/components/equipments/BaseBoard.vue')['default']
|
||||
Canvas: typeof import('./src/components/Canvas.vue')['default']
|
||||
CollapsibleSection: typeof import('./src/components/CollapsibleSection.vue')['default']
|
||||
ComponentSelector: typeof import('./src/components/LabCanvas/ComponentSelector.vue')['default']
|
||||
DDR: typeof import('./src/components/equipments/DDR.vue')['default']
|
||||
DDS: typeof import('./src/components/equipments/DDS.vue')['default']
|
||||
DDSPropertyEditor: typeof import('./src/components/equipments/DDSPropertyEditor.vue')['default']
|
||||
DiagramCanvas: typeof import('./src/components/LabCanvas/DiagramCanvas.vue')['default']
|
||||
Dialog: typeof import('./src/components/Dialog.vue')['default']
|
||||
ETH: typeof import('./src/components/equipments/ETH.vue')['default']
|
||||
HDMI: typeof import('./src/components/equipments/HDMI.vue')['default']
|
||||
LabCanvas: typeof import('./src/components/LabCanvasNew/LabCanvas.vue')['default']
|
||||
LabCanvasNew: typeof import('./src/components/LabCanvas/LabCanvasNew.vue')['default']
|
||||
LabComponentsDrawer: typeof import('./src/components/LabCanvasNew/LabComponentsDrawer.vue')['default']
|
||||
LabComponentsDrawerNew: typeof import('./src/components/LabCanvas/LabComponentsDrawerNew.vue')['default']
|
||||
LoginCard: typeof import('./src/components/LoginCard.vue')['default']
|
||||
MarkdownRenderer: typeof import('./src/components/MarkdownRenderer.vue')['default']
|
||||
MechanicalButton: typeof import('./src/components/equipments/MechanicalButton.vue')['default']
|
||||
MotherBoard: typeof import('./src/components/equipments/MotherBoard.vue')['default']
|
||||
MotherBoardCaps: typeof import('./src/components/equipments/MotherBoardCaps.vue')['default']
|
||||
Navbar: typeof import('./src/components/Navbar.vue')['default']
|
||||
PG2L100H_FBG676: typeof import('./src/components/equipments/PG2L100H_FBG676.vue')['default']
|
||||
Pin: typeof import('./src/components/equipments/Pin.vue')['default']
|
||||
PopButton: typeof import('./src/components/PopButton.vue')['default']
|
||||
PropertyEditor: typeof import('./src/components/PropertyEditor.vue')['default']
|
||||
PropertyPanel: typeof import('./src/components/PropertyPanel.vue')['default']
|
||||
RekaSplitterGroup: typeof import('reka-ui')['SplitterGroup']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SD: typeof import('./src/components/equipments/SD.vue')['default']
|
||||
SevenSegmentDisplay: typeof import('./src/components/equipments/SevenSegmentDisplay.vue')['default']
|
||||
SFP: typeof import('./src/components/equipments/SFP.vue')['default']
|
||||
Sidebar: typeof import('./src/components/Sidebar.vue')['default']
|
||||
SMA: typeof import('./src/components/equipments/SMA.vue')['default']
|
||||
SMT_LED: typeof import('./src/components/equipments/SMT_LED.vue')['default']
|
||||
SplitterGroup: typeof import('reka-ui')['SplitterGroup']
|
||||
SplitterPanel: typeof import('reka-ui')['SplitterPanel']
|
||||
SplitterResizeHandle: typeof import('reka-ui')['SplitterResizeHandle']
|
||||
Switch: typeof import('./src/components/equipments/Switch.vue')['default']
|
||||
ThemeControlButton: typeof import('./src/components/ThemeControlButton.vue')['default']
|
||||
ThemeControlToggle: typeof import('./src/components/ThemeControlToggle.vue')['default']
|
||||
TutorialCarousel: typeof import('./src/components/TutorialCarousel.vue')['default']
|
||||
UploadCard: typeof import('./src/components/UploadCard.vue')['default']
|
||||
Wire: typeof import('./src/components/equipments/Wire.vue')['default']
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
sqls
|
||||
sql-studio
|
||||
zlib
|
||||
bash
|
||||
# Backend
|
||||
(dotnetCorePackages.combinePackages [
|
||||
dotnetCorePackages.sdk_9_0
|
||||
|
||||
741
package-lock.json
generated
741
package-lock.json
generated
@@ -9,10 +9,14 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"axios": "^1.11.0",
|
||||
"echarts": "^5.6.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"konva": "^9.3.20",
|
||||
"lodash": "^4.17.21",
|
||||
"log-symbols": "^7.0.0",
|
||||
"lucide-vue-next": "^0.525.0",
|
||||
@@ -23,6 +27,8 @@
|
||||
"ts-log": "^2.2.7",
|
||||
"ts-results-es": "^5.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-konva": "^3.2.1",
|
||||
"vue-router": "4",
|
||||
"yocto-queue": "^1.2.1",
|
||||
"zod": "^3.24.2"
|
||||
@@ -36,10 +42,13 @@
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"daisyui": "^5.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"nswag": "^14.3.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "~5.7.3",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^6.1.0",
|
||||
@@ -538,6 +547,30 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
|
||||
@@ -1712,6 +1745,19 @@
|
||||
"tailwindcss": "4.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
@@ -1722,6 +1768,25 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/vue-table": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/vue-table/-/vue-table-8.21.3.tgz",
|
||||
"integrity": "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/table-core": "8.21.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/vue-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz",
|
||||
@@ -1738,6 +1803,34 @@
|
||||
"vue": "^2.7.0 || ^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node22": {
|
||||
"version": "22.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.1.tgz",
|
||||
@@ -2177,6 +2270,19 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/alien-signals": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
|
||||
@@ -2224,6 +2330,13 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||
@@ -2245,6 +2358,12 @@
|
||||
"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": {
|
||||
"version": "10.4.21",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||
@@ -2283,6 +2402,17 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -2384,6 +2514,19 @@
|
||||
"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": {
|
||||
"version": "1.0.30001715",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
|
||||
@@ -2430,6 +2573,18 @@
|
||||
"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": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz",
|
||||
@@ -2472,6 +2627,13 @@
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2526,6 +2688,16 @@
|
||||
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
@@ -2606,6 +2778,15 @@
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"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": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
@@ -2616,6 +2797,46 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
|
||||
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "5.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts/node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.140",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.140.tgz",
|
||||
@@ -2659,6 +2880,51 @@
|
||||
"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": {
|
||||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
|
||||
@@ -2771,6 +3037,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
|
||||
@@ -2800,6 +3090,55 @@
|
||||
"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": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
@@ -2844,6 +3183,15 @@
|
||||
"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": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@@ -2854,6 +3202,43 @@
|
||||
"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": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
||||
@@ -2871,6 +3256,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
|
||||
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
@@ -2894,6 +3292,18 @@
|
||||
"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": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -2901,6 +3311,45 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
@@ -3172,6 +3621,26 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/konva": {
|
||||
"version": "9.3.20",
|
||||
"resolved": "https://registry.npmjs.org/konva/-/konva-9.3.20.tgz",
|
||||
"integrity": "sha512-7XPD/YtgfzC8b1c7z0hhY5TF1IO/pBYNa29zMTA2PeBaqI0n5YplUeo4JRuRcljeAF8lWtW65jePZZF7064c8w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/lavrton"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/konva"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/lavrton"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.29.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
|
||||
@@ -3479,6 +3948,13 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
|
||||
@@ -3491,6 +3967,15 @@
|
||||
"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": {
|
||||
"version": "14.4.0",
|
||||
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.4.0.tgz",
|
||||
@@ -3536,6 +4021,27 @@
|
||||
"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": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
@@ -3632,6 +4138,46 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
@@ -3923,6 +4469,12 @@
|
||||
"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/quansync": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
|
||||
@@ -4025,6 +4577,16 @@
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
@@ -4276,6 +4838,50 @@
|
||||
"integrity": "sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-results-es": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-results-es/-/ts-results-es-5.0.1.tgz",
|
||||
@@ -4288,6 +4894,26 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.20.3",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz",
|
||||
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-function": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz",
|
||||
@@ -4447,6 +5073,13 @@
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
@@ -4638,6 +5271,79 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-demi": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
||||
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-echarts": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-7.0.3.tgz",
|
||||
"integrity": "sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": "^0.13.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/runtime-core": "^3.0.0",
|
||||
"echarts": "^5.5.1",
|
||||
"vue": "^2.7.0 || ^3.1.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/runtime-core": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-konva": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-konva/-/vue-konva-3.2.1.tgz",
|
||||
"integrity": "sha512-gLF+VYnlrBfwtaN3NkgzzEqlj9nyCll80VZv2DdvLUM3cisUsdcRJJuMwGTBJOTebcnn6MB22r33IFd2m+m/ig==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/lavrton"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/konva"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/lavrton"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"konva": ">7",
|
||||
"vue": "^3"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz",
|
||||
@@ -4676,6 +5382,16 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-virtual-modules": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||
@@ -4706,6 +5422,16 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
|
||||
@@ -4738,6 +5464,21 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
|
||||
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender/node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
package.json
13
package.json
@@ -9,16 +9,18 @@
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"pregen-api": "cd server && dotnet run --property:Configuration=Release &",
|
||||
"gen-api": "npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts",
|
||||
"postgen-api": "pkill server"
|
||||
"gen-api": "npx tsx scripts/GenerateWebAPI.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"axios": "^1.11.0",
|
||||
"echarts": "^5.6.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"konva": "^9.3.20",
|
||||
"lodash": "^4.17.21",
|
||||
"log-symbols": "^7.0.0",
|
||||
"lucide-vue-next": "^0.525.0",
|
||||
@@ -29,6 +31,8 @@
|
||||
"ts-log": "^2.2.7",
|
||||
"ts-results-es": "^5.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-konva": "^3.2.1",
|
||||
"vue-router": "4",
|
||||
"yocto-queue": "^1.2.1",
|
||||
"zod": "^3.24.2"
|
||||
@@ -42,10 +46,13 @@
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"daisyui": "^5.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"nswag": "^14.3.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "~5.7.3",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^6.1.0",
|
||||
|
||||
419
scripts/GenerateWebAPI.ts
Normal file
419
scripts/GenerateWebAPI.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
import { spawn, exec, ChildProcess } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import fetch from 'node-fetch';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Windows 支持函数
|
||||
function getCommand(command: string): string {
|
||||
// dotnet 在 Windows 上不需要 .cmd 后缀
|
||||
if (command === 'dotnet') {
|
||||
return 'dotnet';
|
||||
}
|
||||
return process.platform === 'win32' ? `${command}.cmd` : command;
|
||||
}
|
||||
|
||||
function getSpawnOptions() {
|
||||
return process.platform === 'win32' ? { stdio: 'pipe', shell: true } : { stdio: 'pipe' };
|
||||
}
|
||||
|
||||
async function waitForServer(url: string, maxRetries: number = 30, interval: number = 1000): Promise<boolean> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
console.log('✓ Server is ready');
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
// Server not ready yet
|
||||
}
|
||||
console.log(`Waiting for server... (${i + 1}/${maxRetries})`);
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 改进全局变量类型
|
||||
let serverProcess: ChildProcess | null = null;
|
||||
let webProcess: ChildProcess | null = null;
|
||||
|
||||
async function startWeb(): Promise<ChildProcess> {
|
||||
console.log('Starting Vite frontend...');
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn(getCommand('npm'), ['run', 'dev'], getSpawnOptions() as any);
|
||||
|
||||
let webStarted = false;
|
||||
|
||||
process.stdout?.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
console.log(`Web: ${output}`);
|
||||
|
||||
// 检查 Vite 是否已启动
|
||||
if ((output.includes('Local:') || output.includes('ready in')) && !webStarted) {
|
||||
webStarted = true;
|
||||
resolve(process);
|
||||
}
|
||||
});
|
||||
|
||||
process.stderr?.on('data', (data) => {
|
||||
console.error(`Web Error: ${data}`);
|
||||
});
|
||||
|
||||
process.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
process.on('exit', (code, signal) => {
|
||||
console.log(`Web process exited with code ${code} and signal ${signal}`);
|
||||
if (!webStarted) {
|
||||
reject(new Error(`Web process exited unexpectedly with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
// 存储进程引用
|
||||
webProcess = process;
|
||||
|
||||
// 超时处理
|
||||
setTimeout(() => {
|
||||
if (!webStarted) {
|
||||
reject(new Error('Web server failed to start within timeout'));
|
||||
}
|
||||
}, 30000); // 30秒超时
|
||||
});
|
||||
}
|
||||
|
||||
async function startServer(): Promise<ChildProcess> {
|
||||
console.log('Starting .NET server...');
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn(getCommand('dotnet'), ['run', '--property:Configuration=Release'], {
|
||||
cwd: 'server',
|
||||
...getSpawnOptions()
|
||||
} as any);
|
||||
|
||||
let serverStarted = false;
|
||||
|
||||
process.stdout?.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
console.log(`Server: ${output}`);
|
||||
|
||||
// 检查服务器是否已启动
|
||||
if (output.includes('Now listening on:') && !serverStarted) {
|
||||
serverStarted = true;
|
||||
resolve(process);
|
||||
}
|
||||
});
|
||||
|
||||
process.stderr?.on('data', (data) => {
|
||||
console.error(`Server Error: ${data}`);
|
||||
});
|
||||
|
||||
process.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
process.on('exit', (code, signal) => {
|
||||
console.log(`Server process exited with code ${code} and signal ${signal}`);
|
||||
if (!serverStarted) {
|
||||
reject(new Error(`Server process exited unexpectedly with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
// 存储进程引用
|
||||
serverProcess = process;
|
||||
|
||||
// 超时处理
|
||||
setTimeout(() => {
|
||||
if (!serverStarted) {
|
||||
reject(new Error('Server failed to start within timeout'));
|
||||
}
|
||||
}, 30000); // 30秒超时
|
||||
});
|
||||
}
|
||||
|
||||
async function stopServer(): Promise<void> {
|
||||
console.log('Stopping server...');
|
||||
|
||||
if (!serverProcess) {
|
||||
console.log('No server process to stop');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查进程是否还存在
|
||||
if (serverProcess.killed || serverProcess.exitCode !== null) {
|
||||
console.log('✓ Server process already terminated');
|
||||
serverProcess = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送 SIGTERM 信号
|
||||
const killed = serverProcess.kill('SIGTERM');
|
||||
if (!killed) {
|
||||
console.warn('Failed to send SIGTERM to server process');
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待进程优雅退出
|
||||
const exitPromise = new Promise<void>((resolve) => {
|
||||
if (serverProcess) {
|
||||
serverProcess.on('exit', () => {
|
||||
console.log('✓ Server stopped gracefully');
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
// 设置超时,如果 3 秒内没有退出则强制终止
|
||||
const timeoutPromise = new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
if (serverProcess && !serverProcess.killed && serverProcess.exitCode === null) {
|
||||
console.log('Force killing server process...');
|
||||
serverProcess.kill('SIGKILL');
|
||||
}
|
||||
resolve();
|
||||
}, 3000); // 减少超时时间到3秒
|
||||
});
|
||||
|
||||
await Promise.race([exitPromise, timeoutPromise]);
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Warning: Could not stop server process:', error);
|
||||
} finally {
|
||||
serverProcess = null;
|
||||
|
||||
// 只有在进程可能没有正常退出时才执行清理
|
||||
// 移除自动清理逻辑,因为正常退出时不需要
|
||||
}
|
||||
}
|
||||
|
||||
async function stopWeb(): Promise<void> {
|
||||
console.log('Stopping web server...');
|
||||
|
||||
if (!webProcess) {
|
||||
console.log('No web process to stop');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查进程是否还存在
|
||||
if (webProcess.killed || webProcess.exitCode !== null) {
|
||||
console.log('✓ Web process already terminated');
|
||||
webProcess = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送 SIGTERM 信号
|
||||
const killed = webProcess.kill('SIGTERM');
|
||||
if (!killed) {
|
||||
console.warn('Failed to send SIGTERM to web process');
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待进程优雅退出
|
||||
const exitPromise = new Promise<void>((resolve) => {
|
||||
if (webProcess) {
|
||||
webProcess.on('exit', () => {
|
||||
console.log('✓ Web server stopped gracefully');
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
// 设置超时,如果 3 秒内没有退出则强制终止
|
||||
const timeoutPromise = new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
if (webProcess && !webProcess.killed && webProcess.exitCode === null) {
|
||||
console.log('Force killing web process...');
|
||||
webProcess.kill('SIGKILL');
|
||||
}
|
||||
resolve();
|
||||
}, 3000); // 减少超时时间到3秒
|
||||
});
|
||||
|
||||
await Promise.race([exitPromise, timeoutPromise]);
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Warning: Could not stop web process:', error);
|
||||
} finally {
|
||||
webProcess = null;
|
||||
|
||||
// 只有在进程可能没有正常退出时才执行清理
|
||||
// 移除自动清理逻辑,因为正常退出时不需要
|
||||
}
|
||||
}
|
||||
|
||||
async function postProcessApiClient(): Promise<void> {
|
||||
console.log('Post-processing API client...');
|
||||
try {
|
||||
const filePath = 'src/APIClient.ts';
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`API client file not found: ${filePath}`);
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// 替换 ArgumentException 中的 message 属性声明
|
||||
content = content.replace(
|
||||
/(\s+)message!:\s*string;/g,
|
||||
'$1declare message: string;'
|
||||
);
|
||||
|
||||
// 写回文件
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
|
||||
console.log('✓ API client post-processing completed');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to post-process API client: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateApiClient(): Promise<void> {
|
||||
console.log('Generating API client...');
|
||||
try {
|
||||
const url = 'http://127.0.0.1:5000/GetAPIClientCode';
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch API client code: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const code = await response.text();
|
||||
|
||||
// 写入 APIClient.ts
|
||||
const filePath = 'src/APIClient.ts';
|
||||
fs.writeFileSync(filePath, code, 'utf8');
|
||||
console.log('✓ API client code fetched and written successfully');
|
||||
|
||||
// 添加后处理步骤
|
||||
await postProcessApiClient();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to generate API client: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
// Start web frontend first
|
||||
await startWeb();
|
||||
console.log('✓ Frontend started');
|
||||
|
||||
// Wait a bit for frontend to fully initialize
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
// Start server
|
||||
await startServer();
|
||||
console.log('✓ Backend started');
|
||||
|
||||
// Wait for server to be ready (给服务器额外时间完全启动)
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Check if swagger endpoint is available
|
||||
const serverReady = await waitForServer('http://localhost:5000/swagger/v1/swagger.json');
|
||||
|
||||
if (!serverReady) {
|
||||
throw new Error('Server failed to start within the expected time');
|
||||
}
|
||||
|
||||
// Generate API client
|
||||
await generateApiClient();
|
||||
|
||||
console.log('✓ API generation completed successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Always try to stop processes in order: server first, then web
|
||||
await stopServer();
|
||||
await stopWeb();
|
||||
}
|
||||
}
|
||||
|
||||
// 改进的进程终止处理 - 添加防重复执行
|
||||
let isCleaningUp = false;
|
||||
|
||||
const cleanup = async (signal: string) => {
|
||||
if (isCleaningUp) {
|
||||
console.log('Cleanup already in progress, ignoring signal');
|
||||
return;
|
||||
}
|
||||
|
||||
isCleaningUp = true;
|
||||
console.log(`\nReceived ${signal}, cleaning up...`);
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
stopServer(),
|
||||
stopWeb()
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error during cleanup:', error);
|
||||
}
|
||||
|
||||
// 立即退出,不等待
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => cleanup('SIGINT'));
|
||||
process.on('SIGTERM', () => cleanup('SIGTERM'));
|
||||
|
||||
// 处理未捕获的异常
|
||||
process.on('uncaughtException', async (error) => {
|
||||
if (isCleaningUp) return;
|
||||
|
||||
console.error('❌ Uncaught exception:', error);
|
||||
isCleaningUp = true;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
stopServer(),
|
||||
stopWeb()
|
||||
]);
|
||||
} catch (cleanupError) {
|
||||
console.error('Error during cleanup:', cleanupError);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', async (reason, promise) => {
|
||||
if (isCleaningUp) return;
|
||||
|
||||
console.error('❌ Unhandled rejection at:', promise, 'reason:', reason);
|
||||
isCleaningUp = true;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
stopServer(),
|
||||
stopWeb()
|
||||
]);
|
||||
} catch (cleanupError) {
|
||||
console.error('Error during cleanup:', cleanupError);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
main().catch(async (error) => {
|
||||
if (isCleaningUp) return;
|
||||
|
||||
console.error('❌ Unhandled error:', error);
|
||||
isCleaningUp = true;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
stopServer(),
|
||||
stopWeb()
|
||||
]);
|
||||
} catch (cleanupError) {
|
||||
console.error('Error during cleanup:', cleanupError);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -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
286
server.test/NumberTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
using NJsonSchema.CodeGeneration.TypeScript;
|
||||
using NLog;
|
||||
using NLog.Web;
|
||||
using NSwag;
|
||||
using NSwag.CodeGeneration.TypeScript;
|
||||
using NSwag.Generation.Processors.Security;
|
||||
using server.Services;
|
||||
|
||||
// Early init of NLog to allow startup and exception logging, before host is built
|
||||
var logger = NLog.LogManager.Setup()
|
||||
@@ -36,6 +45,37 @@ try
|
||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
});
|
||||
|
||||
// Add JWT Token Authorization
|
||||
builder.Services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
RequireExpirationTime = true,
|
||||
ValidIssuer = "dlut.edu.cn",
|
||||
ValidAudience = "dlut.edu.cn",
|
||||
IssuerSigningKey = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes("my secret key 1234567890my secret key 1234567890")),
|
||||
};
|
||||
options.Authority = $"http://{Global.localhost}:5000";
|
||||
options.RequireHttpsMetadata = false;
|
||||
});
|
||||
// Add JWT Token Authorization Policy
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("Admin", policy =>
|
||||
{
|
||||
policy.RequireClaim(ClaimTypes.Role, new string[] {
|
||||
Database.User.UserPermission.Admin.ToString(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add CORS policy
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -52,13 +92,13 @@ try
|
||||
{
|
||||
options.AddPolicy("Users", policy => policy
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
);
|
||||
});
|
||||
|
||||
// Add Swagger
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddOpenApiDocument(options =>
|
||||
builder.Services.AddSwaggerDocument(options =>
|
||||
{
|
||||
options.PostProcess = document =>
|
||||
{
|
||||
@@ -80,8 +120,23 @@ try
|
||||
// }
|
||||
};
|
||||
};
|
||||
|
||||
// Authorization
|
||||
options.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme
|
||||
{
|
||||
Description = "请输入token,格式为 Bearer xxxxxxxx(注意中间必须有空格)",
|
||||
Name = "Authorization",
|
||||
In = NSwag.OpenApiSecurityApiKeyLocation.Header,
|
||||
Type = NSwag.OpenApiSecuritySchemeType.ApiKey,
|
||||
});
|
||||
options.OperationProcessors.Add(new OperationSecurityScopeProcessor("Bearer"));
|
||||
});
|
||||
|
||||
|
||||
// 添加 HTTP 视频流服务
|
||||
builder.Services.AddSingleton<HttpVideoStreamService>();
|
||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpVideoStreamService>());
|
||||
|
||||
// Application Settings
|
||||
var app = builder.Build();
|
||||
// Configure the HTTP request pipeline.
|
||||
@@ -95,12 +150,14 @@ try
|
||||
logger.Info($"Use Static Files : {Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")}");
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles(); // Serves files from wwwroot by default
|
||||
|
||||
// Assets Files
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "assets")),
|
||||
RequestPath = "/assets"
|
||||
});
|
||||
|
||||
// Log Files
|
||||
if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "log")))
|
||||
{
|
||||
@@ -111,16 +168,35 @@ try
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")),
|
||||
RequestPath = "/log"
|
||||
});
|
||||
|
||||
// Exam Files (实验静态资源)
|
||||
if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "exam")))
|
||||
{
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "exam")),
|
||||
RequestPath = "/exam"
|
||||
});
|
||||
}
|
||||
|
||||
app.MapFallbackToFile("index.html");
|
||||
}
|
||||
// Add logs
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// Swagger
|
||||
app.UseOpenApi();
|
||||
app.UseOpenApi(settings =>
|
||||
{
|
||||
settings.PostProcess = (document, httpRequest) =>
|
||||
{
|
||||
document.Servers.Clear();
|
||||
document.Servers.Add(new NSwag.OpenApiServer { Url = $"http://{Global.localhost}:5000" });
|
||||
};
|
||||
});
|
||||
app.UseSwaggerUi();
|
||||
|
||||
// Router
|
||||
@@ -129,6 +205,34 @@ try
|
||||
// Setup Program
|
||||
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());
|
||||
}
|
||||
});
|
||||
|
||||
app.Run();
|
||||
}
|
||||
catch (Exception exception)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
"applicationUrl": "http://0.0.0.0:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
|
||||
@@ -15,7 +15,7 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7278;http://localhost:5000",
|
||||
"applicationUrl": "https://0.0.0.0:7278;http://0.0.0.0:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DotNext" Version="5.19.1" />
|
||||
<PackageReference Include="DotNext.Threading" Version="5.19.1" />
|
||||
<PackageReference Include="ArpLookup" Version="2.0.3" />
|
||||
<PackageReference Include="DotNext" Version="5.23.0" />
|
||||
<PackageReference Include="DotNext.Threading" Version="5.23.0" />
|
||||
<PackageReference Include="Honoo.IO.Hashing.Crc" Version="1.3.3" />
|
||||
<PackageReference Include="linq2db.AspNet" Version="5.4.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="1.6.23" />
|
||||
@@ -25,6 +27,10 @@
|
||||
<PackageReference Include="NLog" Version="5.4.0" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" />
|
||||
<PackageReference Include="NSwag.AspNetCore" Version="14.3.0" />
|
||||
<PackageReference Include="NSwag.CodeGeneration.TypeScript" Version="14.4.0" />
|
||||
<PackageReference Include="OpenCvSharp4" 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="System.Data.SQLite.Core" Version="1.0.119" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
634
server/src/ArpClient.cs
Normal file
634
server/src/ArpClient.cs
Normal file
@@ -0,0 +1,634 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net.NetworkInformation;
|
||||
using ArpLookup;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/// <summary>
|
||||
/// ARP 记录管理静态类(跨平台支持)
|
||||
/// </summary>
|
||||
public static class ArpClient
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 读取所有 ARP 记录
|
||||
/// </summary>
|
||||
/// <returns>ARP 记录列表</returns>
|
||||
public static async Task<List<ArpEntry>> GetArpTableAsync()
|
||||
{
|
||||
var entries = new List<ArpEntry>();
|
||||
|
||||
try
|
||||
{
|
||||
string command = GetArpListCommand();
|
||||
var result = await ExecuteCommandAsync(command);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var lines = result.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var entry = ParseArpEntry(line);
|
||||
if (entry != null)
|
||||
{
|
||||
entries.Add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"读取 ARP 表失败: {ex.Message}");
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 动态更新指定 IP 的 ARP 记录
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">要更新的 IP 地址</param>
|
||||
/// <returns>是否成功发送 Ping</returns>
|
||||
public static async Task<bool> UpdateArpEntryAsync(string ipAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
|
||||
|
||||
try
|
||||
{
|
||||
var ret = await ArpClient.DeleteArpEntryAsync(ipAddress);
|
||||
if (!ret)
|
||||
{
|
||||
logger.Error($"删除 ARP 记录失败: {ipAddress}");
|
||||
}
|
||||
|
||||
PhysicalAddress? mac = await Arp.LookupAsync(IPAddress.Parse(ipAddress));
|
||||
if (mac == null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加 ARP 记录
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">IP 地址</param>
|
||||
/// <param name="macAddress">MAC 地址</param>
|
||||
/// <param name="interfaceName">网络接口名称(可选)</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static async Task<bool> AddArpEntryAsync(string ipAddress, string macAddress, string? interfaceName = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(macAddress))
|
||||
throw new ArgumentException("MAC 地址不能为空", nameof(macAddress));
|
||||
|
||||
try
|
||||
{
|
||||
// 格式化 MAC 地址以适配不同操作系统
|
||||
string formattedMac = FormatMacAddress(macAddress);
|
||||
string command = await GetArpAddCommandAsync(ipAddress, formattedMac, interfaceName);
|
||||
var result = await ExecuteCommandAsync(command);
|
||||
return result.IsSuccess;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"添加 ARP 记录失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除 ARP 记录
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">要删除的 IP 地址</param>
|
||||
/// <param name="interfaceName">网络接口名称(可选)</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static async Task<bool> DeleteArpEntryAsync(string ipAddress, string? interfaceName = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
|
||||
|
||||
try
|
||||
{
|
||||
string command = GetArpDeleteCommand(ipAddress, interfaceName);
|
||||
var result = await ExecuteCommandAsync(command);
|
||||
return result.IsSuccess;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"删除 ARP 记录失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有 ARP 记录
|
||||
/// </summary>
|
||||
/// <returns>是否成功</returns>
|
||||
public static async Task<bool> ClearArpTableAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
string command = GetArpClearCommand();
|
||||
var result = await ExecuteCommandAsync(command);
|
||||
return result.IsSuccess;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"清空 ARP 表失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询特定 IP 的 ARP 记录
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">IP 地址</param>
|
||||
/// <returns>ARP 记录,如果不存在则返回 null</returns>
|
||||
public static async Task<ArpEntry?> GetArpEntryAsync(string ipAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
|
||||
|
||||
try
|
||||
{
|
||||
string command = GetArpQueryCommand(ipAddress);
|
||||
var result = await ExecuteCommandAsync(command);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var lines = result.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var entry = ParseArpEntry(line);
|
||||
if (entry != null && entry.IpAddress == ipAddress)
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"查询 ARP 记录失败: {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 ARP 列表命令
|
||||
/// </summary>
|
||||
private static string GetArpListCommand()
|
||||
{
|
||||
return "arp -a";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 ARP 添加命令
|
||||
/// </summary>
|
||||
private static async Task<string> GetArpAddCommandAsync(string ipAddress, string macAddress, string? interfaceName)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(interfaceName))
|
||||
{
|
||||
// 通过 arp -a 获取接口索引
|
||||
var interfaceIdx = await GetWindowsInterfaceIndexAsync(interfaceName);
|
||||
if (interfaceIdx.HasValue)
|
||||
{
|
||||
return $"netsh -c i i add neighbors {interfaceIdx.Value} {ipAddress} {macAddress}";
|
||||
}
|
||||
}
|
||||
return $"arp -s {ipAddress} {macAddress}";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(interfaceName)
|
||||
? $"arp -s {ipAddress} {macAddress}"
|
||||
: $"arp -s {ipAddress} {macAddress} -i {interfaceName}";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(interfaceName)
|
||||
? $"arp -s {ipAddress} {macAddress}"
|
||||
: $"arp -s {ipAddress} {macAddress} ifscope {interfaceName}";
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException("不支持的操作系统平台");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Windows 接口索引
|
||||
/// </summary>
|
||||
/// <param name="interfaceIp">接口IP地址</param>
|
||||
/// <returns>接口索引(十进制),如果未找到则返回null</returns>
|
||||
private static async Task<int?> GetWindowsInterfaceIndexAsync(string interfaceIp)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await ExecuteCommandAsync("arp -a");
|
||||
if (!result.IsSuccess)
|
||||
return null;
|
||||
|
||||
var lines = result.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
// 匹配接口行格式: Interface: 172.6.1.5 --- 0xa
|
||||
var interfacePattern = @"Interface:\s+(\d+\.\d+\.\d+\.\d+)\s+---\s+(0x[a-fA-F0-9]+)";
|
||||
var match = Regex.Match(line, interfacePattern);
|
||||
|
||||
if (match.Success && match.Groups[1].Value == interfaceIp)
|
||||
{
|
||||
// 将十六进制索引转换为十进制
|
||||
var hexIndex = match.Groups[2].Value;
|
||||
// 去掉 "0x" 前缀
|
||||
var hexValue = hexIndex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? hexIndex.Substring(2)
|
||||
: hexIndex;
|
||||
|
||||
if (int.TryParse(hexValue, System.Globalization.NumberStyles.HexNumber, null, out int decimalIndex))
|
||||
{
|
||||
logger.Debug($"找到接口 {interfaceIp} 的索引: {hexIndex} -> {decimalIndex}");
|
||||
return decimalIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Warn($"未找到接口 {interfaceIp} 的索引");
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"获取接口 {interfaceIp} 索引失败");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 ARP 删除命令
|
||||
/// </summary>
|
||||
private static string GetArpDeleteCommand(string ipAddress, string? interfaceName)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return $"arp -d {ipAddress}";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(interfaceName)
|
||||
? $"arp -d {ipAddress}"
|
||||
: $"arp -d {ipAddress} -i {interfaceName}";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(interfaceName)
|
||||
? $"arp -d {ipAddress}"
|
||||
: $"arp -d {ipAddress} ifscope {interfaceName}";
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException("不支持的操作系统平台");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 ARP 清空命令
|
||||
/// </summary>
|
||||
private static string GetArpClearCommand()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return "arp -d *";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return "ip neigh flush all";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return "arp -d -a";
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException("不支持的操作系统平台");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 ARP 查询命令
|
||||
/// </summary>
|
||||
private static string GetArpQueryCommand(string ipAddress)
|
||||
{
|
||||
return $"arp -a {ipAddress}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行系统命令
|
||||
/// </summary>
|
||||
/// <param name="command">命令</param>
|
||||
/// <returns>命令执行结果</returns>
|
||||
private static async Task<CommandResult> ExecuteCommandAsync(string command)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessStartInfo processInfo;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
processInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/c {command}",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
processInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/bash",
|
||||
Arguments = $"-c \"{command}\"",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
}
|
||||
|
||||
logger.Debug($"Executing command: {processInfo.FileName} {processInfo.Arguments}");
|
||||
|
||||
using var process = new Process { StartInfo = processInfo };
|
||||
process.Start();
|
||||
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
var error = await process.StandardError.ReadToEndAsync();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
logger.Debug($"Command output: {output}");
|
||||
if (!string.IsNullOrWhiteSpace(error))
|
||||
logger.Debug($"Command error: {error}");
|
||||
logger.Debug($"Command exit code: {process.ExitCode}");
|
||||
|
||||
return new CommandResult
|
||||
{
|
||||
IsSuccess = process.ExitCode == 0,
|
||||
Output = output,
|
||||
Error = error,
|
||||
ExitCode = process.ExitCode
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"Command execution failed: {command}");
|
||||
return new CommandResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Error = ex.Message,
|
||||
ExitCode = -1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 ARP 记录行
|
||||
/// </summary>
|
||||
/// <param name="line">ARP 记录行</param>
|
||||
/// <returns>解析后的 ARP 记录</returns>
|
||||
private static ArpEntry? ParseArpEntry(string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
return null;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return ParseWindowsArpEntry(line);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ParseUnixArpEntry(line);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 Windows ARP 记录
|
||||
/// </summary>
|
||||
private static ArpEntry? ParseWindowsArpEntry(string line)
|
||||
{
|
||||
// 跳过空行和标题行
|
||||
if (string.IsNullOrWhiteSpace(line) ||
|
||||
line.Contains("Interface:") ||
|
||||
line.Contains("Internet Address") ||
|
||||
line.Contains("Physical Address") ||
|
||||
line.Contains("Type"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Windows arp -a 输出格式: IP地址 物理地址 类型
|
||||
// 示例: 172.6.0.1 e4-3a-6e-29-c3-5b dynamic
|
||||
var pattern = @"^\s*(\d+\.\d+\.\d+\.\d+)\s+([a-fA-F0-9-]{17})\s+(\w+)\s*$";
|
||||
var match = Regex.Match(line, pattern);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
return new ArpEntry
|
||||
{
|
||||
IpAddress = match.Groups[1].Value,
|
||||
MacAddress = FormatMacAddress(match.Groups[2].Value), // 格式化 MAC 地址
|
||||
Type = match.Groups[3].Value
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 Unix/Linux ARP 记录
|
||||
/// </summary>
|
||||
private static ArpEntry? ParseUnixArpEntry(string line)
|
||||
{
|
||||
// Unix/Linux arp -a 输出格式: hostname (ip) at mac [ether] PERM on interface
|
||||
var pattern = @"(\S+)\s+\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([a-fA-F0-9:]{17})\s+\[(\w+)\]\s+(\w+)\s+on\s+(\S+)";
|
||||
var match = Regex.Match(line, pattern);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
return new ArpEntry
|
||||
{
|
||||
Hostname = match.Groups[1].Value,
|
||||
IpAddress = match.Groups[2].Value,
|
||||
MacAddress = FormatMacAddress(match.Groups[3].Value), // 格式化 MAC 地址
|
||||
Type = match.Groups[5].Value,
|
||||
Interface = match.Groups[6].Value
|
||||
};
|
||||
}
|
||||
|
||||
// 匹配简单格式: ip mac interface
|
||||
var simplePattern = @"(\d+\.\d+\.\d+\.\d+)\s+([a-fA-F0-9:]{17})\s+(\S+)";
|
||||
var simpleMatch = Regex.Match(line, simplePattern);
|
||||
|
||||
if (simpleMatch.Success)
|
||||
{
|
||||
return new ArpEntry
|
||||
{
|
||||
IpAddress = simpleMatch.Groups[1].Value,
|
||||
MacAddress = FormatMacAddress(simpleMatch.Groups[2].Value), // 格式化 MAC 地址
|
||||
Interface = simpleMatch.Groups[3].Value
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断当前进程是否具有管理员(或 root)权限
|
||||
/// </summary>
|
||||
/// <returns>如果有管理员权限返回 true,否则返回 false</returns>
|
||||
public static bool IsAdministrator()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Windows: 检查当前用户是否为管理员
|
||||
using var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
|
||||
var principal = new System.Security.Principal.WindowsPrincipal(identity);
|
||||
return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unix/Linux/macOS: 检查是否为 root 用户
|
||||
return Environment.UserName == "root" || (Environment.GetEnvironmentVariable("USER") == "root");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定 IP 是否存在对应的 MAC,如果不存在则删除原有 ARP 记录并新增
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">IP 地址</param>
|
||||
/// <param name="macAddress">MAC 地址</param>
|
||||
/// <param name="interfaceName">网络接口名称(可选)</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static async Task<bool> CheckOrAddAsync(string ipAddress, string macAddress, string? interfaceName = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
|
||||
if (string.IsNullOrWhiteSpace(macAddress))
|
||||
throw new ArgumentException("MAC 地址不能为空", nameof(macAddress));
|
||||
|
||||
// 格式化 MAC 地址以适配不同操作系统
|
||||
string formattedMac = FormatMacAddress(macAddress);
|
||||
|
||||
var entry = await GetArpEntryAsync(ipAddress);
|
||||
if (entry != null && string.Equals(FormatMacAddress(entry.MacAddress), formattedMac, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 已存在且 MAC 匹配,无需操作
|
||||
return true;
|
||||
}
|
||||
|
||||
// 若存在但 MAC 不匹配,先删除
|
||||
if (entry != null)
|
||||
{
|
||||
await DeleteArpEntryAsync(ipAddress, interfaceName);
|
||||
}
|
||||
|
||||
// 新增 ARP 记录
|
||||
var ret = await AddArpEntryAsync(ipAddress, formattedMac, interfaceName);
|
||||
if (!ret) logger.Error($"添加 ARP 记录失败: {ipAddress} -> {formattedMac} on {interfaceName}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化 MAC 地址为指定平台格式
|
||||
/// </summary>
|
||||
/// <param name="macAddress">原始 MAC 地址</param>
|
||||
/// <returns>格式化后的 MAC 地址</returns>
|
||||
public static string FormatMacAddress(string macAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(macAddress))
|
||||
return string.Empty;
|
||||
|
||||
var cleaned = macAddress.Replace("-", "").Replace(":", "").ToLowerInvariant();
|
||||
if (cleaned.Length != 12)
|
||||
return macAddress;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Windows: XX-XX-XX-XX-XX-XX
|
||||
return string.Join("-", Enumerable.Range(0, 6).Select(i => cleaned.Substring(i * 2, 2)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unix/Linux/macOS: xx:xx:xx:xx:xx:xx
|
||||
return string.Join(":", Enumerable.Range(0, 6).Select(i => cleaned.Substring(i * 2, 2)));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// ARP 记录条目
|
||||
/// </summary>
|
||||
public class ArpEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public string Hostname { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public string MacAddress { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public string Interface { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{IpAddress} -> {MacAddress} ({Interface})";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 命令执行结果
|
||||
/// </summary>
|
||||
public class CommandResult
|
||||
{
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; set; }
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public string Output { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public string Error { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public int ExitCode { get; set; }
|
||||
}
|
||||
@@ -1,392 +0,0 @@
|
||||
using System.Collections;
|
||||
using DotNext;
|
||||
|
||||
namespace Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 数字处理工具
|
||||
/// </summary>
|
||||
public class Number
|
||||
{
|
||||
private static readonly byte[] BitReverseTable = new byte[] {
|
||||
0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
|
||||
0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
|
||||
0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
|
||||
0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
|
||||
0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
|
||||
0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
|
||||
0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
|
||||
0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
|
||||
0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
|
||||
0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
|
||||
0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
|
||||
0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
|
||||
0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
|
||||
0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
|
||||
0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
|
||||
0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
|
||||
0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
|
||||
0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
|
||||
0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
|
||||
0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
|
||||
0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
|
||||
0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
|
||||
0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
|
||||
0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
|
||||
0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
|
||||
0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
|
||||
0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
|
||||
0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
|
||||
0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
|
||||
0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
|
||||
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
|
||||
0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 整数转成二进制字节数组
|
||||
/// </summary>
|
||||
/// <param name="num">整数</param>
|
||||
/// <param name="length">整数长度</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>二进制字节数组</returns>
|
||||
public static Result<byte[]> NumberToBytes(ulong num, uint length, bool isLowNumHigh = false)
|
||||
{
|
||||
if (length > 8)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 8 bytes(64 bits).",
|
||||
nameof(length)
|
||||
));
|
||||
}
|
||||
|
||||
var arr = new byte[length];
|
||||
|
||||
if (isLowNumHigh)
|
||||
{
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
arr[length - 1 - i] = Convert.ToByte((num >> (i << 3)) & (0xFF));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
arr[i] = Convert.ToByte((num >> ((int)(length - 1 - i) << 3)) & (0xFF));
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 二进制字节数组转成64bits整数
|
||||
/// </summary>
|
||||
/// <param name="bytes">二进制字节数组</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>整数</returns>
|
||||
public static Result<UInt64> BytesToUInt64(byte[] bytes, bool isLowNumHigh = false)
|
||||
{
|
||||
if (bytes.Length > 8)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 8 bytes(64 bits).",
|
||||
nameof(bytes)
|
||||
));
|
||||
}
|
||||
|
||||
UInt64 num = 0;
|
||||
int len = bytes.Length;
|
||||
|
||||
try
|
||||
{
|
||||
if (isLowNumHigh)
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt64((UInt64)bytes[len - 1 - i] << (i << 3));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt64((UInt64)bytes[i] << ((int)(len - 1 - i) << 3));
|
||||
}
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 二进制字节数组转成32bits整数
|
||||
/// </summary>
|
||||
/// <param name="bytes">二进制字节数组</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>整数</returns>
|
||||
public static Result<UInt32> BytesToUInt32(byte[] bytes, bool isLowNumHigh = false)
|
||||
{
|
||||
if (bytes.Length > 4)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 4 bytes(32 bits).",
|
||||
nameof(bytes)
|
||||
));
|
||||
}
|
||||
|
||||
UInt32 num = 0;
|
||||
int len = bytes.Length;
|
||||
|
||||
try
|
||||
{
|
||||
if (isLowNumHigh)
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt32((UInt32)bytes[len - 1 - i] << (i << 3));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
num += Convert.ToUInt32((UInt32)bytes[i] << ((int)(len - 1 - i) << 3));
|
||||
}
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="uintArray">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public static Result<byte[]> UInt32ArrayToBytes(UInt32[] uintArray)
|
||||
{
|
||||
byte[] byteArray = new byte[uintArray.Length * 4];
|
||||
try
|
||||
{
|
||||
Buffer.BlockCopy(uintArray, 0, byteArray, 0, uintArray.Length * 4);
|
||||
return byteArray;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成二进制字节
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的二进制字节数组</returns>
|
||||
public static Result<byte[]> MultiBitsToBytes(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
|
||||
{
|
||||
return NumberToBytes(MultiBitsToNumber(bits1, bits1Len, bits2, bits2Len).Value,
|
||||
(bits1Len + bits2Len) % 8 != 0 ? (bits1Len + bits2Len) / 8 + 1 : (bits1Len + bits2Len) / 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成整型
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的整型值</returns>
|
||||
public static Result<ulong> MultiBitsToNumber(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
|
||||
{
|
||||
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
|
||||
|
||||
ulong num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
|
||||
return num;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成整型
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的整型值</returns>
|
||||
public static Result<uint> MultiBitsToNumber(uint bits1, uint bits1Len, uint bits2, uint bits2Len)
|
||||
{
|
||||
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
|
||||
|
||||
uint num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
|
||||
return num;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特位检查
|
||||
/// </summary>
|
||||
/// <param name="srcBits">源比特值</param>
|
||||
/// <param name="dstBits">目标比特值</param>
|
||||
/// <param name="mask">掩码(默认为全1)</param>
|
||||
/// <returns>检查结果(是否匹配)</returns>
|
||||
public static bool BitsCheck(ulong srcBits, ulong dstBits, ulong mask = 0xFFFF_FFFF_FFFF_FFFF)
|
||||
{
|
||||
return (srcBits & mask) == dstBits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特位检查
|
||||
/// </summary>
|
||||
/// <param name="srcBits">源比特值</param>
|
||||
/// <param name="dstBits">目标比特值</param>
|
||||
/// <param name="mask">掩码(默认为全1)</param>
|
||||
/// <returns>检查结果(是否匹配)</returns>
|
||||
public static bool BitsCheck(uint srcBits, uint dstBits, uint mask = 0xFFFF_FFFF)
|
||||
{
|
||||
return (srcBits & mask) == dstBits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取整型对应位置的比特
|
||||
/// </summary>
|
||||
/// <param name="srcBits">整型数字</param>
|
||||
/// <param name="location">位置</param>
|
||||
/// <returns>比特</returns>
|
||||
public static Result<bool> ToBit(UInt32 srcBits, int location)
|
||||
{
|
||||
if (location < 0)
|
||||
return new(new ArgumentException(
|
||||
"Location can't be negetive", nameof(location)));
|
||||
|
||||
return ((srcBits >> location) & ((UInt32)0b1)) == 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将BitArray转化为32bits无符号整型
|
||||
/// </summary>
|
||||
/// <param name="bits">BitArray比特数组</param>
|
||||
/// <returns>32bits无符号整型</returns>
|
||||
public static Result<UInt32> BitsToNumber(BitArray bits)
|
||||
{
|
||||
if (bits.Length > 32)
|
||||
throw new ArgumentException("Argument length shall be at most 32 bits.");
|
||||
|
||||
var array = new UInt32[1];
|
||||
bits.CopyTo(array, 0);
|
||||
return array[0];
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 字符串转二进制字节数组
|
||||
/// </summary>
|
||||
/// <param name="str">输入的字符串</param>
|
||||
/// <param name="numBase">进制(默认为16进制)</param>
|
||||
/// <returns>转换后的二进制字节数组</returns>
|
||||
public static byte[] StringToBytes(string str, int numBase = 16)
|
||||
{
|
||||
var len = str.Length;
|
||||
var bytesLen = len / 2;
|
||||
var bytes = new byte[bytesLen];
|
||||
|
||||
for (var i = 0; i < bytesLen; i++)
|
||||
{
|
||||
bytes[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节数组中的子数组
|
||||
/// </summary>
|
||||
/// <param name="srcBytes">源字节数组</param>
|
||||
/// <param name="distance">子数组的长度(反转的步长)</param>
|
||||
/// <returns>反转后的字节数组</returns>
|
||||
public static Result<byte[]> ReverseBytes(byte[] srcBytes, int distance)
|
||||
{
|
||||
if (distance <= 0)
|
||||
return new(new ArgumentException("Distance can't be negetive", nameof(distance)));
|
||||
|
||||
var srcBytesLen = srcBytes.Length;
|
||||
if (distance > srcBytesLen)
|
||||
return new(new ArgumentException(
|
||||
"Distance is larger than bytesArray", nameof(distance)));
|
||||
if (srcBytesLen % distance != 0)
|
||||
return new(new ArgumentException(
|
||||
"The length of bytes can't be divided by distance without reminder", nameof(distance)));
|
||||
|
||||
var dstBytes = new byte[srcBytesLen];
|
||||
var buffer = new byte[distance];
|
||||
|
||||
for (int i = 0; i < srcBytesLen; i += distance)
|
||||
{
|
||||
var end = i + distance;
|
||||
buffer = srcBytes[i..end];
|
||||
Array.Reverse(buffer);
|
||||
Array.Copy(buffer, 0, dstBytes, i, distance);
|
||||
}
|
||||
|
||||
return dstBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节内比特顺序(使用查找表的方法)
|
||||
/// </summary>
|
||||
/// <param name="srcByte">字节</param>
|
||||
/// <returns>反转后的字节</returns>
|
||||
public static byte ReverseBits(byte srcByte)
|
||||
{
|
||||
return BitReverseTable[srcByte];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节数组的字节内比特顺序(使用查找表的方法)
|
||||
/// </summary>
|
||||
/// <param name="srcBytes">字节数组</param>
|
||||
/// <returns>反转后的字节字节数组</returns>
|
||||
public static byte[] ReverseBits(byte[] srcBytes)
|
||||
{
|
||||
var bytesLen = srcBytes.Length;
|
||||
var dstBytes = new byte[bytesLen];
|
||||
for (int i = 0; i < bytesLen; i++)
|
||||
{
|
||||
dstBytes[i] = BitReverseTable[srcBytes[i]];
|
||||
}
|
||||
return dstBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字符串处理工具
|
||||
/// </summary>
|
||||
public class String
|
||||
{
|
||||
/// <summary>
|
||||
/// 反转字符串
|
||||
/// </summary>
|
||||
/// <param name="s">输入的字符串</param>
|
||||
/// <returns>反转后的字符串</returns>
|
||||
public static string Reverse(string s)
|
||||
{
|
||||
char[] charArray = s.ToCharArray();
|
||||
Array.Reverse(charArray);
|
||||
return new string(charArray);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
21
server/src/Common/Global.cs
Normal file
21
server/src/Common/Global.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
public static class Global
|
||||
{
|
||||
|
||||
public static readonly string localhost = "127.0.0.1";
|
||||
|
||||
public static string GetLocalIPAddress()
|
||||
{
|
||||
var host = Dns.GetHostEntry(Dns.GetHostName());
|
||||
foreach (var ip in host.AddressList)
|
||||
{
|
||||
if (ip.AddressFamily == AddressFamily.InterNetwork)
|
||||
{
|
||||
return ip.ToString();
|
||||
}
|
||||
}
|
||||
throw new Exception("No network adapters with an IPv4 address in the system!");
|
||||
}
|
||||
}
|
||||
358
server/src/Common/Image.cs
Normal file
358
server/src/Common/Image.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
using System.Text;
|
||||
using DotNext;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Common;
|
||||
|
||||
/// <summary>
|
||||
/// 图像处理工具
|
||||
/// </summary>
|
||||
public class Image
|
||||
{
|
||||
/// <summary>
|
||||
/// 将 RGB565 格式转换为 RGB24 格式
|
||||
/// RGB565: 5位红色 + 6位绿色 + 5位蓝色 = 16位 (2字节)
|
||||
/// RGB24: 8位红色 + 8位绿色 + 8位蓝色 = 24位 (3字节)
|
||||
/// </summary>
|
||||
/// <param name="rgb565Data">RGB565格式的原始数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="isLittleEndian">是否为小端序(默认为true)</param>
|
||||
/// <returns>RGB24格式的转换后数据</returns>
|
||||
public static Result<byte[]> ConvertRGB565ToRGB24(byte[] rgb565Data, int width, int height, bool isLittleEndian = true)
|
||||
{
|
||||
if (rgb565Data == null)
|
||||
return new(new ArgumentNullException(nameof(rgb565Data)));
|
||||
|
||||
if (width <= 0 || height <= 0)
|
||||
return new(new ArgumentException("Width and height must be positive"));
|
||||
|
||||
// 计算像素数量
|
||||
var expectedPixelCount = width * height;
|
||||
var actualPixelCount = rgb565Data.Length / 2;
|
||||
|
||||
if (actualPixelCount < expectedPixelCount)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
$"RGB565 data length insufficient. Expected: {expectedPixelCount * 2} bytes, Actual: {rgb565Data.Length} bytes"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var pixelCount = Math.Min(actualPixelCount, expectedPixelCount);
|
||||
var rgb24Data = new byte[pixelCount * 3];
|
||||
|
||||
for (int i = 0; i < pixelCount; i++)
|
||||
{
|
||||
// 读取 RGB565 数据
|
||||
var rgb565Index = i * 2;
|
||||
if (rgb565Index + 1 >= rgb565Data.Length) break;
|
||||
|
||||
// 组合成16位值
|
||||
UInt16 rgb565;
|
||||
if (isLittleEndian)
|
||||
{
|
||||
rgb565 = (UInt16)(rgb565Data[rgb565Index] | (rgb565Data[rgb565Index + 1] << 8));
|
||||
}
|
||||
else
|
||||
{
|
||||
rgb565 = (UInt16)((rgb565Data[rgb565Index] << 8) | rgb565Data[rgb565Index + 1]);
|
||||
}
|
||||
|
||||
// 提取各颜色分量
|
||||
var r5 = (rgb565 >> 11) & 0x1F; // 高5位为红色
|
||||
var g6 = (rgb565 >> 5) & 0x3F; // 中间6位为绿色
|
||||
var b5 = rgb565 & 0x1F; // 低5位为蓝色
|
||||
|
||||
// 转换为8位颜色值
|
||||
var r8 = (byte)((r5 * 255) / 31); // 5位扩展到8位
|
||||
var g8 = (byte)((g6 * 255) / 63); // 6位扩展到8位
|
||||
var b8 = (byte)((b5 * 255) / 31); // 5位扩展到8位
|
||||
|
||||
// 存储到 RGB24 数组
|
||||
var rgb24Index = (i%2 == 0)?((i+1) * 3):((i-1) * 3);
|
||||
rgb24Data[rgb24Index] = r8; // R
|
||||
rgb24Data[rgb24Index + 1] = g8; // G
|
||||
rgb24Data[rgb24Index + 2] = b8; // B
|
||||
}
|
||||
|
||||
return rgb24Data;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 RGB24 格式转换为 RGB565 格式
|
||||
/// RGB24: 8位红色 + 8位绿色 + 8位蓝色 = 24位 (3字节)
|
||||
/// RGB565: 5位红色 + 6位绿色 + 5位蓝色 = 16位 (2字节)
|
||||
/// </summary>
|
||||
/// <param name="rgb24Data">RGB24格式的原始数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="isLittleEndian">是否为小端序(默认为true)</param>
|
||||
/// <returns>RGB565格式的转换后数据</returns>
|
||||
public static Result<byte[]> ConvertRGB24ToRGB565(byte[] rgb24Data, int width, int height, bool isLittleEndian = true)
|
||||
{
|
||||
if (rgb24Data == null)
|
||||
return new(new ArgumentNullException(nameof(rgb24Data)));
|
||||
|
||||
if (width <= 0 || height <= 0)
|
||||
return new(new ArgumentException("Width and height must be positive"));
|
||||
|
||||
var expectedPixelCount = width * height;
|
||||
var actualPixelCount = rgb24Data.Length / 3;
|
||||
|
||||
if (actualPixelCount < expectedPixelCount)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
$"RGB24 data length insufficient. Expected: {expectedPixelCount * 3} bytes, Actual: {rgb24Data.Length} bytes"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var pixelCount = Math.Min(actualPixelCount, expectedPixelCount);
|
||||
var rgb565Data = new byte[pixelCount * 2];
|
||||
|
||||
for (int i = 0; i < pixelCount; i++)
|
||||
{
|
||||
var rgb24Index = i * 3;
|
||||
if (rgb24Index + 2 >= rgb24Data.Length) break;
|
||||
|
||||
// 读取 RGB24 数据
|
||||
var r8 = rgb24Data[rgb24Index];
|
||||
var g8 = rgb24Data[rgb24Index + 1];
|
||||
var b8 = rgb24Data[rgb24Index + 2];
|
||||
|
||||
// 转换为5位、6位、5位
|
||||
var r5 = (UInt16)((r8 * 31) / 255);
|
||||
var g6 = (UInt16)((g8 * 63) / 255);
|
||||
var b5 = (UInt16)((b8 * 31) / 255);
|
||||
|
||||
// 组合成16位值
|
||||
var rgb565 = (UInt16)((r5 << 11) | (g6 << 5) | b5);
|
||||
|
||||
// 存储到 RGB565 数组
|
||||
var rgb565Index = i * 2;
|
||||
if (isLittleEndian)
|
||||
{
|
||||
rgb565Data[rgb565Index] = (byte)(rgb565 & 0xFF);
|
||||
rgb565Data[rgb565Index + 1] = (byte)(rgb565 >> 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
rgb565Data[rgb565Index] = (byte)(rgb565 >> 8);
|
||||
rgb565Data[rgb565Index + 1] = (byte)(rgb565 & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
return rgb565Data;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 RGB24 数据转换为 JPEG 格式
|
||||
/// </summary>
|
||||
/// <param name="rgb24Data">RGB24格式的图像数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="quality">JPEG质量(1-100,默认80)</param>
|
||||
/// <returns>JPEG格式的字节数组</returns>
|
||||
public static Result<byte[]> ConvertRGB24ToJpeg(byte[] rgb24Data, int width, int height, int quality = 80)
|
||||
{
|
||||
if (rgb24Data == null)
|
||||
return new(new ArgumentNullException(nameof(rgb24Data)));
|
||||
|
||||
if (width <= 0 || height <= 0)
|
||||
return new(new ArgumentException("Width and height must be positive"));
|
||||
|
||||
if (quality < 1 || quality > 100)
|
||||
return new(new ArgumentException("Quality must be between 1 and 100"));
|
||||
|
||||
var expectedDataLength = width * height * 3;
|
||||
if (rgb24Data.Length < expectedDataLength)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
$"RGB24 data length insufficient. Expected: {expectedDataLength} bytes, Actual: {rgb24Data.Length} bytes"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var image = new SixLabors.ImageSharp.Image<Rgb24>(width, height);
|
||||
|
||||
// 将 RGB 数据复制到 ImageSharp 图像
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
int index = (y * width + x) * 3;
|
||||
if (index + 2 < rgb24Data.Length)
|
||||
{
|
||||
var pixel = new Rgb24(rgb24Data[index], rgb24Data[index + 1], rgb24Data[index + 2]);
|
||||
image[x, y] = pixel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
image.SaveAsJpeg(stream, new JpegEncoder { Quality = quality });
|
||||
return stream.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 RGB565 数据直接转换为 JPEG 格式
|
||||
/// </summary>
|
||||
/// <param name="rgb565Data">RGB565格式的图像数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="quality">JPEG质量(1-100,默认80)</param>
|
||||
/// <param name="isLittleEndian">是否为小端序(默认为true)</param>
|
||||
/// <returns>JPEG格式的字节数组</returns>
|
||||
public static Result<byte[]> ConvertRGB565ToJpeg(byte[] rgb565Data, int width, int height, int quality = 80, bool isLittleEndian = true)
|
||||
{
|
||||
// 先转换为RGB24
|
||||
var rgb24Result = ConvertRGB565ToRGB24(rgb565Data, width, height, isLittleEndian);
|
||||
if (!rgb24Result.IsSuccessful)
|
||||
{
|
||||
return new(rgb24Result.Error);
|
||||
}
|
||||
|
||||
// 再转换为JPEG
|
||||
return ConvertRGB24ToJpeg(rgb24Result.Value, width, height, quality);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 MJPEG 帧头部
|
||||
/// </summary>
|
||||
/// <param name="frameDataLength">帧数据长度</param>
|
||||
/// <param name="boundary">边界字符串(默认为"--boundary")</param>
|
||||
/// <returns>MJPEG帧头部字节数组</returns>
|
||||
public static byte[] CreateMjpegFrameHeader(int frameDataLength, string boundary = "--boundary")
|
||||
{
|
||||
var header = $"{boundary}\r\nContent-Type: image/jpeg\r\nContent-Length: {frameDataLength}\r\n\r\n";
|
||||
return Encoding.ASCII.GetBytes(header);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建 MJPEG 帧尾部
|
||||
/// </summary>
|
||||
/// <returns>MJPEG帧尾部字节数组</returns>
|
||||
public static byte[] CreateMjpegFrameFooter()
|
||||
{
|
||||
return Encoding.ASCII.GetBytes("\r\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建完整的 MJPEG 帧数据
|
||||
/// </summary>
|
||||
/// <param name="jpegData">JPEG数据</param>
|
||||
/// <param name="boundary">边界字符串(默认为"--boundary")</param>
|
||||
/// <returns>完整的MJPEG帧数据</returns>
|
||||
public static Result<byte[]> CreateMjpegFrame(byte[] jpegData, string boundary = "--boundary")
|
||||
{
|
||||
if (jpegData == null)
|
||||
return new(new ArgumentNullException(nameof(jpegData)));
|
||||
|
||||
try
|
||||
{
|
||||
var header = CreateMjpegFrameHeader(jpegData.Length, boundary);
|
||||
var footer = CreateMjpegFrameFooter();
|
||||
|
||||
var totalLength = header.Length + jpegData.Length + footer.Length;
|
||||
var frameData = new byte[totalLength];
|
||||
|
||||
var offset = 0;
|
||||
Array.Copy(header, 0, frameData, offset, header.Length);
|
||||
offset += header.Length;
|
||||
|
||||
Array.Copy(jpegData, 0, frameData, offset, jpegData.Length);
|
||||
offset += jpegData.Length;
|
||||
|
||||
Array.Copy(footer, 0, frameData, offset, footer.Length);
|
||||
|
||||
return frameData;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证图像数据长度是否正确
|
||||
/// </summary>
|
||||
/// <param name="data">图像数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <param name="bytesPerPixel">每像素字节数</param>
|
||||
/// <returns>验证结果</returns>
|
||||
public static bool ValidateImageDataLength(byte[] data, int width, int height, int bytesPerPixel)
|
||||
{
|
||||
if (data == null || width <= 0 || height <= 0 || bytesPerPixel <= 0)
|
||||
return false;
|
||||
|
||||
var expectedLength = width * height * bytesPerPixel;
|
||||
return data.Length >= expectedLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取图像格式信息
|
||||
/// </summary>
|
||||
/// <param name="format">图像格式枚举</param>
|
||||
/// <returns>格式信息</returns>
|
||||
public static ImageFormatInfo GetImageFormatInfo(ImageFormat format)
|
||||
{
|
||||
return format switch
|
||||
{
|
||||
ImageFormat.RGB565 => new ImageFormatInfo("RGB565", 2, "16-bit RGB format (5R+6G+5B)"),
|
||||
ImageFormat.RGB24 => new ImageFormatInfo("RGB24", 3, "24-bit RGB format (8R+8G+8B)"),
|
||||
ImageFormat.RGBA32 => new ImageFormatInfo("RGBA32", 4, "32-bit RGBA format (8R+8G+8B+8A)"),
|
||||
ImageFormat.Grayscale8 => new ImageFormatInfo("Grayscale8", 1, "8-bit grayscale format"),
|
||||
_ => new ImageFormatInfo("Unknown", 0, "Unknown image format")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图像格式枚举
|
||||
/// </summary>
|
||||
public enum ImageFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// RGB565
|
||||
/// </summary>
|
||||
RGB565,
|
||||
|
||||
/// <summary>
|
||||
/// RGB888 / RGB24
|
||||
/// </summary>
|
||||
RGB24,
|
||||
|
||||
/// <summary>
|
||||
/// RGBA8888 / RGBA32
|
||||
/// </summary>
|
||||
RGBA32,
|
||||
|
||||
/// <summary>
|
||||
/// 灰度图
|
||||
/// </summary>
|
||||
Grayscale8
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图像格式信息
|
||||
/// </summary>
|
||||
public record ImageFormatInfo(string Name, int BytesPerPixel, string Description);
|
||||
351
server/src/Common/Number.cs
Normal file
351
server/src/Common/Number.cs
Normal file
@@ -0,0 +1,351 @@
|
||||
using System.Collections;
|
||||
using DotNext;
|
||||
|
||||
namespace Common;
|
||||
/// <summary>
|
||||
/// 数字处理工具
|
||||
/// </summary>
|
||||
public class Number
|
||||
{
|
||||
private static readonly byte[] BitReverseTable = new byte[] {
|
||||
0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
|
||||
0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
|
||||
0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
|
||||
0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
|
||||
0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
|
||||
0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
|
||||
0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
|
||||
0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
|
||||
0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
|
||||
0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
|
||||
0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
|
||||
0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
|
||||
0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
|
||||
0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
|
||||
0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
|
||||
0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
|
||||
0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
|
||||
0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
|
||||
0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
|
||||
0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
|
||||
0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
|
||||
0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
|
||||
0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
|
||||
0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
|
||||
0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
|
||||
0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
|
||||
0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
|
||||
0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
|
||||
0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
|
||||
0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
|
||||
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
|
||||
0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 整数转成二进制字节数组
|
||||
/// </summary>
|
||||
/// <param name="num">整数</param>
|
||||
/// <param name="length">整数长度</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>二进制字节数组</returns>
|
||||
public static Result<byte[]> NumberToBytes(ulong num, uint length, bool isLowNumHigh = false)
|
||||
{
|
||||
if (length > 8)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 8 bytes(64 bits).",
|
||||
nameof(length)
|
||||
));
|
||||
}
|
||||
|
||||
var arr = new byte[length];
|
||||
|
||||
if (isLowNumHigh)
|
||||
{
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
arr[i] = Convert.ToByte((num >> (i << 3)) & (0xFF));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
arr[i] = Convert.ToByte((num >> ((int)(length - 1 - i) << 3)) & (0xFF));
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 二进制字节数组转成64bits整数
|
||||
/// </summary>
|
||||
/// <param name="bytes">二进制字节数组</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>整数</returns>
|
||||
public static Result<UInt64> BytesToUInt64(byte[] bytes, bool isLowNumHigh = false)
|
||||
{
|
||||
if (bytes.Length > 8)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 8 bytes(64 bits).",
|
||||
nameof(bytes)
|
||||
));
|
||||
}
|
||||
|
||||
UInt64 num = 0;
|
||||
int len = bytes.Length;
|
||||
|
||||
try
|
||||
{
|
||||
if (!isLowNumHigh)
|
||||
{
|
||||
Array.Reverse(bytes);
|
||||
}
|
||||
num = BitConverter.ToUInt64(bytes, 0);
|
||||
|
||||
return num;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 二进制字节数组转成32bits整数
|
||||
/// </summary>
|
||||
/// <param name="bytes">二进制字节数组</param>
|
||||
/// <param name="isLowNumHigh">是否高位在数组索引低</param>
|
||||
/// <returns>整数</returns>
|
||||
public static Result<UInt32> BytesToUInt32(byte[] bytes, bool isLowNumHigh = false)
|
||||
{
|
||||
if (bytes.Length > 4)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 4 bytes(32 bits).",
|
||||
nameof(bytes)
|
||||
));
|
||||
}
|
||||
|
||||
UInt32 num = 0;
|
||||
int len = bytes.Length;
|
||||
|
||||
try
|
||||
{
|
||||
if (!isLowNumHigh)
|
||||
{
|
||||
Array.Reverse(bytes);
|
||||
}
|
||||
num = BitConverter.ToUInt32(bytes, 0);
|
||||
|
||||
return num;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="uintArray">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public static Result<byte[]> UInt32ArrayToBytes(UInt32[] uintArray)
|
||||
{
|
||||
byte[] byteArray = new byte[uintArray.Length * 4];
|
||||
try
|
||||
{
|
||||
Buffer.BlockCopy(uintArray, 0, byteArray, 0, uintArray.Length * 4);
|
||||
return byteArray;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return new(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成二进制字节
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的二进制字节数组</returns>
|
||||
public static Result<byte[]> MultiBitsToBytes(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
|
||||
{
|
||||
return NumberToBytes(MultiBitsToNumber(bits1, bits1Len, bits2, bits2Len).Value,
|
||||
(bits1Len + bits2Len) % 8 != 0 ? (bits1Len + bits2Len) / 8 + 1 : (bits1Len + bits2Len) / 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成整型
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的整型值</returns>
|
||||
public static Result<ulong> MultiBitsToNumber(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
|
||||
{
|
||||
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
|
||||
|
||||
ulong num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
|
||||
return num;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特合并成整型
|
||||
/// </summary>
|
||||
/// <param name="bits1">第一个比特值</param>
|
||||
/// <param name="bits1Len">第一个比特值的长度(位数)</param>
|
||||
/// <param name="bits2">第二个比特值</param>
|
||||
/// <param name="bits2Len">第二个比特值的长度(位数)</param>
|
||||
/// <returns>合并后的整型值</returns>
|
||||
public static Result<uint> MultiBitsToNumber(uint bits1, uint bits1Len, uint bits2, uint bits2Len)
|
||||
{
|
||||
if (bits1Len + bits2Len > 64) return new(new ArgumentException("Two Bits is more than 64 bits"));
|
||||
|
||||
uint num = (bits1 << Convert.ToInt32(bits2Len)) | bits2;
|
||||
return num;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特位检查
|
||||
/// </summary>
|
||||
/// <param name="srcBits">源比特值</param>
|
||||
/// <param name="dstBits">目标比特值</param>
|
||||
/// <param name="mask">掩码(默认为全1)</param>
|
||||
/// <returns>检查结果(是否匹配)</returns>
|
||||
public static bool BitsCheck(ulong srcBits, ulong dstBits, ulong mask = 0xFFFF_FFFF_FFFF_FFFF)
|
||||
{
|
||||
return (srcBits & mask) == dstBits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 比特位检查
|
||||
/// </summary>
|
||||
/// <param name="srcBits">源比特值</param>
|
||||
/// <param name="dstBits">目标比特值</param>
|
||||
/// <param name="mask">掩码(默认为全1)</param>
|
||||
/// <returns>检查结果(是否匹配)</returns>
|
||||
public static bool BitsCheck(uint srcBits, uint dstBits, uint mask = 0xFFFF_FFFF)
|
||||
{
|
||||
return (srcBits & mask) == dstBits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取整型对应位置的比特
|
||||
/// </summary>
|
||||
/// <param name="srcBits">整型数字</param>
|
||||
/// <param name="location">位置</param>
|
||||
/// <returns>比特</returns>
|
||||
public static Result<bool> ToBit(UInt32 srcBits, int location)
|
||||
{
|
||||
if (location < 0)
|
||||
return new(new ArgumentException(
|
||||
"Location can't be negetive", nameof(location)));
|
||||
|
||||
return ((srcBits >> location) & ((UInt32)0b1)) == 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将BitArray转化为32bits无符号整型
|
||||
/// </summary>
|
||||
/// <param name="bits">BitArray比特数组</param>
|
||||
/// <returns>32bits无符号整型</returns>
|
||||
public static Result<UInt32> BitsToNumber(BitArray bits)
|
||||
{
|
||||
if (bits.Length > 32)
|
||||
throw new ArgumentException("Argument length shall be at most 32 bits.");
|
||||
|
||||
var array = new UInt32[1];
|
||||
bits.CopyTo(array, 0);
|
||||
return array[0];
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 字符串转二进制字节数组
|
||||
/// </summary>
|
||||
/// <param name="str">输入的字符串</param>
|
||||
/// <param name="numBase">进制(默认为16进制)</param>
|
||||
/// <returns>转换后的二进制字节数组</returns>
|
||||
public static byte[] StringToBytes(string str, int numBase = 16)
|
||||
{
|
||||
var len = str.Length;
|
||||
var bytesLen = len / 2;
|
||||
var bytes = new byte[bytesLen];
|
||||
|
||||
for (var i = 0; i < bytesLen; i++)
|
||||
{
|
||||
bytes[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节数组中的子数组
|
||||
/// </summary>
|
||||
/// <param name="srcBytes">源字节数组</param>
|
||||
/// <param name="distance">子数组的长度(反转的步长)</param>
|
||||
/// <returns>反转后的字节数组</returns>
|
||||
public static Result<byte[]> ReverseBytes(byte[] srcBytes, int distance)
|
||||
{
|
||||
if (distance <= 0)
|
||||
return new(new ArgumentException("Distance can't be negetive", nameof(distance)));
|
||||
|
||||
var srcBytesLen = srcBytes.Length;
|
||||
if (distance > srcBytesLen)
|
||||
return new(new ArgumentException(
|
||||
"Distance is larger than bytesArray", nameof(distance)));
|
||||
if (srcBytesLen % distance != 0)
|
||||
return new(new ArgumentException(
|
||||
"The length of bytes can't be divided by distance without reminder", nameof(distance)));
|
||||
|
||||
var dstBytes = new byte[srcBytesLen];
|
||||
var buffer = new byte[distance];
|
||||
|
||||
for (int i = 0; i < srcBytesLen; i += distance)
|
||||
{
|
||||
Buffer.BlockCopy(srcBytes, i, buffer, 0, distance);
|
||||
Array.Reverse(buffer);
|
||||
Buffer.BlockCopy(buffer, 0, dstBytes, i, distance);
|
||||
}
|
||||
|
||||
return dstBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节内比特顺序(使用查找表的方法)
|
||||
/// </summary>
|
||||
/// <param name="srcByte">字节</param>
|
||||
/// <returns>反转后的字节</returns>
|
||||
public static byte ReverseBits(byte srcByte)
|
||||
{
|
||||
return BitReverseTable[srcByte];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反转字节数组的字节内比特顺序(使用查找表的方法)
|
||||
/// </summary>
|
||||
/// <param name="srcBytes">字节数组</param>
|
||||
/// <returns>反转后的字节字节数组</returns>
|
||||
public static byte[] ReverseBits(byte[] srcBytes)
|
||||
{
|
||||
var bytesLen = srcBytes.Length;
|
||||
var dstBytes = new byte[bytesLen];
|
||||
for (int i = 0; i < bytesLen; i++)
|
||||
{
|
||||
dstBytes[i] = BitReverseTable[srcBytes[i]];
|
||||
}
|
||||
return dstBytes;
|
||||
}
|
||||
}
|
||||
116
server/src/Common/SemaphorePool.cs
Normal file
116
server/src/Common/SemaphorePool.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System.Collections.Concurrent;
|
||||
using DotNext;
|
||||
|
||||
namespace Common;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public class SemaphorePool
|
||||
{
|
||||
private SemaphoreSlim semaphore;
|
||||
private ConcurrentQueue<int> queue;
|
||||
private int beginNum;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public int RemainingCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public int MaxCount { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="initialCount">[TODO:parameter]</param>
|
||||
/// <param name="beginNum">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public SemaphorePool(int initialCount, int beginNum = 0)
|
||||
{
|
||||
semaphore = new SemaphoreSlim(initialCount);
|
||||
queue = new ConcurrentQueue<int>();
|
||||
this.beginNum = beginNum;
|
||||
this.RemainingCount = initialCount;
|
||||
this.MaxCount = initialCount;
|
||||
for (int i = 0; i < initialCount; i++)
|
||||
{
|
||||
queue.Enqueue(beginNum + i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="initialCount">[TODO:parameter]</param>
|
||||
/// <param name="maxCount">[TODO:parameter]</param>
|
||||
/// <param name="beginNum">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public SemaphorePool(int initialCount, int maxCount, int beginNum = 0)
|
||||
{
|
||||
semaphore = new SemaphoreSlim(initialCount, maxCount);
|
||||
queue = new ConcurrentQueue<int>();
|
||||
this.beginNum = beginNum;
|
||||
this.RemainingCount = initialCount;
|
||||
this.MaxCount = maxCount;
|
||||
for (int i = 0; i < initialCount; i++)
|
||||
{
|
||||
queue.Enqueue(beginNum + i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public Result<int> Wait()
|
||||
{
|
||||
semaphore.Wait();
|
||||
|
||||
int pop;
|
||||
if (queue.TryDequeue(out pop))
|
||||
{
|
||||
return pop;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(new Exception($"TODO"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public async ValueTask<Result<int>> WaitAsync()
|
||||
{
|
||||
await semaphore.WaitAsync();
|
||||
|
||||
int pop;
|
||||
if (queue.TryDequeue(out pop))
|
||||
{
|
||||
return pop;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(new Exception($"TODO"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public void Release()
|
||||
{
|
||||
semaphore.Release();
|
||||
queue.Clear();
|
||||
for (int i = 0; i < MaxCount; i++)
|
||||
{
|
||||
queue.Enqueue(beginNum + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
server/src/Common/String.cs
Normal file
20
server/src/Common/String.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Common;
|
||||
|
||||
/// <summary>
|
||||
/// 字符串处理工具
|
||||
/// </summary>
|
||||
public class String
|
||||
{
|
||||
/// <summary>
|
||||
/// 反转字符串
|
||||
/// </summary>
|
||||
/// <param name="s">输入的字符串</param>
|
||||
/// <returns>反转后的字符串</returns>
|
||||
public static string Reverse(string s)
|
||||
{
|
||||
char[] charArray = s.ToCharArray();
|
||||
Array.Reverse(charArray);
|
||||
return new string(charArray);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
@@ -11,59 +17,476 @@ namespace server.Controllers;
|
||||
public class DataController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
// 固定的实验板IP,端口,MAC地址
|
||||
private const string BOARD_IP = "169.254.109.0";
|
||||
|
||||
/// <summary>
|
||||
/// 创建数据库表
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>插入的记录数</returns>
|
||||
[EnableCors("Development")]
|
||||
[HttpPost("CreateTable")]
|
||||
public IResult CreateTables()
|
||||
public class UserInfo
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
db.CreateAllTables();
|
||||
return TypedResults.Ok();
|
||||
/// <summary>
|
||||
/// 用户的唯一标识符
|
||||
/// </summary>
|
||||
public Guid ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户的名称
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户的电子邮箱
|
||||
/// </summary>
|
||||
public required string EMail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户关联的板卡ID
|
||||
/// </summary>
|
||||
public Guid BoardID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户绑定板子的过期时间
|
||||
/// </summary>
|
||||
public DateTime? BoardExpireTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除数据库表
|
||||
/// 获取本机IP地址(优先选择与实验板同网段的IP)
|
||||
/// </summary>
|
||||
/// <returns>插入的记录数</returns>
|
||||
[EnableCors("Development")]
|
||||
[HttpDelete("DropTables")]
|
||||
public IResult DropTables()
|
||||
/// <returns>本机IP地址</returns>
|
||||
private IPAddress GetLocalIPAddress()
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
db.DropAllTables();
|
||||
return TypedResults.Ok();
|
||||
try
|
||||
{
|
||||
var boardIpSegments = BOARD_IP.Split('.').Take(3).ToArray();
|
||||
|
||||
// 优先选择与实验板IP前三段相同的IP
|
||||
var sameSegmentIP = System.Net.NetworkInformation.NetworkInterface
|
||||
.GetAllNetworkInterfaces()
|
||||
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
|
||||
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
|
||||
.SelectMany(nic => nic.GetIPProperties().UnicastAddresses)
|
||||
.Where(addr => addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||
.Select(addr => addr.Address)
|
||||
.FirstOrDefault(addr =>
|
||||
{
|
||||
var segments = addr.ToString().Split('.');
|
||||
return segments.Length == 4 &&
|
||||
segments[0] == boardIpSegments[0] &&
|
||||
segments[1] == boardIpSegments[1] &&
|
||||
segments[2] == boardIpSegments[2];
|
||||
});
|
||||
|
||||
if (sameSegmentIP != null)
|
||||
return sameSegmentIP;
|
||||
|
||||
// 如果没有找到同网段的IP,返回第一个可用的IP
|
||||
return System.Net.NetworkInformation.NetworkInterface
|
||||
.GetAllNetworkInterfaces()
|
||||
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
|
||||
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
|
||||
.SelectMany(nic => nic.GetIPProperties().UnicastAddresses)
|
||||
.Where(addr => addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||
.Select(addr => addr.Address)
|
||||
.FirstOrDefault() ?? IPAddress.Loopback;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取本机IP地址失败");
|
||||
return IPAddress.Loopback;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有用户
|
||||
/// 用户登录,获取 JWT 令牌
|
||||
/// </summary>
|
||||
/// <returns>用户列表</returns>
|
||||
[HttpGet("AllUsers")]
|
||||
public IResult AllUsers()
|
||||
/// <param name="name">用户名</param>
|
||||
/// <param name="password">用户密码</param>
|
||||
/// <returns>JWT 令牌字符串</returns>
|
||||
[HttpPost("Login")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult Login(string name, string password)
|
||||
{
|
||||
// 验证用户密码
|
||||
using var db = new Database.AppDataConnection();
|
||||
var ret = db.User.ToList();
|
||||
return TypedResults.Ok(ret);
|
||||
var ret = db.CheckUserPassword(name, password);
|
||||
if (!ret.IsSuccessful) return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败");
|
||||
if (!ret.Value.HasValue) return BadRequest("用户名或密码错误");
|
||||
var user = ret.Value.Value;
|
||||
|
||||
// 生成 JWT
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes("my secret key 1234567890my secret key 1234567890");
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(new Claim[]
|
||||
{
|
||||
new Claim(ClaimTypes.Name, user.Name),
|
||||
new Claim(ClaimTypes.Email, user.EMail),
|
||||
new Claim(ClaimTypes.Role, user.Permission.ToString())
|
||||
}),
|
||||
Expires = DateTime.UtcNow.AddHours(1),
|
||||
SigningCredentials = new SigningCredentials(
|
||||
new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
|
||||
Audience = "dlut.edu.cn",
|
||||
Issuer = "dlut.edu.cn",
|
||||
};
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
var jwt = tokenHandler.WriteToken(token);
|
||||
|
||||
return Ok(jwt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试用户认证,需携带有效 JWT
|
||||
/// </summary>
|
||||
/// <returns>认证成功信息</returns>
|
||||
[Authorize]
|
||||
[HttpGet("TestAuth")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public IActionResult TestAuth()
|
||||
{
|
||||
return Ok("认证成功!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试管理员用户认证,需携带有效 JWT
|
||||
/// </summary>
|
||||
/// <returns>认证成功信息</returns>
|
||||
[Authorize("Admin")]
|
||||
[HttpGet("TestAdminAuth")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public IActionResult TestAdminAuth()
|
||||
{
|
||||
return Ok("认证成功!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户信息
|
||||
/// </summary>
|
||||
/// <returns>用户信息,包括ID、用户名、邮箱和板卡ID</returns>
|
||||
[Authorize]
|
||||
[HttpGet("GetUserInfo")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(UserInfo), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public IActionResult GetUserInfo()
|
||||
{
|
||||
// Get User Name
|
||||
var userName = User.Identity?.Name;
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
return Unauthorized("未找到用户名信息");
|
||||
|
||||
// Get User Info
|
||||
using var db = new Database.AppDataConnection();
|
||||
var ret = db.GetUserByName(userName);
|
||||
if (!ret.IsSuccessful)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败");
|
||||
|
||||
if (!ret.Value.HasValue)
|
||||
return BadRequest("用户不存在");
|
||||
|
||||
var user = ret.Value.Value;
|
||||
return Ok(new UserInfo
|
||||
{
|
||||
ID = user.ID,
|
||||
Name = user.Name,
|
||||
EMail = user.EMail,
|
||||
BoardID = user.BoardID,
|
||||
BoardExpireTime = user.BoardExpireTime,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册新用户
|
||||
/// </summary>
|
||||
/// <param name="name">用户名</param>
|
||||
/// <returns>操作结果</returns>
|
||||
/// <param name="name">用户名(不超过255个字符)</param>
|
||||
/// <param name="email">邮箱地址</param>
|
||||
/// <param name="password">用户密码</param>
|
||||
/// <returns>操作结果,成功返回 true,失败返回错误信息</returns>
|
||||
[HttpPost("SignUpUser")]
|
||||
public IResult SignUpUser(string name)
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult SignUpUser(string name, string email, string password)
|
||||
{
|
||||
if (name.Length > 255)
|
||||
return TypedResults.BadRequest("Name Couln't over 255 characters");
|
||||
// 验证输入参数
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return BadRequest("用户名不能为空");
|
||||
|
||||
using var db = new Database.AppDataConnection();
|
||||
var ret = db.AddUser(name);
|
||||
return TypedResults.Ok(ret);
|
||||
if (name.Length > 255)
|
||||
return BadRequest("用户名不能超过255个字符");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
return BadRequest("邮箱不能为空");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
return BadRequest("密码不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var ret = db.AddUser(name, email, password);
|
||||
return Ok(ret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "注册用户时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "注册失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个空闲的实验板(普通用户权限)
|
||||
/// </summary>
|
||||
/// <param name="durationHours">绑定持续时间(小时),默认为1小时</param>
|
||||
[Authorize]
|
||||
[HttpGet("GetAvailableBoard")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(Database.Board), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IActionResult> GetAvailableBoard(int durationHours = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userName = User.Identity?.Name;
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
return Unauthorized("未找到用户名信息");
|
||||
|
||||
using var db = new Database.AppDataConnection();
|
||||
var userRet = db.GetUserByName(userName);
|
||||
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
|
||||
return BadRequest("用户不存在");
|
||||
|
||||
var user = userRet.Value.Value;
|
||||
var expireTime = DateTime.UtcNow.AddHours(durationHours);
|
||||
|
||||
var boardOpt = db.GetAvailableBoard(user.ID, expireTime);
|
||||
if (!boardOpt.HasValue)
|
||||
return NotFound("没有可用的实验板");
|
||||
|
||||
var boardInfo = boardOpt.Value;
|
||||
if (!(await ArpClient.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString())))
|
||||
{
|
||||
logger.Error($"无法配置ARP,实验板可能会无法连接");
|
||||
}
|
||||
|
||||
return Ok(boardInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取空闲实验板时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解除当前用户绑定的实验板(普通用户权限)
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[HttpPost("UnbindBoard")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult UnbindBoard()
|
||||
{
|
||||
try
|
||||
{
|
||||
var userName = User.Identity?.Name;
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
return Unauthorized("未找到用户名信息");
|
||||
|
||||
using var db = new Database.AppDataConnection();
|
||||
var userRet = db.GetUserByName(userName);
|
||||
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
|
||||
return BadRequest("用户不存在");
|
||||
|
||||
var user = userRet.Value.Value;
|
||||
var result = db.UnbindUserFromBoard(user.ID);
|
||||
return Ok(result > 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "解除实验板绑定时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "解除失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户根据实验板ID获取实验板信息(普通用户权限)
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[HttpGet("GetBoardByID")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(Database.Board), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> GetBoardByID(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var ret = db.GetBoardByID(id);
|
||||
if (!ret.IsSuccessful)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "数据库操作失败");
|
||||
if (!ret.Value.HasValue)
|
||||
return NotFound("未找到对应的实验板");
|
||||
|
||||
var boardInfo = ret.Value.Value;
|
||||
if (!(await ArpClient.CheckOrAddAsync(boardInfo.IpAddr, boardInfo.MacAddr, GetLocalIPAddress().ToString())))
|
||||
{
|
||||
logger.Error($"无法配置ARP,实验板可能会无法连接");
|
||||
}
|
||||
|
||||
return Ok(boardInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取实验板信息时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增板子(管理员权限)
|
||||
/// </summary>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("AddBoard")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(Guid), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult AddBoard(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return BadRequest("板子名称不能为空");
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var ret = db.AddBoard(name);
|
||||
return Ok(ret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "新增板子时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "新增失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除板子(管理员权限)
|
||||
/// </summary>
|
||||
[Authorize("Admin")]
|
||||
[HttpDelete("DeleteBoard")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult DeleteBoard(Guid id)
|
||||
{
|
||||
if (id == Guid.Empty)
|
||||
return BadRequest("板子Guid不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var ret = db.DeleteBoardByID(id);
|
||||
return Ok(ret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "删除板子时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "删除失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取全部板子(管理员权限)
|
||||
/// </summary>
|
||||
[Authorize("Admin")]
|
||||
[HttpGet("GetAllBoards")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(Database.Board[]), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult GetAllBoards()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var boards = db.GetAllBoard();
|
||||
return Ok(boards);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取全部板子时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新板卡名称(管理员权限)
|
||||
/// </summary>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("UpdateBoardName")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult UpdateBoardName(Guid boardId, string newName)
|
||||
{
|
||||
if (boardId == Guid.Empty)
|
||||
return BadRequest("板子Guid不能为空");
|
||||
if (string.IsNullOrWhiteSpace(newName))
|
||||
return BadRequest("新名称不能为空");
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var result = db.UpdateBoardName(boardId, newName);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "更新板卡名称时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "更新失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新板卡状态(管理员权限)
|
||||
/// </summary>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("UpdateBoardStatus")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult UpdateBoardStatus(Guid boardId, Database.Board.BoardStatus newStatus)
|
||||
{
|
||||
if (boardId == Guid.Empty)
|
||||
return BadRequest("板子Guid不能为空");
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var result = db.UpdateBoardStatus(boardId, newStatus);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "更新板卡状态时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "更新失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
467
server/src/Controllers/DebuggerController.cs
Normal file
467
server/src/Controllers/DebuggerController.cs
Normal 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, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
454
server/src/Controllers/ExamController.cs
Normal file
454
server/src/Controllers/ExamController.cs
Normal file
@@ -0,0 +1,454 @@
|
||||
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 ResourceInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 资源ID
|
||||
/// </summary>
|
||||
public int ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 资源名称
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
}
|
||||
|
||||
/// <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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加实验资源
|
||||
/// </summary>
|
||||
/// <param name="examId">实验ID</param>
|
||||
/// <param name="resourceType">资源类型</param>
|
||||
/// <param name="file">资源文件</param>
|
||||
/// <returns>添加结果</returns>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("{examId}/resources/{resourceType}")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> AddExamResource(string examId, string resourceType, IFormFile file)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType) || file == null)
|
||||
return BadRequest("实验ID、资源类型和文件不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
|
||||
// 读取文件数据
|
||||
using var memoryStream = new MemoryStream();
|
||||
await file.CopyToAsync(memoryStream);
|
||||
var fileData = memoryStream.ToArray();
|
||||
|
||||
var result = db.AddExamResource(examId, resourceType, file.FileName, fileData);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
if (result.Error.Message.Contains("不存在"))
|
||||
return NotFound(result.Error.Message);
|
||||
if (result.Error.Message.Contains("已存在"))
|
||||
return Conflict(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
|
||||
};
|
||||
|
||||
logger.Info($"成功添加实验资源: {examId}/{resourceType}/{file.FileName}");
|
||||
return CreatedAtAction(nameof(GetExamResourceById), new { resourceId = resource.ID }, resourceInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"添加实验资源 {examId}/{resourceType}/{file.FileName} 时出错: {ex.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"添加实验资源失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定实验ID的指定资源类型的所有资源的ID和名称
|
||||
/// </summary>
|
||||
/// <param name="examId">实验ID</param>
|
||||
/// <param name="resourceType">资源类型</param>
|
||||
/// <returns>资源列表</returns>
|
||||
[Authorize]
|
||||
[HttpGet("{examId}/resources/{resourceType}")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult GetExamResourceList(string examId, string resourceType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType))
|
||||
return BadRequest("实验ID和资源类型不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var result = db.GetExamResourceList(examId, resourceType);
|
||||
|
||||
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.Name
|
||||
}).ToArray();
|
||||
|
||||
logger.Info($"成功获取实验资源列表: {examId}/{resourceType},共 {resources.Length} 个资源");
|
||||
return Ok(resources);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"获取实验资源列表 {examId}/{resourceType} 时出错: {ex.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验资源列表失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据资源ID下载资源
|
||||
/// </summary>
|
||||
/// <param name="resourceId">资源ID</param>
|
||||
/// <returns>资源文件</returns>
|
||||
[HttpGet("resources/{resourceId}")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult GetExamResourceById(int resourceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var result = db.GetExamResourceById(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, resource.ResourceName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Jtag API
|
||||
/// JTAG 控制器 - 提供 JTAG 相关的 API 操作
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize] // 添加用户认证要求
|
||||
public class JtagController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
@@ -15,134 +17,193 @@ public class JtagController : ControllerBase
|
||||
private const string BITSTREAM_PATH = "bitstream/Jtag";
|
||||
|
||||
/// <summary>
|
||||
/// 页面
|
||||
/// 控制器首页信息
|
||||
/// </summary>
|
||||
/// <returns>控制器描述信息</returns>
|
||||
[HttpGet]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
public string Index()
|
||||
{
|
||||
logger.Info($"User {User.Identity?.Name} accessed Jtag controller index");
|
||||
return "This is Jtag Controller";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取Jtag ID Code
|
||||
/// 获取 JTAG 设备的 ID Code
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
/// <param name="address">JTAG 设备地址</param>
|
||||
/// <param name="port">JTAG 设备端口</param>
|
||||
/// <returns>设备的 ID Code</returns>
|
||||
[HttpGet("GetDeviceIDCode")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(uint), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async ValueTask<IResult> GetDeviceIDCode(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadIDCode();
|
||||
logger.Info($"User {User.Identity?.Name} requesting device ID code from {address}:{port}");
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
try
|
||||
{
|
||||
logger.Info($"Get device {address} ID code: 0x{ret.Value:X4}");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadIDCode();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"User {User.Identity?.Name} successfully got device {address} ID code: 0x{ret.Value:X8}");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"User {User.Identity?.Name} failed to get device {address} ID code: {ret.Error}");
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while getting device {address} ID code");
|
||||
return TypedResults.InternalServerError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取状态寄存器
|
||||
/// 读取 JTAG 设备的状态寄存器
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
/// <param name="address">JTAG 设备地址</param>
|
||||
/// <param name="port">JTAG 设备端口</param>
|
||||
/// <returns>状态寄存器的原始值、二进制表示和解码值</returns>
|
||||
[HttpGet("ReadStatusReg")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async ValueTask<IResult> ReadStatusReg(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadStatusReg();
|
||||
logger.Info($"User {User.Identity?.Name} requesting status register from {address}:{port}");
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
try
|
||||
{
|
||||
var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
|
||||
var decodeValue = new JtagClient.JtagStatusReg(ret.Value);
|
||||
logger.Info($"Read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
|
||||
return TypedResults.Ok(new
|
||||
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadStatusReg();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
original = ret.Value,
|
||||
binaryValue,
|
||||
decodeValue,
|
||||
});
|
||||
var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
|
||||
var decodeValue = new Peripherals.JtagClient.JtagStatusReg(ret.Value);
|
||||
logger.Info($"User {User.Identity?.Name} successfully read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
original = ret.Value,
|
||||
binaryValue,
|
||||
decodeValue,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"User {User.Identity?.Name} failed to read device {address} status register: {ret.Error}");
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while reading device {address} status register");
|
||||
return TypedResults.InternalServerError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传比特流文件
|
||||
/// 上传比特流文件到服务器
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <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("未选择文件");
|
||||
|
||||
// 生成安全的文件名(避免路径遍历攻击)
|
||||
var fileName = Path.GetRandomFileName();
|
||||
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
|
||||
|
||||
// 如果存在文件,则删除原文件再上传
|
||||
if (Directory.Exists(uploadsFolder))
|
||||
{
|
||||
Directory.Delete(uploadsFolder, true);
|
||||
}
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
|
||||
var filePath = Path.Combine(uploadsFolder, fileName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
logger.Info($"Device {address} Upload Bitstream Successfully");
|
||||
return TypedResults.Ok(true);
|
||||
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>
|
||||
/// 通过Jtag下载比特流文件
|
||||
/// 通过 JTAG 下载比特流文件到 FPGA 设备
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
/// <param name="address">JTAG 设备地址</param>
|
||||
/// <param name="port">JTAG 设备端口</param>
|
||||
/// <returns>下载结果</returns>
|
||||
[HttpPost("DownloadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async ValueTask<IResult> DownloadBitstream(string address, int port)
|
||||
{
|
||||
logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port}");
|
||||
|
||||
// 检查文件
|
||||
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
|
||||
{
|
||||
// 读取文件
|
||||
var filePath = Directory.GetFiles(fileDir)[0];
|
||||
logger.Info($"User {User.Identity?.Name} reading bitstream file: {filePath}");
|
||||
|
||||
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
|
||||
{
|
||||
if (fileStream is null || fileStream.Length <= 0)
|
||||
{
|
||||
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");
|
||||
|
||||
// 定义缓冲区大小: 32KB
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
@@ -158,7 +219,10 @@ public class JtagController : ControllerBase
|
||||
// 反转 32bits
|
||||
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
|
||||
if (!retBuffer.IsSuccessful)
|
||||
{
|
||||
logger.Error($"User {User.Identity?.Name} failed to reverse bytes: {retBuffer.Error}");
|
||||
return TypedResults.InternalServerError(retBuffer.Error);
|
||||
}
|
||||
revBuffer = retBuffer.Value;
|
||||
|
||||
for (int i = 0; i < revBuffer.Length; i++)
|
||||
@@ -172,107 +236,148 @@ public class JtagController : ControllerBase
|
||||
|
||||
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
|
||||
var fileBytes = memoryStream.ToArray();
|
||||
logger.Info($"User {User.Identity?.Name} processed {totalBytesRead} bytes for device {address}");
|
||||
|
||||
// 下载比特流
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.DownloadBitstream(fileBytes);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} dowload bitstream successfully");
|
||||
logger.Info($"User {User.Identity?.Name} successfully downloaded bitstream to device {address}");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
logger.Error($"User {User.Identity?.Name} failed to download bitstream to device {address}: {ret.Error}");
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception error)
|
||||
catch (Exception ex)
|
||||
{
|
||||
return TypedResults.InternalServerError(error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while downloading bitstream to device {address}");
|
||||
return TypedResults.InternalServerError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// 执行边界扫描,获取所有端口状态
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
/// <param name="address">JTAG 设备地址</param>
|
||||
/// <param name="port">JTAG 设备端口</param>
|
||||
/// <returns>边界扫描结果</returns>
|
||||
[HttpPost("BoundaryScanAllPorts")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async ValueTask<IResult> BoundaryScanAllPorts(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.BoundaryScan();
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
logger.Info($"User {User.Identity?.Name} initiating boundary scan for all ports on device {address}:{port}");
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
try
|
||||
{
|
||||
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.BoundaryScan();
|
||||
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"User {User.Identity?.Name} boundary scan failed for device {address}: {ret.Error}");
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
logger.Info($"User {User.Identity?.Name} successfully completed boundary scan for device {address}");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"User {User.Identity?.Name} encountered exception during boundary scan for device {address}");
|
||||
return TypedResults.InternalServerError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// 执行逻辑端口边界扫描
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
/// <param name="address">JTAG 设备地址</param>
|
||||
/// <param name="port">JTAG 设备端口</param>
|
||||
/// <returns>逻辑端口状态字典</returns>
|
||||
[HttpPost("BoundaryScanLogicalPorts")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(Dictionary<string, bool>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async ValueTask<IResult> BoundaryScanLogicalPorts(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.BoundaryScanLogicalPorts();
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
logger.Info($"User {User.Identity?.Name} initiating logical ports boundary scan on device {address}:{port}");
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
try
|
||||
{
|
||||
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.BoundaryScanLogicalPorts();
|
||||
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"User {User.Identity?.Name} logical ports boundary scan failed for device {address}: {ret.Error}");
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
logger.Info($"User {User.Identity?.Name} successfully completed logical ports boundary scan for device {address}, found {ret.Value?.Count} ports");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"User {User.Identity?.Name} encountered exception during logical ports boundary scan for device {address}");
|
||||
return TypedResults.InternalServerError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// 设置 JTAG 时钟速度
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="speed">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
/// <param name="address">JTAG 设备地址</param>
|
||||
/// <param name="port">JTAG 设备端口</param>
|
||||
/// <param name="speed">时钟速度 (Hz)</param>
|
||||
/// <returns>设置结果</returns>
|
||||
[HttpPost("SetSpeed")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async ValueTask<IResult> SetSpeed(string address, int port, UInt32 speed)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.SetSpeed(speed);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
logger.Info($"User {User.Identity?.Name} setting JTAG speed to {speed} Hz for device {address}:{port}");
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
try
|
||||
{
|
||||
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.SetSpeed(speed);
|
||||
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"User {User.Identity?.Name} failed to set speed for device {address}: {ret.Error}");
|
||||
if (ret.Error is ArgumentException)
|
||||
return TypedResults.BadRequest(ret.Error);
|
||||
else
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
logger.Info($"User {User.Identity?.Name} successfully set JTAG speed to {speed} Hz for device {address}");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while setting speed for device {address}");
|
||||
return TypedResults.InternalServerError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
419
server/src/Controllers/LogicAnalyzerController.cs
Normal file
419
server/src/Controllers/LogicAnalyzerController.cs
Normal file
@@ -0,0 +1,419 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Peripherals.LogicAnalyzerClient;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 逻辑分析仪控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class LogicAnalyzerController : ControllerBase
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 信号触发配置
|
||||
/// </summary>
|
||||
public class SignalTriggerConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 信号索引 (0-7)
|
||||
/// </summary>
|
||||
public int SignalIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作符
|
||||
/// </summary>
|
||||
public SignalOperator Operator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 信号值
|
||||
/// </summary>
|
||||
public SignalValue Value { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 捕获配置
|
||||
/// </summary>
|
||||
public class CaptureConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局触发模式
|
||||
/// </summary>
|
||||
public GlobalCaptureMode GlobalMode { get; set; }
|
||||
/// <summary>
|
||||
/// 捕获深度
|
||||
/// </summary>
|
||||
public int CaptureLength { get; set; } = 2048 * 32;
|
||||
/// <summary>
|
||||
/// 预采样深度
|
||||
/// </summary>
|
||||
public int PreCaptureLength { get; set; } = 2048;
|
||||
/// <summary>
|
||||
/// 有效通道
|
||||
/// </summary>
|
||||
public AnalyzerChannelDiv ChannelDiv { get; set; } = AnalyzerChannelDiv.EIGHT;
|
||||
/// <summary>
|
||||
/// 信号触发配置列表
|
||||
/// </summary>
|
||||
public SignalTriggerConfig[] SignalConfigs { get; set; } = Array.Empty<SignalTriggerConfig>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取逻辑分析仪实例
|
||||
/// </summary>
|
||||
private Analyzer? GetAnalyzer()
|
||||
{
|
||||
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 Analyzer(board.IpAddr, board.Port, 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取逻辑分析仪实例时发生异常");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置捕获模式
|
||||
/// </summary>
|
||||
/// <param name="captureOn">是否开始捕获</param>
|
||||
/// <param name="force">是否强制捕获</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetCaptureMode")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> SetCaptureMode(bool captureOn, bool force = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.SetCaptureMode(captureOn, force);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置捕获模式失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置捕获模式失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置捕获模式时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取捕获状态
|
||||
/// </summary>
|
||||
/// <returns>捕获状态</returns>
|
||||
[HttpGet("GetCaptureStatus")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(CaptureStatus), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> GetCaptureStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.ReadCaptureStatus();
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"读取捕获状态失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "读取捕获状态失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "读取捕获状态时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置全局触发模式
|
||||
/// </summary>
|
||||
/// <param name="mode">全局触发模式</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetGlobalTrigMode")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> SetGlobalTrigMode(GlobalCaptureMode mode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.SetGlobalTrigMode(mode);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置全局触发模式失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置全局触发模式失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置全局触发模式时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置信号触发模式
|
||||
/// </summary>
|
||||
/// <param name="signalIndex">信号索引 (0-7)</param>
|
||||
/// <param name="op">操作符</param>
|
||||
/// <param name="val">信号值</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetSignalTrigMode")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> SetSignalTrigMode(int signalIndex, SignalOperator op, SignalValue val)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (signalIndex < 0 || signalIndex > 31)
|
||||
return BadRequest("信号索引必须在0-31之间");
|
||||
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.SetSignalTrigMode(signalIndex, op, val);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置信号触发模式失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置信号触发模式失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置信号触发模式时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置深度、预采样深度、有效通道
|
||||
/// </summary>
|
||||
/// <param name="capture_length">深度</param>
|
||||
/// <param name="pre_capture_length">预采样深度</param>
|
||||
/// <param name="channel_div">有效通道(0-[1],1-[2],2-[4],3-[8],4-[16],5-[32])</param>
|
||||
/// <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>
|
||||
/// <param name="config">捕获配置</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("ConfigureCapture")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> ConfigureCapture([FromBody] CaptureConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (config == null)
|
||||
return BadRequest("配置参数不能为空");
|
||||
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
// 设置全局触发模式
|
||||
var globalResult = await analyzer.SetGlobalTrigMode(config.GlobalMode);
|
||||
if (!globalResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置全局触发模式失败: {globalResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置全局触发模式失败");
|
||||
}
|
||||
|
||||
// 设置信号触发模式
|
||||
foreach (var signalConfig in config.SignalConfigs)
|
||||
{
|
||||
if (signalConfig.SignalIndex < 0 || signalConfig.SignalIndex > 31)
|
||||
return BadRequest($"信号索引{signalConfig.SignalIndex}超出范围0-31");
|
||||
|
||||
var signalResult = await analyzer.SetSignalTrigMode(
|
||||
signalConfig.SignalIndex, signalConfig.Operator, signalConfig.Value);
|
||||
if (!signalResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置信号{signalConfig.SignalIndex}触发模式失败: {signalResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
$"设置信号{signalConfig.SignalIndex}触发模式失败");
|
||||
}
|
||||
}
|
||||
// 设置深度、预采样深度、有效通道
|
||||
var paramsResult = await analyzer.SetCaptureParams(
|
||||
config.CaptureLength, config.PreCaptureLength, config.ChannelDiv);
|
||||
if (!paramsResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置深度、预采样深度、有效通道失败: {paramsResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置深度、预采样深度、有效通道失败");
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "配置捕获参数时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制捕获
|
||||
/// </summary>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("ForceCapture")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> ForceCapture()
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.SetCaptureMode(true, true);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"强制捕获失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "强制捕获失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "强制捕获时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取捕获数据
|
||||
/// </summary>
|
||||
/// <returns>捕获的波形数据(Base64编码)</returns>
|
||||
[HttpGet("GetCaptureData")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> GetCaptureData(int capture_length = 2048 * 32)
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.ReadCaptureData(capture_length);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"读取捕获数据失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "读取捕获数据失败");
|
||||
}
|
||||
|
||||
// 将二进制数据编码为Base64字符串返回
|
||||
var base64Data = Convert.ToBase64String(result.Value);
|
||||
return Ok(base64Data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "读取捕获数据时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
776
server/src/Controllers/NetConfigController.cs
Normal file
776
server/src/Controllers/NetConfigController.cs
Normal file
@@ -0,0 +1,776 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Peripherals.NetConfigClient;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 网络配置控制器(仅管理员权限)
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize("Admin")]
|
||||
public class NetConfigController : ControllerBase
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
// 固定的实验板IP,端口,MAC地址
|
||||
private const string BOARD_IP = "169.254.109.0";
|
||||
private const int BOARD_PORT = 1234;
|
||||
|
||||
// 本机网络信息
|
||||
private readonly IPAddress _localIP;
|
||||
private readonly byte[] _localMAC;
|
||||
private readonly string _localIPString;
|
||||
private readonly string _localMACString;
|
||||
private readonly string _localInterface;
|
||||
|
||||
public NetConfigController()
|
||||
{
|
||||
// 初始化本机IP地址
|
||||
_localIP = GetLocalIPAddress();
|
||||
_localIPString = _localIP?.ToString() ?? "未知";
|
||||
|
||||
// 初始化本机MAC地址
|
||||
_localMAC = GetLocalMACAddress();
|
||||
_localMACString = _localMAC != null ? BitConverter.ToString(_localMAC).Replace("-", ":") : "未知";
|
||||
|
||||
// 获取本机网络接口名称
|
||||
_localInterface = GetLocalNetworkInterface();
|
||||
|
||||
logger.Info($"NetConfigController 初始化完成 - 本机IP: {_localIPString}, 本机MAC: {_localMACString}, 接口: {_localInterface}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本机IP地址(优先选择与实验板同网段的IP)
|
||||
/// </summary>
|
||||
/// <returns>本机IP地址</returns>
|
||||
private IPAddress GetLocalIPAddress()
|
||||
{
|
||||
try
|
||||
{
|
||||
var boardIpSegments = BOARD_IP.Split('.').Take(3).ToArray();
|
||||
|
||||
// 优先选择与实验板IP前三段相同的IP
|
||||
var sameSegmentIP = System.Net.NetworkInformation.NetworkInterface
|
||||
.GetAllNetworkInterfaces()
|
||||
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
|
||||
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
|
||||
.SelectMany(nic => nic.GetIPProperties().UnicastAddresses)
|
||||
.Where(addr => addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||
.Select(addr => addr.Address)
|
||||
.FirstOrDefault(addr =>
|
||||
{
|
||||
var segments = addr.ToString().Split('.');
|
||||
return segments.Length == 4 &&
|
||||
segments[0] == boardIpSegments[0] &&
|
||||
segments[1] == boardIpSegments[1] &&
|
||||
segments[2] == boardIpSegments[2];
|
||||
});
|
||||
|
||||
if (sameSegmentIP != null)
|
||||
return sameSegmentIP;
|
||||
|
||||
// 如果没有找到同网段的IP,返回第一个可用的IP
|
||||
return System.Net.NetworkInformation.NetworkInterface
|
||||
.GetAllNetworkInterfaces()
|
||||
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
|
||||
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
|
||||
.SelectMany(nic => nic.GetIPProperties().UnicastAddresses)
|
||||
.Where(addr => addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||
.Select(addr => addr.Address)
|
||||
.FirstOrDefault() ?? IPAddress.Loopback;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取本机IP地址失败");
|
||||
return IPAddress.Loopback;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本机MAC地址
|
||||
/// </summary>
|
||||
/// <returns>本机MAC地址字节数组</returns>
|
||||
private byte[] GetLocalMACAddress()
|
||||
{
|
||||
try
|
||||
{
|
||||
return System.Net.NetworkInformation.NetworkInterface
|
||||
.GetAllNetworkInterfaces()
|
||||
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
|
||||
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
|
||||
.Select(nic => nic.GetPhysicalAddress()?.GetAddressBytes())
|
||||
.FirstOrDefault(bytes => bytes != null && bytes.Length == 6) ?? new byte[6];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取本机MAC地址失败");
|
||||
return new byte[6];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本机网络接口名称
|
||||
/// </summary>
|
||||
/// <returns>网络接口名称</returns>
|
||||
private string GetLocalNetworkInterface()
|
||||
{
|
||||
return GetLocalIPAddress().ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化ARP记录
|
||||
/// </summary>
|
||||
/// <returns>是否成功</returns>
|
||||
private async Task<bool> InitializeArpAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await ArpClient.UpdateArpEntryAsync(BOARD_IP);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "初始化ARP记录失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取主机IP地址
|
||||
/// </summary>
|
||||
/// <returns>主机IP地址</returns>
|
||||
[HttpGet("GetHostIP")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> GetHostIP()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!(await InitializeArpAsync()))
|
||||
{
|
||||
throw new Exception("无法配置ARP记录");
|
||||
}
|
||||
|
||||
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
|
||||
var result = await netConfig.GetHostIP();
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取主机IP失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取失败: {result.Error}");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取主机IP时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取板卡IP地址
|
||||
/// </summary>
|
||||
/// <returns>板卡IP地址</returns>
|
||||
[HttpGet("GetBoardIP")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> GetBoardIP()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!(await InitializeArpAsync()))
|
||||
{
|
||||
throw new Exception("无法配置ARP记录");
|
||||
}
|
||||
|
||||
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
|
||||
var result = await netConfig.GetBoardIP();
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取板卡IP失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取失败: {result.Error}");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取板卡IP时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取主机MAC地址
|
||||
/// </summary>
|
||||
/// <returns>主机MAC地址</returns>
|
||||
[HttpGet("GetHostMAC")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> GetHostMAC()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!(await InitializeArpAsync()))
|
||||
{
|
||||
throw new Exception("无法配置ARP记录");
|
||||
}
|
||||
|
||||
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
|
||||
var result = await netConfig.GetHostMAC();
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取主机MAC地址失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取失败: {result.Error}");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取主机MAC地址时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取板卡MAC地址
|
||||
/// </summary>
|
||||
/// <returns>板卡MAC地址</returns>
|
||||
[HttpGet("GetBoardMAC")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> GetBoardMAC()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!(await InitializeArpAsync()))
|
||||
{
|
||||
throw new Exception("无法配置ARP记录");
|
||||
}
|
||||
|
||||
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
|
||||
var result = await netConfig.GetBoardMAC();
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取板卡MAC地址失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取失败: {result.Error}");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取板卡MAC地址时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有网络配置信息
|
||||
/// </summary>
|
||||
/// <returns>网络配置信息</returns>
|
||||
[HttpGet("GetNetworkConfig")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(NetworkConfigDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> GetNetworkConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!(await InitializeArpAsync()))
|
||||
{
|
||||
throw new Exception("无法配置ARP记录");
|
||||
}
|
||||
|
||||
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
|
||||
|
||||
var hostIPResult = await netConfig.GetHostIP();
|
||||
var boardIPResult = await netConfig.GetBoardIP();
|
||||
var hostMACResult = await netConfig.GetHostMAC();
|
||||
var boardMACResult = await netConfig.GetBoardMAC();
|
||||
|
||||
var config = new NetworkConfigDto
|
||||
{
|
||||
HostIP = hostIPResult.IsSuccessful ? hostIPResult.Value : "获取失败",
|
||||
BoardIP = boardIPResult.IsSuccessful ? boardIPResult.Value : "获取失败",
|
||||
HostMAC = hostMACResult.IsSuccessful ? hostMACResult.Value : "获取失败",
|
||||
BoardMAC = boardMACResult.IsSuccessful ? boardMACResult.Value : "获取失败"
|
||||
};
|
||||
|
||||
return Ok(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取网络配置信息时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本机所有网络接口信息
|
||||
/// </summary>
|
||||
/// <returns>网络接口信息列表</returns>
|
||||
[HttpGet("GetLocalNetworkInterfaces")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(List<NetworkInterfaceDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult GetLocalNetworkInterfaces()
|
||||
{
|
||||
try
|
||||
{
|
||||
var interfaces = System.Net.NetworkInformation.NetworkInterface
|
||||
.GetAllNetworkInterfaces()
|
||||
.Where(nic => nic.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
|
||||
&& nic.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)
|
||||
.Select(nic => new NetworkInterfaceDto
|
||||
{
|
||||
Name = nic.Name,
|
||||
Description = nic.Description,
|
||||
Type = nic.NetworkInterfaceType.ToString(),
|
||||
Status = nic.OperationalStatus.ToString(),
|
||||
IPAddresses = nic.GetIPProperties().UnicastAddresses
|
||||
.Where(addr => addr.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
|
||||
.Select(addr => addr.Address.ToString())
|
||||
.ToList(),
|
||||
MACAddress = nic.GetPhysicalAddress().ToString()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Ok(interfaces);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取本机网络接口信息时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置主机IP地址
|
||||
/// </summary>
|
||||
/// <param name="hostIp">主机IP地址</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetHostIP")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> SetHostIP(string hostIp)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hostIp))
|
||||
return BadRequest("主机IP地址不能为空");
|
||||
|
||||
if (!IPAddress.TryParse(hostIp, out var hostIpAddress))
|
||||
return BadRequest("主机IP地址格式不正确");
|
||||
|
||||
try
|
||||
{
|
||||
if (!(await InitializeArpAsync()))
|
||||
{
|
||||
throw new Exception("无法配置ARP记录");
|
||||
}
|
||||
|
||||
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
|
||||
var result = await netConfig.SetHostIP(hostIpAddress);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置主机IP失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置主机IP时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置板卡IP地址
|
||||
/// </summary>
|
||||
/// <param name="newBoardIp">新的板卡IP地址</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetBoardIP")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> SetBoardIP(string newBoardIp)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newBoardIp))
|
||||
return BadRequest("新的板卡IP地址不能为空");
|
||||
|
||||
if (!IPAddress.TryParse(newBoardIp, out var newIpAddress))
|
||||
return BadRequest("新的板卡IP地址格式不正确");
|
||||
|
||||
try
|
||||
{
|
||||
if (!(await InitializeArpAsync()))
|
||||
{
|
||||
throw new Exception("无法配置ARP记录");
|
||||
}
|
||||
|
||||
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
|
||||
var result = await netConfig.SetBoardIP(newIpAddress);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置板卡IP失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置板卡IP时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置板卡MAC地址
|
||||
/// </summary>
|
||||
/// <param name="boardMac">板卡MAC地址(格式:AA:BB:CC:DD:EE:FF)</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetBoardMAC")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> SetBoardMAC(string boardMac)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(boardMac))
|
||||
return BadRequest("板卡MAC地址不能为空");
|
||||
|
||||
// 解析MAC地址
|
||||
if (!TryParseMacAddress(boardMac, out var macBytes))
|
||||
return BadRequest("MAC地址格式不正确,请使用格式:AA:BB:CC:DD:EE:FF");
|
||||
|
||||
try
|
||||
{
|
||||
if (!(await InitializeArpAsync()))
|
||||
{
|
||||
throw new Exception("无法配置ARP记录");
|
||||
}
|
||||
// 创建网络配置客户端
|
||||
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
|
||||
var result = await netConfig.SetBoardMAC(macBytes);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置板卡MAC地址失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置板卡MAC地址时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置主机MAC地址
|
||||
/// </summary>
|
||||
/// <param name="hostMac">主机MAC地址(格式:AA:BB:CC:DD:EE:FF)</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetHostMAC")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> SetHostMAC(string hostMac)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hostMac))
|
||||
return BadRequest("主机MAC地址不能为空");
|
||||
|
||||
// 解析MAC地址
|
||||
if (!TryParseMacAddress(hostMac, out var macBytes))
|
||||
return BadRequest("MAC地址格式不正确,请使用格式:AA:BB:CC:DD:EE:FF");
|
||||
|
||||
try
|
||||
{
|
||||
if (!(await InitializeArpAsync()))
|
||||
{
|
||||
throw new Exception("无法配置ARP记录");
|
||||
}
|
||||
|
||||
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
|
||||
var result = await netConfig.SetHostMAC(macBytes);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置主机MAC地址失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置主机MAC地址时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动获取本机IP地址并设置为实验板主机IP
|
||||
/// </summary>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("UpdateHostIP")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> UpdateHostIP()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_localIP == null)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "无法获取本机IP地址");
|
||||
|
||||
if (!(await InitializeArpAsync()))
|
||||
{
|
||||
throw new Exception("无法配置ARP记录");
|
||||
}
|
||||
|
||||
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
|
||||
var result = await netConfig.SetHostIP(_localIP);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"自动设置主机IP失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "自动设置主机IP时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新主机MAC地址
|
||||
/// </summary>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("UpdateHostMAC")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> UpdateHostMAC()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_localMAC == null || _localMAC.Length != 6)
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "无法获取本机MAC地址");
|
||||
|
||||
if (!(await InitializeArpAsync()))
|
||||
{
|
||||
throw new Exception("无法配置ARP记录");
|
||||
}
|
||||
|
||||
var netConfig = new NetConfig(BOARD_IP, BOARD_PORT, 0);
|
||||
var result = await netConfig.SetHostMAC(_localMAC);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置主机MAC地址失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"设置失败: {result.Error}");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置主机MAC地址时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本机网络信息
|
||||
/// </summary>
|
||||
/// <returns>本机网络信息</returns>
|
||||
[HttpGet("GetLocalNetworkInfo")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
public IActionResult GetLocalNetworkInfo()
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
LocalIP = _localIPString,
|
||||
LocalMAC = _localMACString,
|
||||
LocalInterface = _localInterface
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析MAC地址字符串为字节数组
|
||||
/// </summary>
|
||||
/// <param name="macAddress">MAC地址字符串</param>
|
||||
/// <param name="macBytes">解析后的字节数组</param>
|
||||
/// <returns>是否解析成功</returns>
|
||||
private static bool TryParseMacAddress(string macAddress, out byte[] macBytes)
|
||||
{
|
||||
macBytes = Array.Empty<byte>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(macAddress))
|
||||
return false;
|
||||
|
||||
// 移除可能的分隔符并统一为冒号
|
||||
var cleanMac = macAddress.Replace("-", ":").Replace(" ", "").ToUpper();
|
||||
|
||||
// 验证格式
|
||||
if (cleanMac.Length != 17 || cleanMac.Count(c => c == ':') != 5)
|
||||
return false;
|
||||
|
||||
var parts = cleanMac.Split(':');
|
||||
if (parts.Length != 6)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
macBytes = new byte[6];
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
macBytes[i] = Convert.ToByte(parts[i], 16);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
macBytes = Array.Empty<byte>();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 网络配置数据传输对象
|
||||
/// </summary>
|
||||
public class NetworkConfigDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 主机IP地址
|
||||
/// </summary>
|
||||
public string? HostIP { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 板卡IP地址
|
||||
/// </summary>
|
||||
public string? BoardIP { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 主机MAC地址
|
||||
/// </summary>
|
||||
public string? HostMAC { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 板卡MAC地址
|
||||
/// </summary>
|
||||
public string? BoardMAC { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 网络配置操作结果
|
||||
/// </summary>
|
||||
public class NetworkConfigResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 主机IP设置结果
|
||||
/// </summary>
|
||||
public bool? HostIPResult { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 主机IP设置错误信息
|
||||
/// </summary>
|
||||
public string? HostIPError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 板卡IP设置结果
|
||||
/// </summary>
|
||||
public bool? BoardIPResult { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 板卡IP设置错误信息
|
||||
/// </summary>
|
||||
public string? BoardIPError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 主机MAC设置结果
|
||||
/// </summary>
|
||||
public bool? HostMACResult { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 主机MAC设置错误信息
|
||||
/// </summary>
|
||||
public string? HostMACError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 板卡MAC设置结果
|
||||
/// </summary>
|
||||
public bool? BoardMACResult { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 板卡MAC设置错误信息
|
||||
/// </summary>
|
||||
public string? BoardMACError { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 网络接口信息数据传输对象
|
||||
/// </summary>
|
||||
public class NetworkInterfaceDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 网络接口名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 网络接口描述
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 网络接口类型
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 网络接口状态
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// IP地址列表
|
||||
/// </summary>
|
||||
public List<string> IPAddresses { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// MAC地址
|
||||
/// </summary>
|
||||
public string MACAddress { get; set; } = string.Empty;
|
||||
}
|
||||
484
server/src/Controllers/OscilloscopeController.cs
Normal file
484
server/src/Controllers/OscilloscopeController.cs
Normal file
@@ -0,0 +1,484 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Peripherals.OscilloscopeClient;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 示波器API控制器 - 普通用户权限
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize]
|
||||
public class OscilloscopeApiController : ControllerBase
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 示波器完整配置
|
||||
/// </summary>
|
||||
public class OscilloscopeFullConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启动捕获
|
||||
/// </summary>
|
||||
public bool CaptureEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发电平(0-255)
|
||||
/// </summary>
|
||||
public byte TriggerLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 触发边沿(true为上升沿,false为下降沿)
|
||||
/// </summary>
|
||||
public bool TriggerRisingEdge { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 水平偏移量(0-1023)
|
||||
/// </summary>
|
||||
public ushort HorizontalShift { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 抽样率(0-1023)
|
||||
/// </summary>
|
||||
public ushort DecimationRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否自动刷新RAM
|
||||
/// </summary>
|
||||
public bool AutoRefreshRAM { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 示波器状态和数据
|
||||
/// </summary>
|
||||
public class OscilloscopeDataResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// AD采样频率
|
||||
/// </summary>
|
||||
public uint ADFrequency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AD采样幅度
|
||||
/// </summary>
|
||||
public byte ADVpp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AD采样最大值
|
||||
/// </summary>
|
||||
public byte ADMax { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AD采样最小值
|
||||
/// </summary>
|
||||
public byte ADMin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 波形数据(Base64编码)
|
||||
/// </summary>
|
||||
public string WaveformData { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取示波器实例
|
||||
/// </summary>
|
||||
private Oscilloscope? GetOscilloscope()
|
||||
{
|
||||
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 Oscilloscope(board.IpAddr, board.Port);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取示波器实例时发生异常");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化示波器
|
||||
/// </summary>
|
||||
/// <param name="config">示波器配置</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("Initialize")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> Initialize([FromBody] OscilloscopeFullConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (config == null)
|
||||
return BadRequest("配置参数不能为空");
|
||||
|
||||
if (config.HorizontalShift > 1023)
|
||||
return BadRequest("水平偏移量必须在0-1023之间");
|
||||
|
||||
if (config.DecimationRate > 1023)
|
||||
return BadRequest("抽样率必须在0-1023之间");
|
||||
|
||||
var oscilloscope = GetOscilloscope();
|
||||
if (oscilloscope == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
// 首先关闭捕获
|
||||
var stopResult = await oscilloscope.SetCaptureEnable(false);
|
||||
if (!stopResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"关闭捕获失败: {stopResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "关闭捕获失败");
|
||||
}
|
||||
|
||||
// 设置触发电平
|
||||
var levelResult = await oscilloscope.SetTriggerLevel(config.TriggerLevel);
|
||||
if (!levelResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置触发电平失败: {levelResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置触发电平失败");
|
||||
}
|
||||
|
||||
// 设置触发边沿
|
||||
var edgeResult = await oscilloscope.SetTriggerEdge(config.TriggerRisingEdge);
|
||||
if (!edgeResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置触发边沿失败: {edgeResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置触发边沿失败");
|
||||
}
|
||||
|
||||
// 设置水平偏移量
|
||||
var shiftResult = await oscilloscope.SetHorizontalShift(config.HorizontalShift);
|
||||
if (!shiftResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置水平偏移量失败: {shiftResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置水平偏移量失败");
|
||||
}
|
||||
|
||||
// 设置抽样率
|
||||
var rateResult = await oscilloscope.SetDecimationRate(config.DecimationRate);
|
||||
if (!rateResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置抽样率失败: {rateResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置抽样率失败");
|
||||
}
|
||||
|
||||
// 刷新RAM
|
||||
if (config.AutoRefreshRAM)
|
||||
{
|
||||
var refreshResult = await oscilloscope.RefreshRAM();
|
||||
if (!refreshResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"刷新RAM失败: {refreshResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "刷新RAM失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 设置捕获开关
|
||||
var captureResult = await oscilloscope.SetCaptureEnable(config.CaptureEnabled);
|
||||
if (!captureResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置捕获开关失败: {captureResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置捕获开关失败");
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "初始化示波器时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动捕获
|
||||
/// </summary>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("StartCapture")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> StartCapture()
|
||||
{
|
||||
try
|
||||
{
|
||||
var oscilloscope = GetOscilloscope();
|
||||
if (oscilloscope == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await oscilloscope.SetCaptureEnable(true);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"启动捕获失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "启动捕获失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "启动捕获时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止捕获
|
||||
/// </summary>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("StopCapture")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> StopCapture()
|
||||
{
|
||||
try
|
||||
{
|
||||
var oscilloscope = GetOscilloscope();
|
||||
if (oscilloscope == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await oscilloscope.SetCaptureEnable(false);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"停止捕获失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "停止捕获失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "停止捕获时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取示波器数据和状态
|
||||
/// </summary>
|
||||
/// <returns>示波器数据和状态信息</returns>
|
||||
[HttpGet("GetData")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(OscilloscopeDataResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> GetData()
|
||||
{
|
||||
try
|
||||
{
|
||||
var oscilloscope = GetOscilloscope();
|
||||
if (oscilloscope == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var freqResult = await oscilloscope.GetADFrequency();
|
||||
var vppResult = await oscilloscope.GetADVpp();
|
||||
var maxResult = await oscilloscope.GetADMax();
|
||||
var minResult = await oscilloscope.GetADMin();
|
||||
var waveformResult = await oscilloscope.GetWaveformData();
|
||||
|
||||
if (!freqResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取AD采样频率失败: {freqResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取AD采样频率失败");
|
||||
}
|
||||
|
||||
if (!vppResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取AD采样幅度失败: {vppResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取AD采样幅度失败");
|
||||
}
|
||||
|
||||
if (!maxResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取AD采样最大值失败: {maxResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取AD采样最大值失败");
|
||||
}
|
||||
|
||||
if (!minResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取AD采样最小值失败: {minResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取AD采样最小值失败");
|
||||
}
|
||||
|
||||
if (!waveformResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取波形数据失败: {waveformResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "获取波形数据失败");
|
||||
}
|
||||
|
||||
var response = new OscilloscopeDataResponse
|
||||
{
|
||||
ADFrequency = freqResult.Value,
|
||||
ADVpp = vppResult.Value,
|
||||
ADMax = maxResult.Value,
|
||||
ADMin = minResult.Value,
|
||||
WaveformData = Convert.ToBase64String(waveformResult.Value)
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取示波器数据时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新触发参数
|
||||
/// </summary>
|
||||
/// <param name="level">触发电平(0-255)</param>
|
||||
/// <param name="risingEdge">触发边沿(true为上升沿,false为下降沿)</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("UpdateTrigger")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> UpdateTrigger(byte level, bool risingEdge)
|
||||
{
|
||||
try
|
||||
{
|
||||
var oscilloscope = GetOscilloscope();
|
||||
if (oscilloscope == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
// 设置触发电平
|
||||
var levelResult = await oscilloscope.SetTriggerLevel(level);
|
||||
if (!levelResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置触发电平失败: {levelResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置触发电平失败");
|
||||
}
|
||||
|
||||
// 设置触发边沿
|
||||
var edgeResult = await oscilloscope.SetTriggerEdge(risingEdge);
|
||||
if (!edgeResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置触发边沿失败: {edgeResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置触发边沿失败");
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "更新触发参数时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新采样参数
|
||||
/// </summary>
|
||||
/// <param name="horizontalShift">水平偏移量(0-1023)</param>
|
||||
/// <param name="decimationRate">抽样率(0-1023)</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("UpdateSampling")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> UpdateSampling(ushort horizontalShift, ushort decimationRate)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (horizontalShift > 1023)
|
||||
return BadRequest("水平偏移量必须在0-1023之间");
|
||||
|
||||
if (decimationRate > 1023)
|
||||
return BadRequest("抽样率必须在0-1023之间");
|
||||
|
||||
var oscilloscope = GetOscilloscope();
|
||||
if (oscilloscope == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
// 设置水平偏移量
|
||||
var shiftResult = await oscilloscope.SetHorizontalShift(horizontalShift);
|
||||
if (!shiftResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置水平偏移量失败: {shiftResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置水平偏移量失败");
|
||||
}
|
||||
|
||||
// 设置抽样率
|
||||
var rateResult = await oscilloscope.SetDecimationRate(decimationRate);
|
||||
if (!rateResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置抽样率失败: {rateResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置抽样率失败");
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "更新采样参数时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动刷新RAM
|
||||
/// </summary>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("RefreshRAM")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> RefreshRAM()
|
||||
{
|
||||
try
|
||||
{
|
||||
var oscilloscope = GetOscilloscope();
|
||||
if (oscilloscope == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await oscilloscope.RefreshRAM();
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"刷新RAM失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "刷新RAM失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "刷新RAM时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
0
server/src/Controllers/ProgressController.cs
Normal file
0
server/src/Controllers/ProgressController.cs
Normal file
@@ -1,4 +1,5 @@
|
||||
using DotNext;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -24,6 +25,7 @@ public class RemoteUpdateController : ControllerBase
|
||||
/// <param name="bitstream2">比特流文件2</param>
|
||||
/// <param name="bitstream3">比特流文件3</param>
|
||||
/// <returns>上传结果</returns>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("UploadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
@@ -129,6 +131,7 @@ public class RemoteUpdateController : ControllerBase
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
/// <param name="bitstreamNum"> 比特流位号 </param>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("DownloadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
@@ -150,7 +153,7 @@ public class RemoteUpdateController : ControllerBase
|
||||
if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error);
|
||||
|
||||
// 下载比特流
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var ret = await remoteUpdater.UpdateBitstream(bitstreamNum, fileBytes.Value);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
@@ -179,6 +182,7 @@ public class RemoteUpdateController : ControllerBase
|
||||
/// <param name="port">设备端口</param>
|
||||
/// <param name="bitstreamNum">比特流编号</param>
|
||||
/// <returns>总共上传比特流的数量</returns>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("DownloadMultiBitstreams")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
|
||||
@@ -210,7 +214,7 @@ public class RemoteUpdateController : ControllerBase
|
||||
}
|
||||
|
||||
// 下载比特流
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
{
|
||||
var ret = await remoteUpdater.UploadBitstreams(bitstreams[0], bitstreams[1], bitstreams[2], bitstreams[3]);
|
||||
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
|
||||
@@ -239,6 +243,7 @@ public class RemoteUpdateController : ControllerBase
|
||||
/// <param name="port">设备端口</param>
|
||||
/// <param name="bitstreamNum">比特流编号</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("HotResetBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
@@ -246,7 +251,7 @@ public class RemoteUpdateController : ControllerBase
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> HotResetBitstream(string address, int port, int bitstreamNum)
|
||||
{
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var ret = await remoteUpdater.HotResetBitstream(bitstreamNum);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
@@ -267,6 +272,7 @@ public class RemoteUpdateController : ControllerBase
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost("GetFirmwareVersion")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(UInt32), StatusCodes.Status200OK)]
|
||||
@@ -274,7 +280,7 @@ public class RemoteUpdateController : ControllerBase
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> GetFirmwareVersion(string address, int port)
|
||||
{
|
||||
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var remoteUpdater = new Peripherals.RemoteUpdateClient.RemoteUpdater(address, port);
|
||||
var ret = await remoteUpdater.GetVersion();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
|
||||
@@ -14,6 +14,11 @@ public class TutorialController : ControllerBase
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="environment">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public TutorialController(IWebHostEnvironment environment)
|
||||
{
|
||||
_environment = environment;
|
||||
@@ -106,15 +106,16 @@ public class UDPController : ControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定IP地址接受的数据列表
|
||||
/// 获取指定IP地址接收的数据列表
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
[HttpGet("GetRecvDataArray")]
|
||||
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> GetRecvDataArray(string address)
|
||||
public async ValueTask<IResult> GetRecvDataArray(string address, int taskID)
|
||||
{
|
||||
var ret = await MsgBus.UDPServer.GetDataArrayAsync(address);
|
||||
var ret = await MsgBus.UDPServer.GetDataArrayAsync(address, taskID);
|
||||
|
||||
if (ret.HasValue)
|
||||
{
|
||||
|
||||
556
server/src/Controllers/VideoStreamController.cs
Normal file
556
server/src/Controllers/VideoStreamController.cs
Normal file
@@ -0,0 +1,556 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
/// <summary>
|
||||
/// 视频流控制器,支持动态配置摄像头连接
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class VideoStreamController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
private readonly server.Services.HttpVideoStreamService _videoStreamService;
|
||||
|
||||
/// <summary>
|
||||
/// 视频流信息结构体
|
||||
/// </summary>
|
||||
public class StreamInfoResult
|
||||
{
|
||||
/// <summary>
|
||||
/// TODO:
|
||||
/// </summary>
|
||||
public int FrameRate { get; set; }
|
||||
/// <summary>
|
||||
/// TODO:
|
||||
/// </summary>
|
||||
public int FrameWidth { get; set; }
|
||||
/// <summary>
|
||||
/// TODO:
|
||||
/// </summary>
|
||||
public int FrameHeight { get; set; }
|
||||
/// <summary>
|
||||
/// TODO:
|
||||
/// </summary>
|
||||
public string Format { get; set; } = "MJPEG";
|
||||
/// <summary>
|
||||
/// TODO:
|
||||
/// </summary>
|
||||
public string HtmlUrl { get; set; } = "";
|
||||
/// <summary>
|
||||
/// TODO:
|
||||
/// </summary>
|
||||
public string MjpegUrl { get; set; } = "";
|
||||
/// <summary>
|
||||
/// TODO:
|
||||
/// </summary>
|
||||
public string SnapshotUrl { get; set; } = "";
|
||||
/// <summary>
|
||||
/// TODO:
|
||||
/// </summary>
|
||||
public string UsbCameraUrl { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头配置请求模型
|
||||
/// </summary>
|
||||
public class CameraConfigRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 摄像头地址
|
||||
/// </summary>
|
||||
[Required]
|
||||
[RegularExpression(@"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", ErrorMessage = "请输入有效的IP地址")]
|
||||
public string Address { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 摄像头端口
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Range(1, 65535, ErrorMessage = "端口必须在1-65535范围内")]
|
||||
public int Port { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分辨率配置请求模型
|
||||
/// </summary>
|
||||
public class ResolutionConfigRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 宽度
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Range(640, 1920, ErrorMessage = "宽度必须在640-1920范围内")]
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 高度
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Range(480, 1080, ErrorMessage = "高度必须在480-1080范围内")]
|
||||
public int Height { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化HTTP视频流控制器
|
||||
/// </summary>
|
||||
/// <param name="videoStreamService">HTTP视频流服务</param>
|
||||
public VideoStreamController(server.Services.HttpVideoStreamService videoStreamService)
|
||||
{
|
||||
logger.Info("创建VideoStreamController,命名空间:{Namespace}", this.GetType().Namespace);
|
||||
_videoStreamService = videoStreamService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 HTTP 视频流服务状态
|
||||
/// </summary>
|
||||
/// <returns>服务状态信息</returns>
|
||||
[HttpGet("Status")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public IResult GetStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("GetStatus方法被调用,控制器:{Controller},路径:api/VideoStream/Status", this.GetType().Name);
|
||||
|
||||
// 使用HttpVideoStreamService提供的状态信息
|
||||
var status = _videoStreamService.GetServiceStatus();
|
||||
|
||||
// 转换为小写首字母的JSON属性(符合前端惯例)
|
||||
return TypedResults.Ok(status);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取 HTTP 视频流服务状态失败");
|
||||
return TypedResults.InternalServerError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 HTTP 视频流信息
|
||||
/// </summary>
|
||||
/// <returns>流信息</returns>
|
||||
[HttpGet("StreamInfo")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(StreamInfoResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public IResult GetStreamInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("获取 HTTP 视频流信息");
|
||||
var result = new StreamInfoResult
|
||||
{
|
||||
FrameRate = _videoStreamService.FrameRate,
|
||||
FrameWidth = _videoStreamService.FrameWidth,
|
||||
FrameHeight = _videoStreamService.FrameHeight,
|
||||
Format = "MJPEG",
|
||||
HtmlUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/video-feed.html",
|
||||
MjpegUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/video-stream",
|
||||
SnapshotUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/snapshot",
|
||||
UsbCameraUrl = $"http://{Global.localhost}:{_videoStreamService.ServerPort}/usb-camera"
|
||||
};
|
||||
return TypedResults.Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取 HTTP 视频流信息失败");
|
||||
return TypedResults.InternalServerError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置摄像头连接参数
|
||||
/// </summary>
|
||||
/// <param name="config">摄像头配置</param>
|
||||
/// <returns>配置结果</returns>
|
||||
[HttpPost("ConfigureCamera")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IResult> ConfigureCamera([FromBody] CameraConfigRequest config)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("配置摄像头连接: {Address}:{Port}", config.Address, config.Port);
|
||||
|
||||
var success = await _videoStreamService.ConfigureCameraAsync(config.Address, config.Port);
|
||||
|
||||
if (success)
|
||||
{
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = "摄像头配置成功",
|
||||
cameraAddress = config.Address,
|
||||
cameraPort = config.Port
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return TypedResults.BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
message = "摄像头配置失败",
|
||||
cameraAddress = config.Address,
|
||||
cameraPort = config.Port
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "配置摄像头连接失败");
|
||||
return TypedResults.InternalServerError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前摄像头配置
|
||||
/// </summary>
|
||||
/// <returns>摄像头配置信息</returns>
|
||||
[HttpGet("CameraConfig")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public IResult GetCameraConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("获取摄像头配置");
|
||||
var cameraStatus = _videoStreamService.GetCameraStatus();
|
||||
|
||||
return TypedResults.Ok(cameraStatus);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取摄像头配置失败");
|
||||
return TypedResults.InternalServerError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 控制 HTTP 视频流服务开关
|
||||
/// </summary>
|
||||
/// <param name="enabled">是否启用服务</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetEnabled")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IResult> SetEnabled([FromQuery] bool enabled)
|
||||
{
|
||||
logger.Info("设置视频流服务开关: {Enabled}", enabled);
|
||||
await _videoStreamService.SetEnable(enabled);
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 HTTP 视频流连接
|
||||
/// </summary>
|
||||
/// <returns>连接测试结果</returns>
|
||||
[HttpPost("TestConnection")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IResult> TestConnection()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("测试 HTTP 视频流连接");
|
||||
|
||||
// 尝试通过HTTP请求检查视频流服务是否可访问
|
||||
bool isConnected = false;
|
||||
using (var httpClient = new HttpClient())
|
||||
{
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(2); // 设置较短的超时时间
|
||||
var response = await httpClient.GetAsync($"http://{Global.localhost}:{_videoStreamService.ServerPort}/");
|
||||
|
||||
// 只要能连接上就认为成功,不管返回状态
|
||||
isConnected = response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
logger.Info("测试摄像头连接");
|
||||
|
||||
var (isSuccess, message) = await _videoStreamService.TestCameraConnectionAsync();
|
||||
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
isConnected = isConnected,
|
||||
success = isSuccess,
|
||||
message = message,
|
||||
cameraAddress = _videoStreamService.CameraAddress,
|
||||
cameraPort = _videoStreamService.CameraPort,
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "HTTP 视频流连接测试失败");
|
||||
// 连接失败但不抛出异常,而是返回连接失败的结果
|
||||
return TypedResults.Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置视频流分辨率
|
||||
/// </summary>
|
||||
/// <param name="request">分辨率配置请求</param>
|
||||
/// <returns>设置结果</returns>
|
||||
[HttpPost("Resolution")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IResult> SetResolution([FromBody] ResolutionConfigRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info($"设置视频流分辨率为 {request.Width}x{request.Height}");
|
||||
|
||||
var (isSuccess, message) = await _videoStreamService.SetResolutionAsync(request.Width, request.Height);
|
||||
|
||||
if (isSuccess)
|
||||
{
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = message,
|
||||
width = request.Width,
|
||||
height = request.Height,
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return TypedResults.BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
message = message,
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"设置分辨率为 {request.Width}x{request.Height} 失败");
|
||||
return TypedResults.InternalServerError($"设置分辨率失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前分辨率
|
||||
/// </summary>
|
||||
/// <returns>当前分辨率信息</returns>
|
||||
[HttpGet("Resolution")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
|
||||
public IResult GetCurrentResolution()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("获取当前视频流分辨率");
|
||||
|
||||
var (width, height) = _videoStreamService.GetCurrentResolution();
|
||||
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
width = width,
|
||||
height = height,
|
||||
resolution = $"{width}x{height}",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取当前分辨率失败");
|
||||
return TypedResults.InternalServerError($"获取当前分辨率失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取支持的分辨率列表
|
||||
/// </summary>
|
||||
/// <returns>支持的分辨率列表</returns>
|
||||
[HttpGet("SupportedResolutions")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
|
||||
public IResult GetSupportedResolutions()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("获取支持的分辨率列表");
|
||||
|
||||
var resolutions = _videoStreamService.GetSupportedResolutions();
|
||||
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
resolutions = resolutions.Select(r => new
|
||||
{
|
||||
width = r.Width,
|
||||
height = r.Height,
|
||||
name = r.Name,
|
||||
value = $"{r.Width}x{r.Height}"
|
||||
}),
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "获取支持的分辨率列表失败");
|
||||
return TypedResults.InternalServerError($"获取支持的分辨率列表失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化摄像头自动对焦功能
|
||||
/// </summary>
|
||||
/// <returns>初始化结果</returns>
|
||||
[HttpPost("InitAutoFocus")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IResult> InitAutoFocus()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("收到初始化自动对焦请求");
|
||||
|
||||
var result = await _videoStreamService.InitAutoFocusAsync();
|
||||
|
||||
if (result)
|
||||
{
|
||||
logger.Info("自动对焦初始化成功");
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = "自动对焦初始化成功",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("自动对焦初始化失败");
|
||||
return TypedResults.BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
message = "自动对焦初始化失败",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "初始化自动对焦时发生异常");
|
||||
return TypedResults.InternalServerError($"初始化自动对焦失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行自动对焦
|
||||
/// </summary>
|
||||
/// <returns>对焦结果</returns>
|
||||
[HttpPost("AutoFocus")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IResult> AutoFocus()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("收到执行自动对焦请求");
|
||||
|
||||
var result = await _videoStreamService.PerformAutoFocusAsync();
|
||||
|
||||
if (result)
|
||||
{
|
||||
logger.Info("自动对焦执行成功");
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = "自动对焦执行成功",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("自动对焦执行失败");
|
||||
return TypedResults.BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
message = "自动对焦执行失败",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "执行自动对焦时发生异常");
|
||||
return TypedResults.InternalServerError($"执行自动对焦失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行一次自动对焦 (GET方式)
|
||||
/// </summary>
|
||||
/// <returns>对焦结果</returns>
|
||||
[HttpGet("Focus")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IResult> Focus()
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Info("收到执行一次对焦请求 (GET)");
|
||||
|
||||
// 检查摄像头是否已配置
|
||||
if (!_videoStreamService.IsCameraConfigured())
|
||||
{
|
||||
logger.Warn("摄像头未配置,无法执行对焦");
|
||||
return TypedResults.BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
message = "摄像头未配置,请先配置摄像头连接",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
|
||||
var result = await _videoStreamService.PerformAutoFocusAsync();
|
||||
|
||||
if (result)
|
||||
{
|
||||
logger.Info("对焦执行成功");
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = "对焦执行成功",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("对焦执行失败");
|
||||
return TypedResults.BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
message = "对焦执行失败",
|
||||
timestamp = DateTime.Now
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "执行对焦时发生异常");
|
||||
return TypedResults.InternalServerError($"执行对焦失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,9 @@
|
||||
/// </summary>
|
||||
public static class MsgBus
|
||||
{
|
||||
private static readonly UDPServer udpServer = new UDPServer(1234);
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private static readonly UDPServer udpServer = new UDPServer(1234, 12);
|
||||
/// <summary>
|
||||
/// 获取UDP服务器
|
||||
/// </summary>
|
||||
@@ -19,8 +21,13 @@ public static class MsgBus
|
||||
/// 通信总线初始化
|
||||
/// </summary>
|
||||
/// <returns>无</returns>
|
||||
public static void Init()
|
||||
public async static void Init()
|
||||
{
|
||||
if (!ArpClient.IsAdministrator())
|
||||
{
|
||||
logger.Error($"非管理员运行,ARP无法更新,请用管理员权限运行");
|
||||
// throw new Exception($"非管理员运行,ARP无法更新,请用管理员权限运行");
|
||||
}
|
||||
udpServer.Start();
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
1426
server/src/Peripherals/CameraClient.cs
Normal file
1426
server/src/Peripherals/CameraClient.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -108,11 +108,11 @@ public class DDS
|
||||
if (waveNum < 0 || waveNum > 3) return new(new ArgumentException(
|
||||
$"Wave number should be 0 ~ 3 instead of {waveNum}", nameof(waveNum)));
|
||||
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 1);
|
||||
logger.Trace("Clear udp data finished");
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, DDSAddr.Channel[channelNum].WaveSelect, (UInt32)waveNum, this.timeout);
|
||||
this.ep, 1, DDSAddr.Channel[channelNum].WaveSelect, (UInt32)waveNum, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
return new(ret.Error);
|
||||
return ret.Value;
|
||||
@@ -132,11 +132,11 @@ public class DDS
|
||||
if (waveNum < 0 || waveNum > 3) return new(new ArgumentException(
|
||||
$"Wave number should be 0 ~ 3 instead of {waveNum}", nameof(waveNum)));
|
||||
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 1);
|
||||
logger.Trace("Clear udp data finished");
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, DDSAddr.Channel[channelNum].FreqCtrl[waveNum], step, this.timeout);
|
||||
this.ep, 1, DDSAddr.Channel[channelNum].FreqCtrl[waveNum], step, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
return new(ret.Error);
|
||||
return ret.Value;
|
||||
@@ -158,11 +158,11 @@ public class DDS
|
||||
if (phase < 0 || phase > 4096) return new(new ArgumentException(
|
||||
$"Phase should be 0 ~ 4096 instead of {phase}", nameof(phase)));
|
||||
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 1);
|
||||
logger.Trace("Clear udp data finished");
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, DDSAddr.Channel[channelNum].PhaseCtrl[waveNum], (UInt32)phase, this.timeout);
|
||||
this.ep, 1, DDSAddr.Channel[channelNum].PhaseCtrl[waveNum], (UInt32)phase, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
return new(ret.Error);
|
||||
return ret.Value;
|
||||
|
||||
254
server/src/Peripherals/DebuggerClient.cs
Normal file
254
server/src/Peripherals/DebuggerClient.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
using System.Net;
|
||||
using DotNext;
|
||||
|
||||
namespace Peripherals.DebuggerClient;
|
||||
|
||||
/// <summary>
|
||||
/// FPGA调试器的内存地址映射常量
|
||||
/// </summary>
|
||||
class DebuggerAddr
|
||||
{
|
||||
/// <summary>
|
||||
/// 触发器启动地址
|
||||
/// </summary>
|
||||
public const UInt32 Start = 0x5100_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新操作地址
|
||||
/// </summary>
|
||||
public const UInt32 Fresh = 0x5100_FFFF;
|
||||
|
||||
/// <summary>
|
||||
/// 信号标志读取地址
|
||||
/// </summary>
|
||||
public const UInt32 Signal = 0x5000_0001;
|
||||
|
||||
/// <summary>
|
||||
/// 数据读取基地址
|
||||
/// </summary>
|
||||
public const UInt32 Data = 0x5100_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 捕获模式设置地址
|
||||
/// </summary>
|
||||
public const UInt32 Mode = 0x5101_0000;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FPGA调试器命令常量
|
||||
/// </summary>
|
||||
class DebuggerCmd
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动触发器命令
|
||||
/// </summary>
|
||||
public const UInt32 Start = 0xFFFF_FFFF;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新命令
|
||||
/// </summary>
|
||||
public const UInt32 Fresh = 0x0000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 清除信号标志命令
|
||||
/// </summary>
|
||||
public const UInt32 ClearSignal = 0xFFFF_FFFF;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 信号捕获模式枚举
|
||||
/// </summary>
|
||||
public enum CaptureMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// 无捕获模式
|
||||
/// </summary>
|
||||
None = 0,
|
||||
/// <summary>
|
||||
/// 低电平触发模式
|
||||
/// </summary>
|
||||
Logic0 = 1,
|
||||
/// <summary>
|
||||
/// 高电平触发模式
|
||||
/// </summary>
|
||||
Logic1 = 2,
|
||||
/// <summary>
|
||||
/// 上升沿触发模式
|
||||
/// </summary>
|
||||
Rise = 3,
|
||||
/// <summary>
|
||||
/// 下降沿触发模式
|
||||
/// </summary>
|
||||
Fall = 4,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FPGA调试器客户端,用于通过UDP协议与FPGA调试器进行通信
|
||||
/// </summary>
|
||||
public class DebuggerClient
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
readonly int timeout = 2000;
|
||||
readonly int taskID;
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
private IPEndPoint ep;
|
||||
|
||||
private UInt32 captureDataAddr = 0x5100_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化FPGA调试器客户端
|
||||
/// </summary>
|
||||
/// <param name="address">FPGA设备的IP地址</param>
|
||||
/// <param name="port">通信端口号</param>
|
||||
/// <param name="taskID">任务标识符</param>
|
||||
/// <param name="timeout">通信超时时间(毫秒),默认2000ms</param>
|
||||
/// <exception cref="ArgumentException">当timeout为负数时抛出</exception>
|
||||
public DebuggerClient(string address, int port, int taskID, int timeout = 2000)
|
||||
{
|
||||
if (timeout < 0)
|
||||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||||
this.address = address;
|
||||
this.taskID = taskID;
|
||||
this.port = port;
|
||||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置信号捕获模式
|
||||
/// </summary>
|
||||
/// <param name="wireNum">要设置的线</param>
|
||||
/// <param name="mode">要设置的捕获模式</param>
|
||||
/// <returns>操作结果,成功返回true,失败返回错误信息</returns>
|
||||
public async ValueTask<Result<bool>> SetMode(UInt32 wireNum, CaptureMode mode)
|
||||
{
|
||||
if (wireNum > 512)
|
||||
{
|
||||
return new(new ArgumentException($"Wire Num can't be over 512, but receive num: {wireNum}"));
|
||||
}
|
||||
|
||||
UInt32 data = ((UInt32)mode);
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Mode + wireNum, data, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set mode: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to SetMode returned false");
|
||||
return new(new Exception("Failed to set mode"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动信号触发器开始捕获
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回true,失败返回错误信息</returns>
|
||||
public async ValueTask<Result<bool>> StartTrigger()
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Start, DebuggerCmd.Start, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to start trigger: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to StartTrigger returned false");
|
||||
return new(new Exception("Failed to start trigger"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取触发器状态标志
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回状态标志字节,失败返回错误信息</returns>
|
||||
public async ValueTask<Result<byte>> ReadFlag()
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, DebuggerAddr.Signal, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read flag: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 1)
|
||||
{
|
||||
logger.Error("ReadAddr returned invalid data for flag");
|
||||
return new(new Exception("Failed to read flag"));
|
||||
}
|
||||
return ret.Value.Options.Data[3];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除触发器状态标志
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回true,失败返回错误信息</returns>
|
||||
public async ValueTask<Result<bool>> ClearFlag()
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Signal, DebuggerCmd.ClearSignal, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to clear flag: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to ClearFlag returned false");
|
||||
return new(new Exception("Failed to clear flag"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定偏移地址读取捕获的数据
|
||||
/// </summary>
|
||||
/// <param name="portNum">Port数量</param>
|
||||
/// <returns>操作结果,成功返回捕获数据,失败返回错误信息</returns>
|
||||
public async ValueTask<Result<byte[]>> ReadData(UInt32 portNum)
|
||||
{
|
||||
var captureData = new byte[1024 * 4 * portNum];
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr4Bytes(this.ep, this.taskID, this.captureDataAddr, captureData.Length / 4, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read data: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (ret.Value.Length != captureData.Length)
|
||||
{
|
||||
logger.Error($"Receive capture data length should be {captureData.Length} instead of {ret.Value.Length}");
|
||||
return new(new Exception($"Receive capture data length should be {captureData.Length} instead of {ret.Value.Length}"));
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(ret.Value, 0, captureData, 0, captureData.Length);
|
||||
}
|
||||
|
||||
return captureData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新调试器状态,重置内部状态机
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回true,失败返回错误信息</returns>
|
||||
public async ValueTask<Result<bool>> Refresh()
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Fresh, DebuggerCmd.Fresh, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to refresh: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to Refresh returned false");
|
||||
return new(new Exception("Failed to refresh"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
315
server/src/Peripherals/I2cClient.cs
Normal file
315
server/src/Peripherals/I2cClient.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
using System.Net;
|
||||
using DotNext;
|
||||
|
||||
namespace Peripherals.I2cClient;
|
||||
|
||||
static class I2cAddr
|
||||
{
|
||||
|
||||
const UInt32 Base = 0x6000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0000:
|
||||
/// [7:0] 本次传输的i2c地址(最高位总为0);
|
||||
/// [8] 1为读,0为写;
|
||||
/// [16] 1为SCCB协议,0为I2C协议;
|
||||
/// [24] 1为开启本次传输,自动置零
|
||||
/// </summary>
|
||||
public const UInt32 BaseConfig = Base + 0x0000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0001:
|
||||
/// [15:0] 本次传输的数据量(以字节为单位,0为传1个字节);
|
||||
/// [31:16] 若本次传输为读的DUMMY数据量(字节为单位,0为传1个字节)
|
||||
/// </summary>
|
||||
public const UInt32 TranConfig = Base + 0x0000_0001;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0002: [0] cmd_done; [8] cmd_error;
|
||||
/// </summary>
|
||||
public const UInt32 Flag = Base + 0x0000_0002;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0003: FIFO写入口,仅低8位有效,只写
|
||||
/// </summary>
|
||||
public const UInt32 Write = Base + 0x0000_0003;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0004: FIFO读出口,仅低8位有效,只读
|
||||
/// </summary>
|
||||
public const UInt32 Read = Base + 0x0000_0004;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0005: [0] FIFO写入口清空;[8] FIFO读出口清空;
|
||||
/// </summary>
|
||||
public const UInt32 Clear = Base + 0x0000_0005;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:Enum]
|
||||
/// </summary>
|
||||
public enum I2cProtocol
|
||||
{
|
||||
/// <summary>
|
||||
/// [TODO:Enum]
|
||||
/// </summary>
|
||||
I2c = 0,
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:Enum]
|
||||
/// </summary>
|
||||
SCCB = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public class I2c
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
readonly int timeout = 2000;
|
||||
readonly int taskID;
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
private IPEndPoint ep;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="taskID">[TODO:parameter]</param>
|
||||
/// <param name="timeout">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public I2c(string address, int port, int taskID, int timeout = 2000)
|
||||
{
|
||||
if (timeout < 0)
|
||||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||||
this.address = address;
|
||||
this.taskID = taskID;
|
||||
this.port = port;
|
||||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 向指定I2C设备写入数据
|
||||
/// </summary>
|
||||
/// <param name="devAddr">I2C设备地址</param>
|
||||
/// <param name="data">要写入的数据</param>
|
||||
/// <param name="proto">I2C协议类型</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> WriteData(UInt32 devAddr, byte[] data, I2cProtocol proto)
|
||||
{
|
||||
if (data.Length > 0x0000_FFFF)
|
||||
{
|
||||
logger.Error($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF");
|
||||
return new(new ArgumentException($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF"));
|
||||
}
|
||||
|
||||
// 清除UDP服务器接收缓冲区
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
|
||||
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||
|
||||
// 写入数据到I2C FIFO写入口
|
||||
{
|
||||
var i2cData = new byte[data.Length * 4];
|
||||
int i = 0;
|
||||
foreach (var item in data)
|
||||
{
|
||||
i2cData[i++] = 0x00;
|
||||
i2cData[i++] = 0x00;
|
||||
i2cData[i++] = 0x00;
|
||||
i2cData[i++] = item;
|
||||
}
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, I2cAddr.Write, i2cData);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to write data to I2C FIFO: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to I2C FIFO returned false");
|
||||
return new(new Exception("Failed to write data to I2C FIFO"));
|
||||
}
|
||||
}
|
||||
|
||||
// 配置本次传输数据量
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, I2cAddr.TranConfig, ((uint)(data.Length - 1)));
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to configure transfer length: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to TranConfig returned false");
|
||||
return new(new Exception("Failed to configure transfer length"));
|
||||
}
|
||||
}
|
||||
|
||||
// 配置I2C地址、协议及启动传输
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, this.taskID, I2cAddr.BaseConfig, (devAddr) | (((uint)proto) << 16) | (1 << 24));
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to configure I2C address/protocol/start: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to BaseConfig returned false");
|
||||
return new(new Exception("Failed to configure I2C address/protocol/start"));
|
||||
}
|
||||
}
|
||||
|
||||
// 等待I2C命令完成
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, this.taskID, I2cAddr.Flag, 0x0000_0001, 0xFFFF_FFFF, 10);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to wait for I2C command completion: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("ReadAddrWithWait for I2C command completion returned false");
|
||||
return new(new Exception("I2C command did not complete successfully"));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从指定I2C设备读取数据
|
||||
/// </summary>
|
||||
/// <param name="devAddr">I2C设备地址</param>
|
||||
/// <param name="data">要写入的数据(dummy数据)</param>
|
||||
/// <param name="dataReadLength">要读取的数据长度</param>
|
||||
/// <param name="proto">I2C协议类型</param>
|
||||
/// <returns>操作结果,成功返回读取到的数据,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<byte[]>> ReadData(UInt32 devAddr, byte[] data, int dataReadLength, I2cProtocol proto)
|
||||
{
|
||||
if (dataReadLength < 1 || dataReadLength > 0x0000_FFFF)
|
||||
{
|
||||
logger.Error($"Read length {dataReadLength} is invalid or exceeds maximum allowed 0x0000_FFFF");
|
||||
return new(new ArgumentException($"Read length {dataReadLength} is invalid or exceeds maximum allowed 0x0000_FFFF"));
|
||||
}
|
||||
|
||||
if (data.Length > 0x0000_FFFF)
|
||||
{
|
||||
logger.Error($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF");
|
||||
return new(new ArgumentException($"Data length {data.Length} exceeds maximum allowed 0x0000_FFFF"));
|
||||
}
|
||||
|
||||
// 清除UDP服务器接收缓冲区
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
|
||||
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||
|
||||
// 配置写FIFO内容,内容为data[]
|
||||
{
|
||||
var i2cData = new byte[data.Length * 4];
|
||||
int i = 0;
|
||||
foreach (var item in data)
|
||||
{
|
||||
i2cData[i++] = 0x00;
|
||||
i2cData[i++] = 0x00;
|
||||
i2cData[i++] = 0x00;
|
||||
i2cData[i++] = item;
|
||||
}
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, I2cAddr.Write, i2cData);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to write data to I2C FIFO: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to I2C FIFO returned false");
|
||||
return new(new Exception("Failed to write data to I2C FIFO"));
|
||||
}
|
||||
}
|
||||
|
||||
// 配置本次传输数据量:[15:0]为读长度(length-1),[31:16]为dummy长度(data.Length-1)
|
||||
{
|
||||
uint tranConfig = ((uint)(dataReadLength - 1)) | (((uint)(data.Length - 1)) << 16);
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, I2cAddr.TranConfig, tranConfig);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to configure transfer length: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to TranConfig returned false");
|
||||
return new(new Exception("Failed to configure transfer length"));
|
||||
}
|
||||
}
|
||||
|
||||
// 配置I2C地址、协议及启动传输(读操作)
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, this.taskID, I2cAddr.BaseConfig, (devAddr) | (1 << 8) | (((uint)proto) << 16) | (1 << 24));
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to configure I2C address/protocol/start: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to BaseConfig returned false");
|
||||
return new(new Exception("Failed to configure I2C address/protocol/start"));
|
||||
}
|
||||
}
|
||||
|
||||
// 等待I2C命令完成
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, this.taskID, I2cAddr.Flag, 0x0000_0001, 0xFFFF_FFFF, 10);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to wait for I2C command completion: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("ReadAddrWithWait for I2C command completion returned false");
|
||||
return new(new Exception("I2C command did not complete successfully"));
|
||||
}
|
||||
}
|
||||
|
||||
// 读取数据
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, I2cAddr.Read);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read data from I2C FIFO: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (ret.Value.Options.Data == null)
|
||||
{
|
||||
logger.Error($"ReadAddr returned unexpected data length: {ret.Value.Options.Data?.Length ?? 0}");
|
||||
return new(new Exception("Failed to read expected amount of data from I2C FIFO"));
|
||||
}
|
||||
|
||||
return ret.Value.Options.Data[3..]; // 返回读取到的数据,跳过前3个字节
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Collections;
|
||||
using System.Net;
|
||||
using BsdlParser;
|
||||
using DotNext;
|
||||
using Newtonsoft.Json;
|
||||
using server;
|
||||
using WebProtocol;
|
||||
|
||||
namespace JtagClient;
|
||||
namespace Peripherals.JtagClient;
|
||||
|
||||
/// <summary>
|
||||
/// Global Constant Jtag Address
|
||||
@@ -406,15 +406,17 @@ public class Jtag
|
||||
async ValueTask<Result<uint>> ReadFIFO(uint devAddr)
|
||||
{
|
||||
var ret = false;
|
||||
var opts = new SendAddrPackOptions();
|
||||
var opts = new SendAddrPackOptions()
|
||||
{
|
||||
BurstType = BurstType.FixedBurst,
|
||||
BurstLength = 0,
|
||||
CommandID = 0,
|
||||
Address = devAddr,
|
||||
IsWrite = false,
|
||||
};
|
||||
|
||||
opts.BurstType = BurstType.FixedBurst;
|
||||
opts.BurstLength = 0;
|
||||
opts.CommandID = 0;
|
||||
opts.Address = devAddr;
|
||||
|
||||
// Read Jtag State Register
|
||||
opts.IsWrite = false;
|
||||
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
|
||||
if (!ret) return new(new Exception("Send Address Package Failed!"));
|
||||
|
||||
@@ -422,7 +424,7 @@ public class Jtag
|
||||
if (!MsgBus.IsRunning)
|
||||
return new(new Exception("Message Bus not Working!"));
|
||||
|
||||
var retPack = await MsgBus.UDPServer.WaitForDataAsync(address, port);
|
||||
var retPack = await MsgBus.UDPServer.WaitForDataAsync(address, 0, port);
|
||||
if (!retPack.IsSuccessful || !retPack.Value.IsSuccessful)
|
||||
return new(new Exception("Send address package failed"));
|
||||
|
||||
@@ -441,7 +443,7 @@ public class Jtag
|
||||
(UInt32 devAddr, UInt32 data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
|
||||
{
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, devAddr, data, this.timeout);
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
if (!ret.Value) return new(new Exception("Write FIFO failed"));
|
||||
}
|
||||
@@ -450,7 +452,7 @@ public class Jtag
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds));
|
||||
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, JtagAddr.STATE, result, resultMask, this.timeout);
|
||||
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
@@ -460,7 +462,7 @@ public class Jtag
|
||||
(UInt32 devAddr, byte[] data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0)
|
||||
{
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, devAddr, data, this.timeout);
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
if (!ret.Value) return new(new Exception("Write FIFO failed"));
|
||||
}
|
||||
@@ -469,7 +471,7 @@ public class Jtag
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds));
|
||||
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, JtagAddr.STATE, result, resultMask, this.timeout);
|
||||
var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
@@ -627,9 +629,9 @@ public class Jtag
|
||||
public async ValueTask<Result<uint>> ReadIDCode()
|
||||
{
|
||||
// Clear Data
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 0);
|
||||
|
||||
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||
logger.Trace($"Clear up udp server {this.address,0} receive data");
|
||||
|
||||
Result<bool> ret;
|
||||
|
||||
@@ -665,9 +667,9 @@ public class Jtag
|
||||
public async ValueTask<Result<uint>> ReadStatusReg()
|
||||
{
|
||||
// Clear Data
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 0);
|
||||
|
||||
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||
logger.Trace($"Clear up udp server {this.address,0} receive data");
|
||||
|
||||
Result<bool> ret;
|
||||
|
||||
@@ -702,9 +704,9 @@ public class Jtag
|
||||
public async ValueTask<Result<bool>> DownloadBitstream(byte[] bitstream)
|
||||
{
|
||||
// Clear Data
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 0);
|
||||
|
||||
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||
logger.Trace($"Clear up udp server {this.address,0} receive data");
|
||||
|
||||
Result<bool> ret;
|
||||
|
||||
@@ -786,9 +788,9 @@ public class Jtag
|
||||
logger.Debug($"Get boundar scan registers number: {portNum}");
|
||||
|
||||
// Clear Data
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 0);
|
||||
|
||||
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||
logger.Trace($"Clear up udp server {this.address,0} receive data");
|
||||
|
||||
Result<bool> ret;
|
||||
|
||||
@@ -853,9 +855,9 @@ public class Jtag
|
||||
public async ValueTask<Result<bool>> SetSpeed(UInt32 speed)
|
||||
{
|
||||
// Clear Data
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 0);
|
||||
|
||||
logger.Trace($"Clear up udp server {this.address} receive data");
|
||||
logger.Trace($"Clear up udp server {this.address,0} receive data");
|
||||
|
||||
var ret = await WriteFIFO(
|
||||
JtagAddr.SPEED_CTRL, (speed << 16) | speed,
|
||||
494
server/src/Peripherals/LogicAnalyzerClient.cs
Normal file
494
server/src/Peripherals/LogicAnalyzerClient.cs
Normal file
@@ -0,0 +1,494 @@
|
||||
using System.Collections;
|
||||
using System.Net;
|
||||
using Common;
|
||||
using DotNext;
|
||||
|
||||
namespace Peripherals.LogicAnalyzerClient;
|
||||
|
||||
static class AnalyzerAddr
|
||||
{
|
||||
const UInt32 BASE = 0x9000_0000;
|
||||
const UInt32 DMA1_BASE = 0x7000_0000;
|
||||
const UInt32 DDR_BASE = 0x0000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0000 R/W [ 0] capture on: 置1开始等待捕获,0停止捕获。捕获到信号后该位自动清零。 <br/>
|
||||
/// [ 8] capture force: 置1则强制捕获信号,自动置0。 <br/>
|
||||
/// [16] capture busy: 1为逻辑分析仪正在捕获信号。 <br/>
|
||||
/// [24] capture done: 1为逻辑分析仪内存完整存储了此次捕获的信号。 <br/>
|
||||
/// 配置顺序:若[0]为0,则将其置1,随后不断获取[0],若其变为0则表示触发成功。随后不断获取[24],若其为1则表示捕获完成。 <br/>
|
||||
/// </summary>
|
||||
public const UInt32 CAPTURE_MODE = BASE + 0x0000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0001 R/W [1:0] global trig mode: 00: 全局与 (&) <br/>
|
||||
/// 01: 全局或 (|) <br/>
|
||||
/// 10: 全局非与(~&) <br/>
|
||||
/// 11: 全局非或(~|) <br/>
|
||||
/// </summary>
|
||||
public const UInt32 GLOBAL_TRIG_MODE = BASE + 0x0000_0001;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0010 - 0x0000_0017 R/W [5:0] 信号M的触发操作符,共8路 <br/>
|
||||
/// [5:3] M's Operator: 000 == <br/>
|
||||
/// 001 != <br/>
|
||||
/// 010 < <br/>
|
||||
/// 011 <= <br/>
|
||||
/// 100 > <br/>
|
||||
/// 101 >= <br/>
|
||||
/// [2:0] M's Value: 000 LOGIC 0 <br/>
|
||||
/// 001 LOGIC 1 <br/>
|
||||
/// 010 X(not care) <br/>
|
||||
/// 011 RISE <br/>
|
||||
/// 100 FALL <br/>
|
||||
/// 101 RISE OR FALL <br/>
|
||||
/// 110 NOCHANGE <br/>
|
||||
/// 111 SOME NUMBER <br/>
|
||||
/// </summary>
|
||||
public static readonly UInt32[] SIGNAL_TRIG_MODE = {
|
||||
BASE + 0x0000_0010, BASE + 0x0000_0011,
|
||||
BASE + 0x0000_0012, BASE + 0x0000_0013,
|
||||
BASE + 0x0000_0014, BASE + 0x0000_0015,
|
||||
BASE + 0x0000_0016, BASE + 0x0000_0017,
|
||||
BASE + 0x0000_0018, BASE + 0x0000_0019,
|
||||
BASE + 0x0000_001A, BASE + 0x0000_001B,
|
||||
BASE + 0x0000_001C, BASE + 0x0000_001D,
|
||||
BASE + 0x0000_001E, BASE + 0x0000_001F,
|
||||
BASE + 0x0000_0020, BASE + 0x0000_0021,
|
||||
BASE + 0x0000_0022, BASE + 0x0000_0023,
|
||||
BASE + 0x0000_0024, BASE + 0x0000_0025,
|
||||
BASE + 0x0000_0026, BASE + 0x0000_0027,
|
||||
BASE + 0x0000_0028, BASE + 0x0000_0029,
|
||||
BASE + 0x0000_002A, BASE + 0x0000_002B,
|
||||
BASE + 0x0000_002C, BASE + 0x0000_002D,
|
||||
BASE + 0x0000_002E, BASE + 0x0000_002F
|
||||
};
|
||||
public const UInt32 LOAD_NUM_ADDR = BASE + 0x0000_0002;
|
||||
public const UInt32 PRE_LOAD_NUM_ADDR = BASE + 0x0000_0003;
|
||||
public const UInt32 CAHNNEL_DIV_ADDR = BASE + 0x0000_0004;
|
||||
public const UInt32 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>
|
||||
/// 0x0100_0000 - 0x0100_03FF 只读 32位波形存储,得到的32位数据中低八位最先捕获,高八位最后捕获。<br/>
|
||||
/// 共1024个地址,每个地址存储4组,深度为4096。<br/>
|
||||
/// </summary>
|
||||
public const Int32 CAPTURE_DATA_LENGTH = 1024;
|
||||
public const Int32 CAPTURE_DATA_PRELOAD = 512;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 逻辑分析仪运行状态枚举
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum CaptureStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 无状态标志
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 捕获使能位,置1开始等待捕获,0停止捕获。捕获到信号后该位自动清零
|
||||
/// </summary>
|
||||
CaptureOn = 1 << 0, // [0] 捕获使能
|
||||
|
||||
/// <summary>
|
||||
/// 强制捕获位,置1则强制捕获信号,自动置0
|
||||
/// </summary>
|
||||
CaptureForce = 1 << 8, // [8] 强制捕获
|
||||
|
||||
/// <summary>
|
||||
/// 捕获忙碌位,1为逻辑分析仪正在捕获信号
|
||||
/// </summary>
|
||||
CaptureBusy = 1 << 16, // [16] 捕获进行中
|
||||
|
||||
/// <summary>
|
||||
/// 捕获完成位,1为逻辑分析仪内存完整存储了此次捕获的信号
|
||||
/// </summary>
|
||||
CaptureDone = 1 << 24 // [24] 捕获完成
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局触发模式枚举,定义多路信号触发条件的逻辑组合方式
|
||||
/// </summary>
|
||||
public enum GlobalCaptureMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局与模式,所有触发条件都必须满足
|
||||
/// </summary>
|
||||
AND = 0b00,
|
||||
|
||||
/// <summary>
|
||||
/// 全局或模式,任一触发条件满足即可
|
||||
/// </summary>
|
||||
OR = 0b01,
|
||||
|
||||
/// <summary>
|
||||
/// 全局非与模式,不是所有触发条件都满足
|
||||
/// </summary>
|
||||
NAND = 0b10,
|
||||
|
||||
/// <summary>
|
||||
/// 全局非或模式,所有触发条件都不满足
|
||||
/// </summary>
|
||||
NOR = 0b11
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 信号M的操作符枚举
|
||||
/// </summary>
|
||||
public enum SignalOperator : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// 等于操作符
|
||||
/// </summary>
|
||||
Equal = 0b000, // ==
|
||||
/// <summary>
|
||||
/// 不等于操作符
|
||||
/// </summary>
|
||||
NotEqual = 0b001, // !=
|
||||
/// <summary>
|
||||
/// 小于操作符
|
||||
/// </summary>
|
||||
LessThan = 0b010, // <
|
||||
/// <summary>
|
||||
/// 小于等于操作符
|
||||
/// </summary>
|
||||
LessThanOrEqual = 0b011, // <=
|
||||
/// <summary>
|
||||
/// 大于操作符
|
||||
/// </summary>
|
||||
GreaterThan = 0b100, // >
|
||||
/// <summary>
|
||||
/// 大于等于操作符
|
||||
/// </summary>
|
||||
GreaterThanOrEqual = 0b101 // >=
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 信号M的值枚举
|
||||
/// </summary>
|
||||
public enum SignalValue : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// 逻辑0电平
|
||||
/// </summary>
|
||||
Logic0 = 0b000, // LOGIC 0
|
||||
/// <summary>
|
||||
/// 逻辑1电平
|
||||
/// </summary>
|
||||
Logic1 = 0b001, // LOGIC 1
|
||||
/// <summary>
|
||||
/// 不关心该信号状态
|
||||
/// </summary>
|
||||
NotCare = 0b010, // X(not care)
|
||||
/// <summary>
|
||||
/// 上升沿触发
|
||||
/// </summary>
|
||||
Rise = 0b011, // RISE
|
||||
/// <summary>
|
||||
/// 下降沿触发
|
||||
/// </summary>
|
||||
Fall = 0b100, // FALL
|
||||
/// <summary>
|
||||
/// 上升沿或下降沿触发
|
||||
/// </summary>
|
||||
RiseOrFall = 0b101, // RISE OR FALL
|
||||
/// <summary>
|
||||
/// 信号无变化
|
||||
/// </summary>
|
||||
NoChange = 0b110, // NOCHANGE
|
||||
/// <summary>
|
||||
/// 特定数值
|
||||
/// </summary>
|
||||
SomeNumber = 0b111 // SOME NUMBER
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 逻辑分析仪有效通道数
|
||||
/// </summary>
|
||||
public enum AnalyzerChannelDiv
|
||||
{
|
||||
/// <summary>
|
||||
/// 1路
|
||||
/// </summary>
|
||||
ONE = 0x0000_0000,
|
||||
/// <summary>
|
||||
/// 2路
|
||||
/// </summary>
|
||||
TWO = 0x0000_0001,
|
||||
/// <summary>
|
||||
/// 4路
|
||||
/// </summary>
|
||||
FOUR = 0x0000_0002,
|
||||
/// <summary>
|
||||
/// 8路
|
||||
/// </summary>
|
||||
EIGHT = 0x0000_0003,
|
||||
/// <summary>
|
||||
/// 16路
|
||||
/// </summary>
|
||||
XVI = 0x0000_0004,
|
||||
/// <summary>
|
||||
/// 32路
|
||||
/// </summary>
|
||||
XXXII = 0x0000_0005
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FPGA逻辑分析仪客户端,用于控制FPGA上的逻辑分析仪模块进行信号捕获和分析
|
||||
/// </summary>
|
||||
public class Analyzer
|
||||
{
|
||||
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
readonly int timeout = 2000;
|
||||
readonly int taskID;
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
private IPEndPoint ep;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化逻辑分析仪客户端
|
||||
/// </summary>
|
||||
/// <param name="address">FPGA设备的IP地址</param>
|
||||
/// <param name="port">通信端口号</param>
|
||||
/// <param name="taskID">任务标识符</param>
|
||||
/// <param name="timeout">通信超时时间(毫秒),默认2000ms</param>
|
||||
/// <exception cref="ArgumentException">当timeout为负数时抛出</exception>
|
||||
public Analyzer(string address, int port, int taskID, int timeout = 2000)
|
||||
{
|
||||
if (timeout < 0)
|
||||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||||
this.address = address;
|
||||
this.taskID = taskID;
|
||||
this.port = port;
|
||||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 控制逻辑分析仪的捕获模式
|
||||
/// </summary>
|
||||
/// <param name="captureOn">是否开始捕获</param>
|
||||
/// <param name="force">是否强制捕获</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> SetCaptureMode(bool captureOn, bool force)
|
||||
{
|
||||
// 构造寄存器值
|
||||
UInt32 value = 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;
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, value, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set capture mode: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to CAPTURE_MODE returned false");
|
||||
return new(new Exception("Failed to set capture mode"));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取逻辑分析仪捕获运行状态
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回寄存器值,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<CaptureStatus>> ReadCaptureStatus()
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, AnalyzerAddr.CAPTURE_MODE, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read capture status: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 4)
|
||||
{
|
||||
logger.Error("ReadAddr returned invalid data for capture status");
|
||||
return new(new Exception("Failed to read capture status"));
|
||||
}
|
||||
UInt32 status = Number.BytesToUInt32(ret.Value.Options.Data).Value;
|
||||
return (CaptureStatus)status;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置全局触发模式
|
||||
/// </summary>
|
||||
/// <param name="mode">全局触发模式(0:与, 1:或, 2:非与, 3:非或)</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> SetGlobalTrigMode(GlobalCaptureMode mode)
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, this.taskID, AnalyzerAddr.GLOBAL_TRIG_MODE, (byte)mode, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set global trigger mode: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to GLOBAL_TRIG_MODE returned false");
|
||||
return new(new Exception("Failed to set global trigger mode"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定信号通道的触发模式
|
||||
/// </summary>
|
||||
/// <param name="signalIndex">信号通道索引(0-7)</param>
|
||||
/// <param name="op">触发操作符</param>
|
||||
/// <param name="val">触发信号值</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> SetSignalTrigMode(int signalIndex, SignalOperator op, SignalValue val)
|
||||
{
|
||||
if (signalIndex < 0 || signalIndex >= AnalyzerAddr.SIGNAL_TRIG_MODE.Length)
|
||||
return new(new ArgumentException($"Signal index must be 0~{AnalyzerAddr.SIGNAL_TRIG_MODE.Length}"));
|
||||
|
||||
// 计算模式值: [2:0] 信号值, [5:3] 操作符
|
||||
UInt32 mode = ((UInt32)op << 3) | (UInt32)val;
|
||||
|
||||
var addr = AnalyzerAddr.SIGNAL_TRIG_MODE[signalIndex];
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, addr, mode, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set signal trigger mode: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to SIGNAL_TRIG_MODE returned false");
|
||||
return new(new Exception("Failed to set signal trigger mode"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置逻辑分析仪的深度、预采样深度、有效通道
|
||||
/// </summary>
|
||||
/// <param name="capture_length">深度</param>
|
||||
/// <param name="pre_capture_length">预采样深度</param>
|
||||
/// <param name="channel_div">有效通道(0-[1],1-[2],2-[4],3-[8],4-[16],5-[32])</param>
|
||||
/// <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>
|
||||
/// <returns>操作结果,成功返回byte[],否则返回异常信息</returns>
|
||||
public async ValueTask<Result<byte[]>> ReadCaptureData(int capture_length = 2048 * 32)
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr4BytesAsync(
|
||||
this.ep,
|
||||
this.taskID,
|
||||
AnalyzerAddr.STORE_OFFSET_ADDR,
|
||||
capture_length,
|
||||
this.timeout
|
||||
);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read capture data: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
var data = ret.Value;
|
||||
if (data == null || data.Length != capture_length * 4)
|
||||
{
|
||||
logger.Error($"Capture data length mismatch: {data?.Length}");
|
||||
return new(new Exception("Capture data length mismatch"));
|
||||
}
|
||||
var reversed = Common.Number.ReverseBytes(data, 4).Value;
|
||||
return reversed;
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,10 @@ public class MatrixKey
|
||||
public async ValueTask<Result<bool>> EnableControl()
|
||||
{
|
||||
if (MsgBus.IsRunning)
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 1);
|
||||
else return new(new Exception("Message Bus not work!"));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, MatrixKeyAddr.KEY_ENABLE, 1, this.timeout);
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, 1, MatrixKeyAddr.KEY_ENABLE, 1, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
@@ -59,10 +59,10 @@ public class MatrixKey
|
||||
public async ValueTask<Result<bool>> DisableControl()
|
||||
{
|
||||
if (MsgBus.IsRunning)
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 1);
|
||||
else return new(new Exception("Message Bus not work!"));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, MatrixKeyAddr.KEY_ENABLE, 0, this.timeout);
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, 1, MatrixKeyAddr.KEY_ENABLE, 0, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
@@ -75,14 +75,14 @@ public class MatrixKey
|
||||
public async ValueTask<Result<bool>> ControlKey(BitArray keyStates)
|
||||
{
|
||||
if (MsgBus.IsRunning)
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 1);
|
||||
else return new(new Exception("Message Bus not work!"));
|
||||
|
||||
if (keyStates.Length != 16) return new(new ArgumentException(
|
||||
$"The number of key should be 16 instead of {keyStates.Length}", nameof(keyStates)));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, MatrixKeyAddr.KEY_CTRL, Common.Number.BitsToNumber(keyStates).Value, this.timeout);
|
||||
this.ep, 1, MatrixKeyAddr.KEY_CTRL, Common.Number.BitsToNumber(keyStates).Value, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
|
||||
392
server/src/Peripherals/NetConfigClient.cs
Normal file
392
server/src/Peripherals/NetConfigClient.cs
Normal file
@@ -0,0 +1,392 @@
|
||||
using System.Net;
|
||||
using DotNext;
|
||||
|
||||
namespace Peripherals.NetConfigClient;
|
||||
|
||||
static class NetConfigAddr
|
||||
{
|
||||
const UInt32 BASE = 0x30A7_0000;
|
||||
|
||||
public static readonly UInt32[] HOST_IP = { BASE + 0, BASE + 1, BASE + 2, BASE + 3 };
|
||||
public static readonly UInt32[] BOARD_IP = { BASE + 4, BASE + 5, BASE + 6, BASE + 7 };
|
||||
public static readonly UInt32[] HOST_MAC = { BASE + 8, BASE + 9, BASE + 10, BASE + 11, BASE + 12, BASE + 13 };
|
||||
public static readonly UInt32[] BOARD_MAC = { BASE + 14, BASE + 15, BASE + 16, BASE + 17, BASE + 18, BASE + 19 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Network configuration client for FPGA board communication
|
||||
/// </summary>
|
||||
public class NetConfig
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
readonly int timeout = 2000;
|
||||
readonly int taskID;
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
private IPEndPoint ep;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize NetConfig client
|
||||
/// </summary>
|
||||
/// <param name="address">Target board address</param>
|
||||
/// <param name="port">Target board port</param>
|
||||
/// <param name="taskID">Task identifier</param>
|
||||
/// <param name="timeout">Timeout in milliseconds</param>
|
||||
public NetConfig(string address, int port, int taskID, int timeout = 2000)
|
||||
{
|
||||
if (timeout < 0)
|
||||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||||
this.address = address;
|
||||
this.taskID = taskID;
|
||||
this.port = port;
|
||||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set host IP address
|
||||
/// </summary>
|
||||
/// <param name="ip">IP address to set</param>
|
||||
/// <returns>Result indicating success or failure</returns>
|
||||
public async ValueTask<Result<bool>> SetHostIP(IPAddress ip)
|
||||
{
|
||||
// 清除UDP服务器接收缓冲区
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
|
||||
// 刷新ARP
|
||||
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
|
||||
// if (!refrshRet)
|
||||
// {
|
||||
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
|
||||
// }
|
||||
|
||||
var ipBytes = ip.GetAddressBytes();
|
||||
|
||||
var ret = await UDPClientPool.WriteAddrSeq(this.ep, this.taskID, NetConfigAddr.HOST_IP, ipBytes, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set host IP: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error($"Failed to set host IP: operation returned false");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证设置结果
|
||||
var verifyResult = await GetHostIP();
|
||||
if (!verifyResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to verify host IP after setting: {verifyResult.Error}");
|
||||
return new(verifyResult.Error);
|
||||
}
|
||||
|
||||
var expectedIP = ip.ToString();
|
||||
if (verifyResult.Value != expectedIP)
|
||||
{
|
||||
logger.Error($"Host IP verification failed: expected {expectedIP}, got {verifyResult.Value}");
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.Info($"Successfully set and verified host IP: {expectedIP}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set board IP address
|
||||
/// </summary>
|
||||
/// <param name="ip">IP address to set</param>
|
||||
/// <returns>Result indicating success or failure</returns>
|
||||
public async ValueTask<Result<bool>> SetBoardIP(IPAddress ip)
|
||||
{
|
||||
// 清除UDP服务器接收缓冲区
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
|
||||
// 刷新ARP
|
||||
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
|
||||
// if (!refrshRet)
|
||||
// {
|
||||
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
|
||||
// }
|
||||
|
||||
var ipBytes = ip.GetAddressBytes();
|
||||
|
||||
var ret = await UDPClientPool.WriteAddrSeq(this.ep, this.taskID, NetConfigAddr.BOARD_IP, ipBytes, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set board IP: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error($"Failed to set board IP: operation returned false");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证设置结果
|
||||
var verifyResult = await GetBoardIP();
|
||||
if (!verifyResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to verify board IP after setting: {verifyResult.Error}");
|
||||
return new(verifyResult.Error);
|
||||
}
|
||||
|
||||
var expectedIP = ip.ToString();
|
||||
if (verifyResult.Value != expectedIP)
|
||||
{
|
||||
logger.Error($"Board IP verification failed: expected {expectedIP}, got {verifyResult.Value}");
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.Info($"Successfully set and verified board IP: {expectedIP}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set host MAC address
|
||||
/// </summary>
|
||||
/// <param name="macAddress">MAC address bytes (6 bytes)</param>
|
||||
/// <returns>Result indicating success or failure</returns>
|
||||
public async ValueTask<Result<bool>> SetHostMAC(byte[] macAddress)
|
||||
{
|
||||
if (macAddress == null)
|
||||
throw new ArgumentNullException(nameof(macAddress));
|
||||
if (macAddress.Length != 6)
|
||||
throw new ArgumentException("MAC address must be 6 bytes", nameof(macAddress));
|
||||
|
||||
// 清除UDP服务器接收缓冲区
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
|
||||
// 刷新ARP
|
||||
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
|
||||
// if (!refrshRet)
|
||||
// {
|
||||
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
|
||||
// }
|
||||
|
||||
var ret = await UDPClientPool.WriteAddrSeq(this.ep, this.taskID, NetConfigAddr.HOST_MAC, macAddress, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set host MAC address: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error($"Failed to set host MAC address: operation returned false");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证设置结果
|
||||
var verifyResult = await GetHostMAC();
|
||||
if (!verifyResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to verify host MAC after setting: {verifyResult.Error}");
|
||||
return new(verifyResult.Error);
|
||||
}
|
||||
|
||||
var expectedMAC = string.Join(":", macAddress.Select(b => $"{b:X2}"));
|
||||
if (verifyResult.Value != expectedMAC)
|
||||
{
|
||||
logger.Error($"Host MAC verification failed: expected {expectedMAC}, got {verifyResult.Value}");
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.Info($"Successfully set and verified host MAC: {expectedMAC}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set board MAC address
|
||||
/// </summary>
|
||||
/// <param name="macAddress">MAC address bytes (6 bytes)</param>
|
||||
/// <returns>Result indicating success or failure</returns>
|
||||
public async ValueTask<Result<bool>> SetBoardMAC(byte[] macAddress)
|
||||
{
|
||||
if (macAddress == null)
|
||||
throw new ArgumentNullException(nameof(macAddress));
|
||||
if (macAddress.Length != 6)
|
||||
throw new ArgumentException("MAC address must be 6 bytes", nameof(macAddress));
|
||||
|
||||
// 清除UDP服务器接收缓冲区
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
|
||||
// 刷新ARP
|
||||
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
|
||||
// if (!refrshRet)
|
||||
// {
|
||||
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
|
||||
// }
|
||||
|
||||
var ret = await UDPClientPool.WriteAddrSeq(this.ep, this.taskID, NetConfigAddr.BOARD_MAC, macAddress, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set board MAC address: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error($"Failed to set board MAC address: operation returned false");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证设置结果
|
||||
var verifyResult = await GetBoardMAC();
|
||||
if (!verifyResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to verify board MAC after setting: {verifyResult.Error}");
|
||||
return new(verifyResult.Error);
|
||||
}
|
||||
|
||||
var expectedMAC = string.Join(":", macAddress.Select(b => $"{b:X2}"));
|
||||
if (verifyResult.Value != expectedMAC)
|
||||
{
|
||||
logger.Error($"Board MAC verification failed: expected {expectedMAC}, got {verifyResult.Value}");
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.Info($"Successfully set and verified board MAC: {expectedMAC}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get host IP address
|
||||
/// </summary>
|
||||
/// <returns>Host IP address as string</returns>
|
||||
public async ValueTask<Result<string>> GetHostIP()
|
||||
{
|
||||
// 清除UDP服务器接收缓冲区
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
|
||||
// 刷新ARP
|
||||
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
|
||||
// if (!refrshRet)
|
||||
// {
|
||||
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
|
||||
// }
|
||||
|
||||
var ret = await UDPClientPool.ReadAddrSeq(this.ep, this.taskID, NetConfigAddr.HOST_IP, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to get host IP: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
var ip = "";
|
||||
for (int i = 0; i < NetConfigAddr.HOST_IP.Length; i++)
|
||||
{
|
||||
ip += $"{ret.Value[i * 4 + 3]}";
|
||||
if (i != NetConfigAddr.HOST_IP.Length - 1)
|
||||
ip += ".";
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get board IP address
|
||||
/// </summary>
|
||||
/// <returns>Board IP address as string</returns>
|
||||
public async ValueTask<Result<string>> GetBoardIP()
|
||||
{
|
||||
// 清除UDP服务器接收缓冲区
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
|
||||
// 刷新ARP
|
||||
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
|
||||
// if (!refrshRet)
|
||||
// {
|
||||
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
|
||||
// }
|
||||
|
||||
var ret = await UDPClientPool.ReadAddrSeq(this.ep, this.taskID, NetConfigAddr.BOARD_IP, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to get board IP: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
var ip = "";
|
||||
for (int i = 0; i < NetConfigAddr.BOARD_IP.Length; i++)
|
||||
{
|
||||
ip += $"{ret.Value[i * 4 + 3]}";
|
||||
if (i != NetConfigAddr.BOARD_IP.Length - 1)
|
||||
ip += ".";
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get host MAC address
|
||||
/// </summary>
|
||||
/// <returns>Host MAC address as formatted string (XX:XX:XX:XX:XX:XX)</returns>
|
||||
public async ValueTask<Result<string>> GetHostMAC()
|
||||
{
|
||||
// 清除UDP服务器接收缓冲区
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
|
||||
// 刷新ARP
|
||||
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
|
||||
// if (!refrshRet)
|
||||
// {
|
||||
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
|
||||
// }
|
||||
|
||||
var ret = await UDPClientPool.ReadAddrSeq(this.ep, this.taskID, NetConfigAddr.HOST_MAC, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to get host MAC address: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
var mac = "";
|
||||
for (int i = 0; i < NetConfigAddr.HOST_MAC.Length; i++)
|
||||
{
|
||||
mac += $"{ret.Value[i * 4 + 3]:X2}";
|
||||
if (i != NetConfigAddr.HOST_MAC.Length - 1)
|
||||
mac += ":";
|
||||
}
|
||||
|
||||
return mac;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get board MAC address
|
||||
/// </summary>
|
||||
/// <returns>Board MAC address as formatted string (XX:XX:XX:XX:XX:XX)</returns>
|
||||
public async ValueTask<Result<string>> GetBoardMAC()
|
||||
{
|
||||
// 清除UDP服务器接收缓冲区
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
|
||||
// 刷新ARP
|
||||
// var refrshRet = await Arp.UpdateArpEntryByPingAsync(this.address);
|
||||
// if (!refrshRet)
|
||||
// {
|
||||
// logger.Warn($"Refrash Arp failed, but maybe not a big deal.");
|
||||
// }
|
||||
|
||||
var ret = await UDPClientPool.ReadAddrSeq(this.ep, this.taskID, NetConfigAddr.BOARD_MAC, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to get board MAC address: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
|
||||
var mac = "";
|
||||
for (int i = 0; i < NetConfigAddr.BOARD_MAC.Length; i++)
|
||||
{
|
||||
mac += $"{ret.Value[i * 4 + 3]:X2}";
|
||||
if (i != NetConfigAddr.BOARD_MAC.Length - 1)
|
||||
mac += ":";
|
||||
}
|
||||
|
||||
return mac;
|
||||
}
|
||||
}
|
||||
348
server/src/Peripherals/OscilloscopeClient.cs
Normal file
348
server/src/Peripherals/OscilloscopeClient.cs
Normal file
@@ -0,0 +1,348 @@
|
||||
using System.Net;
|
||||
using Common;
|
||||
using DotNext;
|
||||
|
||||
namespace Peripherals.OscilloscopeClient;
|
||||
|
||||
static class OscilloscopeAddr
|
||||
{
|
||||
const UInt32 BASE = 0x8000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0000:R/W[0] wave_run 启动捕获/关闭
|
||||
/// </summary>
|
||||
public const UInt32 START_CAPTURE = BASE + 0x0000_0000;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0001: R/W[7:0] trig_level 触发电平
|
||||
/// </summary>
|
||||
public const UInt32 TRIG_LEVEL = BASE + 0x0000_0001;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0002:R/W[0] trig_edge 触发边沿,0-下降沿,1-上升沿
|
||||
/// </summary>
|
||||
public const UInt32 TRIG_EDGE = BASE + 0x0000_0002;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0003: R/W[9:0] h shift 水平偏移量
|
||||
/// </summary>
|
||||
public const UInt32 H_SHIFT = BASE + 0x0000_0003;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0004: R/W[9:0] deci rate 抽样率,0—1023
|
||||
/// </summary>
|
||||
public const UInt32 DECI_RATE = BASE + 0x0000_0004;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0005:R/W[0] ram refresh RAM刷新
|
||||
/// </summary>
|
||||
public const UInt32 RAM_FRESH = BASE + 0x0000_0005;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000 0006:R[19: 0] ad_freq AD采样频率
|
||||
/// </summary>
|
||||
public const UInt32 AD_FREQ = BASE + 0x0000_0006;
|
||||
|
||||
/// <summary>
|
||||
/// Ox0000_0007: R[7:0] ad_vpp AD采样幅度
|
||||
/// </summary>
|
||||
public const UInt32 AD_VPP = BASE + 0x0000_0007;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0008: R[7:0] ad max AD采样最大值
|
||||
/// </summary>
|
||||
public const UInt32 AD_MAX = BASE + 0x0000_0008;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_0009: R[7:0] ad_min AD采样最小值
|
||||
/// </summary>
|
||||
public const UInt32 AD_MIN = BASE + 0x0000_0009;
|
||||
|
||||
/// <summary>
|
||||
/// 0x0000_1000-0x0000_13FF:R[7:0] wave_rd_data 共1024个字节
|
||||
/// </summary>
|
||||
public const UInt32 RD_DATA_ADDR = BASE + 0x0000_1000;
|
||||
public const UInt32 RD_DATA_LENGTH = 0x0000_0400;
|
||||
}
|
||||
|
||||
class Oscilloscope
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
readonly int timeout = 2000;
|
||||
readonly int taskID = 0;
|
||||
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
private IPEndPoint ep;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化示波器客户端
|
||||
/// </summary>
|
||||
/// <param name="address">示波器设备IP地址</param>
|
||||
/// <param name="port">示波器设备端口</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
public Oscilloscope(string address, int port, int timeout = 2000)
|
||||
{
|
||||
if (timeout < 0)
|
||||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 控制示波器的捕获开关
|
||||
/// </summary>
|
||||
/// <param name="enable">是否启动捕获</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> SetCaptureEnable(bool enable)
|
||||
{
|
||||
UInt32 value = enable ? 1u : 0u;
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, OscilloscopeAddr.START_CAPTURE, value, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set capture enable: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to START_CAPTURE returned false");
|
||||
return new(new Exception("Failed to set capture enable"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置触发电平
|
||||
/// </summary>
|
||||
/// <param name="level">触发电平值(0-255)</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> SetTriggerLevel(byte level)
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, OscilloscopeAddr.TRIG_LEVEL, level, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set trigger level: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to TRIG_LEVEL returned false");
|
||||
return new(new Exception("Failed to set trigger level"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置触发边沿
|
||||
/// </summary>
|
||||
/// <param name="risingEdge">true为上升沿,false为下降沿</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> SetTriggerEdge(bool risingEdge)
|
||||
{
|
||||
UInt32 value = risingEdge ? 1u : 0u;
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, OscilloscopeAddr.TRIG_EDGE, value, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set trigger edge: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to TRIG_EDGE returned false");
|
||||
return new(new Exception("Failed to set trigger edge"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置水平偏移量
|
||||
/// </summary>
|
||||
/// <param name="shift">水平偏移量值(0-1023)</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> SetHorizontalShift(UInt16 shift)
|
||||
{
|
||||
if (shift > 1023)
|
||||
return new(new ArgumentException("Horizontal shift must be 0-1023", nameof(shift)));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, OscilloscopeAddr.H_SHIFT, shift, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set horizontal shift: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to H_SHIFT returned false");
|
||||
return new(new Exception("Failed to set horizontal shift"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置抽样率
|
||||
/// </summary>
|
||||
/// <param name="rate">抽样率值(0-1023)</param>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> SetDecimationRate(UInt16 rate)
|
||||
{
|
||||
if (rate > 1023)
|
||||
return new(new ArgumentException("Decimation rate must be 0-1023", nameof(rate)));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, OscilloscopeAddr.DECI_RATE, rate, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set decimation rate: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to DECI_RATE returned false");
|
||||
return new(new Exception("Failed to set decimation rate"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新RAM
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回true,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<bool>> RefreshRAM()
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, OscilloscopeAddr.RAM_FRESH, 1u, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to refresh RAM: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error("WriteAddr to RAM_FRESH returned false");
|
||||
return new(new Exception("Failed to refresh RAM"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取AD采样频率
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回采样频率值,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<UInt32>> GetADFrequency()
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_FREQ, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read AD frequency: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 4)
|
||||
{
|
||||
logger.Error("ReadAddr returned invalid data for AD frequency");
|
||||
return new(new Exception("Failed to read AD frequency"));
|
||||
}
|
||||
UInt32 freq = Number.BytesToUInt32(ret.Value.Options.Data).Value;
|
||||
// 取低20位 [19:0]
|
||||
freq &= 0xFFFFF;
|
||||
return freq;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取AD采样幅度
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回采样幅度值,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<byte>> GetADVpp()
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_VPP, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read AD VPP: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 1)
|
||||
{
|
||||
logger.Error("ReadAddr returned invalid data for AD VPP");
|
||||
return new(new Exception("Failed to read AD VPP"));
|
||||
}
|
||||
return ret.Value.Options.Data[3];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取AD采样最大值
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回采样最大值,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<byte>> GetADMax()
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_MAX, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read AD max: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 1)
|
||||
{
|
||||
logger.Error("ReadAddr returned invalid data for AD max");
|
||||
return new(new Exception("Failed to read AD max"));
|
||||
}
|
||||
return ret.Value.Options.Data[3];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取AD采样最小值
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回采样最小值,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<byte>> GetADMin()
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, this.taskID, OscilloscopeAddr.AD_MIN, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read AD min: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (ret.Value.Options.Data == null || ret.Value.Options.Data.Length < 1)
|
||||
{
|
||||
logger.Error("ReadAddr returned invalid data for AD min");
|
||||
return new(new Exception("Failed to read AD min"));
|
||||
}
|
||||
return ret.Value.Options.Data[3];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取波形采样数据
|
||||
/// </summary>
|
||||
/// <returns>操作结果,成功返回采样数据数组,否则返回异常信息</returns>
|
||||
public async ValueTask<Result<byte[]>> GetWaveformData()
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr4BytesAsync(
|
||||
this.ep,
|
||||
this.taskID,
|
||||
OscilloscopeAddr.RD_DATA_ADDR,
|
||||
(int)OscilloscopeAddr.RD_DATA_LENGTH / 32,
|
||||
this.timeout
|
||||
);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to read waveform data: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
var data = ret.Value;
|
||||
if (data == null || data.Length != OscilloscopeAddr.RD_DATA_LENGTH / 8)
|
||||
{
|
||||
logger.Error($"Waveform data length mismatch: {data?.Length}");
|
||||
return new(new Exception("Waveform data length mismatch"));
|
||||
}
|
||||
|
||||
// 处理波形数据:从每4个字节中提取第4个字节(索引3)作为有效数据
|
||||
// 数据格式:低八位有效,即[4*i + 3]才是有效数据
|
||||
int sampleCount = data.Length / 4;
|
||||
byte[] waveformData = new byte[sampleCount];
|
||||
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
{
|
||||
waveformData[i] = data[4 * i + 3];
|
||||
}
|
||||
|
||||
return waveformData;
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,10 @@ public class Power
|
||||
public async ValueTask<Result<bool>> SetPowerOnOff(bool enable)
|
||||
{
|
||||
if (MsgBus.IsRunning)
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 1);
|
||||
else return new(new Exception("Message Bus not work!"));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, PowerAddr.PowerCtrl, Convert.ToUInt32(enable), this.timeout);
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, 1, PowerAddr.PowerCtrl, Convert.ToUInt32(enable), this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net;
|
||||
using DotNext;
|
||||
namespace RemoteUpdateClient;
|
||||
|
||||
namespace Peripherals.RemoteUpdateClient;
|
||||
|
||||
static class RemoteUpdaterAddr
|
||||
{
|
||||
@@ -98,7 +99,7 @@ public class RemoteUpdater
|
||||
const int FLASH_SECTOR_LENGTH = 4 * 1024;
|
||||
|
||||
readonly int timeout = 2000;
|
||||
readonly int timeoutForWait = 60 * 1000;
|
||||
readonly int timeoutForWait = 20 * 1000;
|
||||
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
@@ -142,7 +143,7 @@ public class RemoteUpdater
|
||||
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, RemoteUpdaterAddr.WriteCtrl,
|
||||
this.ep, 0, RemoteUpdaterAddr.WriteCtrl,
|
||||
Convert.ToUInt32((writeSectorNum << 16) | (1 << 15) | Convert.ToInt32(flashAddr / 4096)), this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
if (!ret.Value) return new(new Exception("Enable write flash failed"));
|
||||
@@ -150,23 +151,23 @@ public class RemoteUpdater
|
||||
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddrWithWait(
|
||||
this.ep, RemoteUpdaterAddr.WriteSign,
|
||||
0x00_00_00_01, 0x00_00_00_01, this.timeoutForWait);
|
||||
this.ep, 0, RemoteUpdaterAddr.WriteSign,
|
||||
0x00_00_00_01, 0x00_00_00_01, 100, this.timeoutForWait);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
if (!ret.Value) return new(new Exception(
|
||||
$"Flash clear failed after {this.timeoutForWait} milliseconds"));
|
||||
}
|
||||
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdaterAddr.WriteFIFO, bytesData, this.timeout);
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, 0, RemoteUpdaterAddr.WriteFIFO, bytesData, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
if (!ret.Value) return new(new Exception("Send data to flash failed"));
|
||||
}
|
||||
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddrWithWait(
|
||||
this.ep, RemoteUpdaterAddr.WriteSign,
|
||||
0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
|
||||
this.ep, 0, RemoteUpdaterAddr.WriteSign,
|
||||
0x00_00_01_00, 0x00_00_01_00, 100, this.timeoutForWait);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
@@ -314,14 +315,14 @@ public class RemoteUpdater
|
||||
private async ValueTask<Result<bool>> CheckBitstreamCRC(int bitstreamNum, int bitstreamLen, UInt32 checkSum)
|
||||
{
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdaterAddr.ReadCtrl2, 0x00_00_00_00, this.timeout);
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, 0, RemoteUpdaterAddr.ReadCtrl2, 0x00_00_00_00, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
if (!ret.Value) return new(new Exception("Write read control 2 failed"));
|
||||
}
|
||||
|
||||
{
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, RemoteUpdaterAddr.ReadCtrl1,
|
||||
this.ep, 0, RemoteUpdaterAddr.ReadCtrl1,
|
||||
Convert.ToUInt32((bitstreamLen << 16) | (1 << 15) | Convert.ToInt32(FlashAddr.Bitstream[bitstreamNum] / 4096)),
|
||||
this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
@@ -330,15 +331,15 @@ public class RemoteUpdater
|
||||
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddrWithWait(
|
||||
this.ep, RemoteUpdaterAddr.ReadSign,
|
||||
0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
|
||||
this.ep, 0, RemoteUpdaterAddr.ReadSign,
|
||||
0x00_00_01_00, 0x00_00_01_00, 10, this.timeoutForWait);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
if (!ret.Value) return new(new Exception(
|
||||
$"Read bitstream failed after {this.timeoutForWait} milliseconds"));
|
||||
}
|
||||
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdaterAddr.ReadCRC, this.timeout);
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, 0, RemoteUpdaterAddr.ReadCRC, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
|
||||
var bytes = ret.Value.Options.Data;
|
||||
@@ -368,7 +369,7 @@ public class RemoteUpdater
|
||||
$"Bitsteam num should be 0 ~ 3 for HotRest, but given {bitstreamNum}", nameof(bitstreamNum)));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, RemoteUpdaterAddr.HotResetCtrl,
|
||||
this.ep, 0, RemoteUpdaterAddr.HotResetCtrl,
|
||||
((FlashAddr.Bitstream[bitstreamNum] << 8) | 1), this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
@@ -381,7 +382,7 @@ public class RemoteUpdater
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public async ValueTask<Result<bool>> HotResetBitstream(int bitstreamNum)
|
||||
{
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 0);
|
||||
logger.Trace("Clear udp data finished");
|
||||
|
||||
{
|
||||
@@ -411,7 +412,7 @@ public class RemoteUpdater
|
||||
byte[]? bitstream2,
|
||||
byte[]? bitstream3)
|
||||
{
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 0);
|
||||
logger.Trace("Clear udp data finished");
|
||||
|
||||
for (int bitstreamNum = 0; bitstreamNum < 4; bitstreamNum++)
|
||||
@@ -462,7 +463,7 @@ public class RemoteUpdater
|
||||
$"The length of data should be divided by 4096, bug given {bytesData.Length}", nameof(bytesData)));
|
||||
var bitstreamBlockNum = bytesData.Length / (4 * 1024);
|
||||
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 0);
|
||||
logger.Trace("Clear udp data finished");
|
||||
|
||||
{
|
||||
@@ -538,11 +539,11 @@ public class RemoteUpdater
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public async ValueTask<Result<UInt32>> GetVersion()
|
||||
{
|
||||
await MsgBus.UDPServer.ClearUDPData(this.address);
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, 0);
|
||||
logger.Trace("Clear udp data finished");
|
||||
|
||||
{
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdaterAddr.Version, this.timeout);
|
||||
var ret = await UDPClientPool.ReadAddr(this.ep, 0, RemoteUpdaterAddr.Version, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
|
||||
var retData = ret.Value.Options.Data;
|
||||
1211
server/src/Services/HttpVideoStreamService.cs
Normal file
1211
server/src/Services/HttpVideoStreamService.cs
Normal file
File diff suppressed because it is too large
Load Diff
0
server/src/Services/ProgressService.cs
Normal file
0
server/src/Services/ProgressService.cs
Normal file
@@ -1,30 +0,0 @@
|
||||
// 此接口提供获取例程目录服务
|
||||
// GET /api/tutorials 返回所有可用的例程目录
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
// 获取当前文件的目录
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const publicDir = path.resolve(__dirname, '../public');
|
||||
|
||||
export function getTutorials(req: Request, res: Response) {
|
||||
try {
|
||||
const docDir = path.join(publicDir, 'doc');
|
||||
|
||||
// 读取doc目录下的所有文件夹
|
||||
const entries = fs.readdirSync(docDir, { withFileTypes: true });
|
||||
const dirs = entries
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
|
||||
// 返回文件夹列表
|
||||
res.json({ tutorials: dirs });
|
||||
} catch (error) {
|
||||
console.error('获取例程目录失败:', error);
|
||||
res.status(500).json({ error: '无法读取例程目录' });
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,8 @@ public class UDPClientPool
|
||||
var sendLen = socket.SendTo(buf, endPoint);
|
||||
socket.Close();
|
||||
|
||||
logger.Debug($"UDP socket send bytes to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
||||
logger.Debug($" Original Data: {BitConverter.ToString(buf).Replace("-", " ")}");
|
||||
// logger.Debug($"UDP socket send bytes to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
||||
// logger.Debug($" Original Data: {BitConverter.ToString(buf).Replace("-", " ")}");
|
||||
|
||||
if (sendLen == buf.Length) { return true; }
|
||||
else { return false; }
|
||||
@@ -91,9 +91,9 @@ public class UDPClientPool
|
||||
var sendLen = socket.SendTo(sendBytes, endPoint);
|
||||
socket.Close();
|
||||
|
||||
logger.Debug($"UDP socket send address package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
||||
logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
|
||||
logger.Debug($" Decoded Data: {pkg.ToString()}");
|
||||
// logger.Debug($"UDP socket send address package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
||||
// logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
|
||||
// logger.Debug($" Decoded Data: {pkg.ToString()}");
|
||||
|
||||
if (sendLen == sendBytes.Length) { return true; }
|
||||
else { return false; }
|
||||
@@ -110,6 +110,46 @@ public class UDPClientPool
|
||||
return await Task.Run(() => { return SendAddrPack(endPoint, pkg); });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送多个地址包
|
||||
/// </summary>
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="pkgs">地址包集合(最多512 / 8)</param>
|
||||
/// <returns>是否全部成功</returns>
|
||||
public static bool SendMultiAddrPack(IPEndPoint endPoint, IEnumerable<WebProtocol.SendAddrPackage> pkgs)
|
||||
{
|
||||
const int maxPkgs = 512 / 8;
|
||||
var pkgList = pkgs.Take(maxPkgs).ToList();
|
||||
if (pkgList.Count == 0) return false;
|
||||
|
||||
// 合并所有包为一个buffer
|
||||
int totalLen = pkgList.Sum(pkg => pkg.ToBytes().Length);
|
||||
byte[] buffer = new byte[totalLen];
|
||||
int offset = 0;
|
||||
foreach (var pkg in pkgList)
|
||||
{
|
||||
var bytes = pkg.ToBytes();
|
||||
Buffer.BlockCopy(bytes, 0, buffer, offset, bytes.Length);
|
||||
offset += bytes.Length;
|
||||
}
|
||||
|
||||
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
var sendLen = socket.SendTo(buffer.ToArray(), endPoint);
|
||||
socket.Close();
|
||||
|
||||
return sendLen == buffer.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步发送多个地址包
|
||||
/// </summary>
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="pkgs">地址包集合(最多512 / 8)</param>
|
||||
/// <returns>是否全部成功</returns>
|
||||
public async static ValueTask<bool> SendMultiAddrPackAsync(IPEndPoint endPoint, IEnumerable<WebProtocol.SendAddrPackage> pkgs)
|
||||
{
|
||||
return await Task.Run(() => SendMultiAddrPack(endPoint, pkgs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送数据包
|
||||
@@ -124,8 +164,8 @@ public class UDPClientPool
|
||||
var sendLen = socket.SendTo(sendBytes, endPoint);
|
||||
socket.Close();
|
||||
|
||||
logger.Debug($"UDP socket send data package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
||||
logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
|
||||
// logger.Debug($"UDP socket send data package to device {endPoint.Address.ToString()}:{endPoint.Port.ToString()}:");
|
||||
// logger.Debug($" Original Data: {BitConverter.ToString(pkg.ToBytes()).Replace("-", " ")}");
|
||||
|
||||
if (sendLen == sendBytes.Length) { return true; }
|
||||
else { return false; }
|
||||
@@ -177,25 +217,28 @@ public class UDPClientPool
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// 读取设备地址数据
|
||||
/// </summary>
|
||||
/// <param name="endPoint">[TODO:parameter]</param>
|
||||
/// <param name="devAddr">[TODO:parameter]</param>
|
||||
/// <param name="timeout">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="devAddr">设备地址</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>读取结果,包含接收到的数据包</returns>
|
||||
public static async ValueTask<Result<RecvDataPackage>> ReadAddr(
|
||||
IPEndPoint endPoint, uint devAddr, int timeout = 1000)
|
||||
IPEndPoint endPoint, int taskID, uint devAddr, int timeout = 1000)
|
||||
{
|
||||
var ret = false;
|
||||
var opts = new SendAddrPackOptions();
|
||||
var opts = new SendAddrPackOptions()
|
||||
{
|
||||
BurstType = BurstType.FixedBurst,
|
||||
BurstLength = 0,
|
||||
CommandID = Convert.ToByte(taskID),
|
||||
Address = devAddr,
|
||||
IsWrite = false,
|
||||
};
|
||||
|
||||
opts.BurstType = BurstType.FixedBurst;
|
||||
opts.BurstLength = 0;
|
||||
opts.CommandID = 0;
|
||||
opts.Address = devAddr;
|
||||
|
||||
// Read Jtag State Register
|
||||
opts.IsWrite = false;
|
||||
// Read Register
|
||||
ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
|
||||
if (!ret) return new(new Exception("Send Address Package Failed!"));
|
||||
|
||||
@@ -204,7 +247,7 @@ public class UDPClientPool
|
||||
return new(new Exception("Message Bus not Working!"));
|
||||
|
||||
var retPack = await MsgBus.UDPServer.WaitForDataAsync(
|
||||
endPoint.Address.ToString(), endPoint.Port, timeout);
|
||||
endPoint.Address.ToString(), taskID, endPoint.Port, timeout);
|
||||
if (!retPack.IsSuccessful) return new(retPack.Error);
|
||||
else if (!retPack.Value.IsSuccessful)
|
||||
return new(new Exception("Send address package failed"));
|
||||
@@ -217,20 +260,21 @@ public class UDPClientPool
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// 读取设备地址数据并校验结果
|
||||
/// </summary>
|
||||
/// <param name="endPoint">[TODO:parameter]</param>
|
||||
/// <param name="devAddr">[TODO:parameter]</param>
|
||||
/// <param name="result">[TODO:parameter]</param>
|
||||
/// <param name="resultMask">[TODO:parameter]</param>
|
||||
/// <param name="timeout">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="devAddr">设备地址</param>
|
||||
/// <param name="result">期望的结果值</param>
|
||||
/// <param name="resultMask">结果掩码,用于位校验</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>校验结果,true表示数据匹配期望值</returns>
|
||||
public static async ValueTask<Result<bool>> ReadAddr(
|
||||
IPEndPoint endPoint, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000)
|
||||
IPEndPoint endPoint, int taskID, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000)
|
||||
{
|
||||
var address = endPoint.Address.ToString();
|
||||
|
||||
var ret = await ReadAddr(endPoint, devAddr, timeout);
|
||||
var ret = await ReadAddr(endPoint, taskID, devAddr, timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
if (!ret.Value.IsSuccessful)
|
||||
return new(new Exception($"Read device {address} address {devAddr} failed"));
|
||||
@@ -255,16 +299,18 @@ public class UDPClientPool
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// 读取设备地址数据并等待直到结果匹配或超时
|
||||
/// </summary>
|
||||
/// <param name="endPoint">[TODO:parameter]</param>
|
||||
/// <param name="devAddr">[TODO:parameter]</param>
|
||||
/// <param name="result">[TODO:parameter]</param>
|
||||
/// <param name="resultMask">[TODO:parameter]</param>
|
||||
/// <param name="timeout">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="devAddr">设备地址</param>
|
||||
/// <param name="result">期望的结果值</param>
|
||||
/// <param name="resultMask">结果掩码,用于位校验</param>
|
||||
/// <param name="waittime">等待间隔时间(毫秒)</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>校验结果,true表示在超时前数据匹配期望值</returns>
|
||||
public static async ValueTask<Result<bool>> ReadAddrWithWait(
|
||||
IPEndPoint endPoint, uint devAddr, UInt32 result, UInt32 resultMask, int timeout = 1000)
|
||||
IPEndPoint endPoint, int taskID, uint devAddr, UInt32 result, UInt32 resultMask, int waittime = 100, int timeout = 1000)
|
||||
{
|
||||
var address = endPoint.Address.ToString();
|
||||
|
||||
@@ -274,10 +320,10 @@ public class UDPClientPool
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
if (elapsed >= TimeSpan.FromMilliseconds(timeout)) break;
|
||||
var timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
|
||||
|
||||
await Task.Delay(waittime);
|
||||
try
|
||||
{
|
||||
var ret = await ReadAddr(endPoint, devAddr, Convert.ToInt32(timeleft.TotalMilliseconds));
|
||||
var ret = await ReadAddr(endPoint, taskID, devAddr, Convert.ToInt32(timeleft.TotalMilliseconds));
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
if (!ret.Value.IsSuccessful)
|
||||
return new(new Exception($"Read device {address} address {devAddr} failed"));
|
||||
@@ -302,28 +348,255 @@ public class UDPClientPool
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// 从设备地址读取字节数组数据(支持大数据量分段传输)
|
||||
/// </summary>
|
||||
/// <param name="endPoint">[TODO:parameter]</param>
|
||||
/// <param name="devAddr">[TODO:parameter]</param>
|
||||
/// <param name="data">[TODO:parameter]</param>
|
||||
/// <param name="timeout">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public static async ValueTask<Result<bool>> WriteAddr(
|
||||
IPEndPoint endPoint, UInt32 devAddr, UInt32 data, int timeout = 1000)
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="devAddr">设备地址</param>
|
||||
/// <param name="dataLength">要读取的数据长度(4字节)</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>读取结果,包含接收到的字节数组</returns>
|
||||
public static async ValueTask<Result<byte[]>> ReadAddr4Bytes(
|
||||
IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, int timeout = 1000)
|
||||
{
|
||||
var ret = false;
|
||||
var opts = new SendAddrPackOptions();
|
||||
var opts = new SendAddrPackOptions()
|
||||
{
|
||||
BurstLength = 0,
|
||||
Address = 0,
|
||||
BurstType = BurstType.FixedBurst,
|
||||
CommandID = Convert.ToByte(taskID),
|
||||
IsWrite = false,
|
||||
};
|
||||
var resultData = new List<byte>();
|
||||
|
||||
opts.BurstType = BurstType.FixedBurst;
|
||||
opts.BurstLength = 0;
|
||||
opts.CommandID = 0;
|
||||
opts.Address = devAddr;
|
||||
|
||||
// Write Jtag State Register
|
||||
opts.IsWrite = true;
|
||||
// Check Msg Bus
|
||||
if (!MsgBus.IsRunning)
|
||||
return new(new Exception("Message bus not working!"));
|
||||
|
||||
// Calculate read times and segments
|
||||
var max4BytesPerRead = 0x80; // 512 bytes per read
|
||||
var rest4Bytes = dataLength % max4BytesPerRead;
|
||||
var readTimes = (rest4Bytes != 0) ?
|
||||
(dataLength / max4BytesPerRead + 1) :
|
||||
(dataLength / max4BytesPerRead);
|
||||
|
||||
for (var i = 0; i < readTimes; i++)
|
||||
{
|
||||
// Calculate current segment size
|
||||
var isLastSegment = i == readTimes - 1;
|
||||
var currentSegmentSize = (isLastSegment && rest4Bytes != 0) ? rest4Bytes : max4BytesPerRead;
|
||||
|
||||
// Set burst length (in 32-bit words)
|
||||
opts.BurstLength = (byte)(currentSegmentSize - 1);
|
||||
|
||||
// Update address for current segment
|
||||
opts.Address = devAddr + (uint)(i * max4BytesPerRead);
|
||||
|
||||
// Send read address package
|
||||
ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
|
||||
if (!ret) return new(new Exception($"Send address package failed at segment {i}!"));
|
||||
|
||||
// Wait for data response
|
||||
var retPack = await MsgBus.UDPServer.WaitForDataAsync(
|
||||
endPoint.Address.ToString(), taskID, endPoint.Port, timeout);
|
||||
if (!retPack.IsSuccessful) return new(retPack.Error);
|
||||
|
||||
if (!retPack.Value.IsSuccessful)
|
||||
return new(new Exception($"Read address package failed at segment {i}"));
|
||||
|
||||
var retPackOpts = retPack.Value.Options;
|
||||
if (retPackOpts.Data is null)
|
||||
return new(new Exception($"Data is null at segment {i}, package: {retPackOpts.ToString()}"));
|
||||
|
||||
// Validate received data length
|
||||
if (retPackOpts.Data.Length != currentSegmentSize * 4)
|
||||
return new(new Exception($"Expected {currentSegmentSize * 4} bytes but received {retPackOpts.Data.Length} bytes at segment {i}"));
|
||||
|
||||
// Add received data to result
|
||||
resultData.AddRange(retPackOpts.Data);
|
||||
}
|
||||
|
||||
// Validate total data length
|
||||
if (resultData.Count != dataLength * 4)
|
||||
return new(new Exception($"Expected total {dataLength * 4} bytes but received {resultData.Count} bytes"));
|
||||
|
||||
return resultData.ToArray();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 从设备地址读取字节数组数据(支持大数据量分段传输,先发送所有地址包再接收所有数据包)
|
||||
/// </summary>
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="devAddr">设备地址</param>
|
||||
/// <param name="dataLength">要读取的数据长度(4字节)</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>读取结果,包含接收到的字节数组</returns>
|
||||
public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync(
|
||||
IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, int timeout = 1000)
|
||||
{
|
||||
var pkgList = new List<SendAddrPackage>();
|
||||
var resultData = new List<byte>();
|
||||
|
||||
// Check Msg Bus
|
||||
if (!MsgBus.IsRunning)
|
||||
return new(new Exception("Message bus not working!"));
|
||||
|
||||
// Prepare packages for each segment
|
||||
var max4BytesPerRead = 0x80; // 512 bytes per read
|
||||
var rest4Bytes = dataLength % max4BytesPerRead;
|
||||
var readTimes = (rest4Bytes != 0) ?
|
||||
(dataLength / max4BytesPerRead + 1) :
|
||||
(dataLength / max4BytesPerRead);
|
||||
|
||||
for (var i = 0; i < readTimes; i++)
|
||||
{
|
||||
var isLastSegment = i == readTimes - 1;
|
||||
var currentSegmentSize = (isLastSegment && rest4Bytes != 0) ? rest4Bytes : max4BytesPerRead;
|
||||
|
||||
var opts = new SendAddrPackOptions
|
||||
{
|
||||
BurstType = BurstType.FixedBurst,
|
||||
CommandID = Convert.ToByte(taskID),
|
||||
IsWrite = false,
|
||||
BurstLength = (byte)(currentSegmentSize - 1),
|
||||
Address = devAddr + (uint)(i * max4BytesPerRead)
|
||||
};
|
||||
pkgList.Add(new SendAddrPackage(opts));
|
||||
}
|
||||
|
||||
// Send address packages in batches of 128, control outstanding
|
||||
int sentCount = 0;
|
||||
var startTime = DateTime.Now;
|
||||
const int batchSize = 32;
|
||||
while (sentCount < pkgList.Count)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
if (elapsed >= TimeSpan.FromMilliseconds(timeout))
|
||||
break;
|
||||
var timeleft = timeout - (int)elapsed.TotalMilliseconds;
|
||||
|
||||
var found = await MsgBus.UDPServer.GetDataCountAsync(endPoint.Address.ToString(), taskID, timeleft);
|
||||
int outstanding = sentCount - (found.HasValue ? found.Value : 0);
|
||||
|
||||
// If outstanding >= 512 - batchSize, wait for some data to be received
|
||||
if (outstanding >= 128 - batchSize)
|
||||
continue;
|
||||
|
||||
|
||||
// Send next batch of address packages (up to 128)
|
||||
int batchSend = Math.Min(batchSize, pkgList.Count - sentCount);
|
||||
var batchPkgs = pkgList.Skip(sentCount).Take(batchSend);
|
||||
var ret = await UDPClientPool.SendMultiAddrPackAsync(endPoint, batchPkgs);
|
||||
if (!ret) return new(new Exception($"Send address package batch failed at segment {sentCount}!"));
|
||||
sentCount += batchSend;
|
||||
// Task.Delay(1).Wait();
|
||||
}
|
||||
|
||||
// Wait until enough data is received or timeout
|
||||
startTime = DateTime.Now;
|
||||
var udpDatas = new List<UDPData>();
|
||||
while (true)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
if (elapsed >= TimeSpan.FromMilliseconds(timeout)) break;
|
||||
var timeleft = timeout - (int)elapsed.TotalMilliseconds;
|
||||
|
||||
var found = await MsgBus.UDPServer.GetDataCountAsync(endPoint.Address.ToString(), taskID, timeleft);
|
||||
if (found.HasValue && found.Value >= readTimes)
|
||||
{
|
||||
var dataArr = await MsgBus.UDPServer.FindDataArrayAsync(endPoint.Address.ToString(), taskID, timeleft);
|
||||
if (dataArr.HasValue)
|
||||
{
|
||||
udpDatas.AddRange(dataArr.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (udpDatas.Count < readTimes)
|
||||
return new(new Exception($"Expected {readTimes} UDP data packets but received {udpDatas.Count}"));
|
||||
|
||||
// Collect and validate all received data
|
||||
for (var i = 0; i < udpDatas.Count; i++)
|
||||
{
|
||||
var bytes = udpDatas[i].Data;
|
||||
var expectedLen = ((pkgList[i].Options.BurstLength + 1) * 4);
|
||||
if ((bytes.Length - 8) != expectedLen)
|
||||
return new(new Exception($"Expected {expectedLen} bytes but received {bytes.Length - 8} bytes at segment {i}"));
|
||||
resultData.AddRange(bytes[8..]);
|
||||
}
|
||||
|
||||
// Validate total data length
|
||||
if (resultData.Count != dataLength * 4)
|
||||
return new(new Exception($"Expected total {dataLength * 4} bytes but received {resultData.Count} bytes"));
|
||||
|
||||
return resultData.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 顺序读取多个地址的数据,并合并BodyData后返回
|
||||
/// </summary>
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="addr">地址数组</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>合并后的BodyData字节数组</returns>
|
||||
public static async ValueTask<Result<byte[]>> ReadAddrSeq(IPEndPoint endPoint, int taskID, UInt32[] addr, int timeout = 1000)
|
||||
{
|
||||
var length = addr.Length;
|
||||
var resultData = new List<byte>();
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
var ret = await ReadAddr(endPoint, taskID, addr[i], timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"ReadAddrSeq failed at index {i}: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value.IsSuccessful)
|
||||
{
|
||||
logger.Error($"ReadAddrSeq failed at index {i}: Read not successful");
|
||||
return new(new Exception($"ReadAddrSeq failed at index {i}"));
|
||||
}
|
||||
var data = ret.Value.Options.Data;
|
||||
if (data is null)
|
||||
{
|
||||
logger.Error($"ReadAddrSeq got null data at index {i}");
|
||||
return new(new Exception($"ReadAddrSeq got null data at index {i}"));
|
||||
}
|
||||
resultData.AddRange(data);
|
||||
}
|
||||
return resultData.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 向设备地址写入32位数据
|
||||
/// </summary>
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="devAddr">设备地址</param>
|
||||
/// <param name="data">要写入的32位数据</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>写入结果,true表示写入成功</returns>
|
||||
public static async ValueTask<Result<bool>> WriteAddr(
|
||||
IPEndPoint endPoint, int taskID, UInt32 devAddr, UInt32 data, int timeout = 1000)
|
||||
{
|
||||
var ret = false;
|
||||
var opts = new SendAddrPackOptions()
|
||||
{
|
||||
BurstType = BurstType.FixedBurst,
|
||||
BurstLength = 0,
|
||||
CommandID = Convert.ToByte(taskID),
|
||||
Address = devAddr,
|
||||
IsWrite = true,
|
||||
};
|
||||
|
||||
// Write Register
|
||||
ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
|
||||
if (!ret) return new(new Exception("Send 1st address package failed!"));
|
||||
// Send Data Package
|
||||
@@ -337,50 +610,59 @@ public class UDPClientPool
|
||||
|
||||
// Wait for Write Ack
|
||||
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(
|
||||
endPoint.Address.ToString(), endPoint.Port, timeout);
|
||||
endPoint.Address.ToString(), taskID, endPoint.Port, timeout);
|
||||
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
|
||||
|
||||
return udpWriteAck.Value.IsSuccessful;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// 向设备地址写入字节数组数据(支持大数据量分段传输)
|
||||
/// </summary>
|
||||
/// <param name="endPoint">[TODO:parameter]</param>
|
||||
/// <param name="devAddr">[TODO:parameter]</param>
|
||||
/// <param name="dataArray">[TODO:parameter]</param>
|
||||
/// <param name="timeout">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public static async ValueTask<Result<bool>> WriteAddr(IPEndPoint endPoint, UInt32 devAddr, byte[] dataArray, int timeout = 1000)
|
||||
/// <param name="endPoint">IP端点(IP地址与端口)</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="devAddr">设备地址</param>
|
||||
/// <param name="dataArray">要写入的字节数组</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>写入结果,true表示写入成功</returns>
|
||||
public static async ValueTask<Result<bool>> WriteAddr(
|
||||
IPEndPoint endPoint, int taskID, UInt32 devAddr, byte[] dataArray, int timeout = 1000)
|
||||
{
|
||||
var ret = false;
|
||||
var opts = new SendAddrPackOptions();
|
||||
var opts = new SendAddrPackOptions()
|
||||
{
|
||||
BurstType = BurstType.FixedBurst,
|
||||
CommandID = Convert.ToByte(taskID),
|
||||
Address = devAddr,
|
||||
BurstLength = 0,
|
||||
IsWrite = true,
|
||||
};
|
||||
|
||||
|
||||
opts.BurstType = BurstType.FixedBurst;
|
||||
opts.CommandID = 0;
|
||||
opts.Address = devAddr;
|
||||
var max4BytesPerRead = 128; // 1024 bytes per read
|
||||
|
||||
// Check Msg Bus
|
||||
if (!MsgBus.IsRunning)
|
||||
return new(new Exception("Message bus not working!"));
|
||||
|
||||
opts.IsWrite = true;
|
||||
var hasRest = dataArray.Length % (256 * (32 / 8)) != 0;
|
||||
var hasRest = dataArray.Length % (max4BytesPerRead * (32 / 8)) != 0;
|
||||
var writeTimes = hasRest ?
|
||||
dataArray.Length / (256 * (32 / 8)) + 1 :
|
||||
dataArray.Length / (256 * (32 / 8));
|
||||
dataArray.Length / (max4BytesPerRead * (32 / 8)) + 1 :
|
||||
dataArray.Length / (max4BytesPerRead * (32 / 8));
|
||||
for (var i = 0; i < writeTimes; i++)
|
||||
{
|
||||
// Sperate Data Array
|
||||
var isLastData = i == writeTimes - 1;
|
||||
var sendDataArray =
|
||||
isLastData ?
|
||||
dataArray[(i * (256 * (32 / 8)))..] :
|
||||
dataArray[(i * (256 * (32 / 8)))..((i + 1) * (256 * (32 / 8)))];
|
||||
var sendDataArray = isLastData ?
|
||||
dataArray[(i * (max4BytesPerRead * (32 / 8)))..] :
|
||||
dataArray[(i * (max4BytesPerRead * (32 / 8)))..((i + 1) * (max4BytesPerRead * (32 / 8)))];
|
||||
|
||||
// Calculate BurstLength
|
||||
opts.BurstLength = ((byte)(
|
||||
sendDataArray.Length % 4 == 0 ?
|
||||
(sendDataArray.Length / 4 - 1) :
|
||||
(sendDataArray.Length / 4)
|
||||
));
|
||||
|
||||
// Write Jtag State Register
|
||||
opts.BurstLength = ((byte)(sendDataArray.Length / 4 - 1));
|
||||
ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts));
|
||||
if (!ret) return new(new Exception("Send 1st address package failed!"));
|
||||
|
||||
@@ -389,7 +671,7 @@ public class UDPClientPool
|
||||
if (!ret) return new(new Exception("Send data package failed!"));
|
||||
|
||||
// Wait for Write Ack
|
||||
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(endPoint.Address.ToString(), endPoint.Port, timeout);
|
||||
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(endPoint.Address.ToString(), taskID, endPoint.Port, timeout);
|
||||
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
|
||||
|
||||
if (!udpWriteAck.Value.IsSuccessful)
|
||||
@@ -399,4 +681,39 @@ public class UDPClientPool
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="endPoint">[TODO:parameter]</param>
|
||||
/// <param name="taskID">[TODO:parameter]</param>
|
||||
/// <param name="addr">[TODO:parameter]</param>
|
||||
/// <param name="data">[TODO:parameter]</param>
|
||||
/// <param name="timeout">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public static async ValueTask<Result<bool>> WriteAddrSeq(IPEndPoint endPoint, int taskID, UInt32[] addr, byte[] data, int timeout = 1000)
|
||||
{
|
||||
var length = addr.Length;
|
||||
if (length != data.Length)
|
||||
{
|
||||
logger.Error($"TODO");
|
||||
return new(new ArgumentException($"TODO"));
|
||||
}
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
var ret = await WriteAddr(endPoint, taskID, addr[i], (UInt32)data[i], timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"TODO");
|
||||
return new(ret.Error);
|
||||
}
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Error($"TODO");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation; // 添加这个引用
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using Common;
|
||||
using DotNext;
|
||||
using DotNext.Threading;
|
||||
using Newtonsoft.Json;
|
||||
@@ -15,6 +18,10 @@ public class UDPData
|
||||
/// </summary>
|
||||
public required DateTime DateTime { get; set; }
|
||||
/// <summary>
|
||||
/// 数据包时间戳
|
||||
/// </summary>
|
||||
public required UInt32 Timestamp { get; set; }
|
||||
/// <summary>
|
||||
/// 发送来源的IP地址
|
||||
/// </summary>
|
||||
public required string Address { get; set; }
|
||||
@@ -22,6 +29,11 @@ public class UDPData
|
||||
/// 发送来源的端口号
|
||||
/// </summary>
|
||||
public required int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务ID
|
||||
/// </summary>
|
||||
public required int TaskID { get; set; }
|
||||
/// <summary>
|
||||
/// 接受到的数据
|
||||
/// </summary>
|
||||
@@ -42,8 +54,10 @@ public class UDPData
|
||||
return new UDPData()
|
||||
{
|
||||
DateTime = this.DateTime,
|
||||
Timestamp = this.Timestamp,
|
||||
Address = new string(this.Address),
|
||||
Port = this.Port,
|
||||
TaskID = this.TaskID,
|
||||
Data = cloneData,
|
||||
HasRead = this.HasRead
|
||||
};
|
||||
@@ -66,17 +80,22 @@ public class UDPServer
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private static Dictionary<string, Queue<UDPData>> udpData = new Dictionary<string, Queue<UDPData>>();
|
||||
private ConcurrentDictionary<string, SortedList<UInt32, UDPData>> udpData
|
||||
= new ConcurrentDictionary<string, SortedList<UInt32, UDPData>>();
|
||||
private readonly AsyncReaderWriterLock udpDataLock = new AsyncReaderWriterLock();
|
||||
|
||||
private int listenPort;
|
||||
private UdpClient listener;
|
||||
private List<UdpClient> listeners = new List<UdpClient>();
|
||||
private List<Task> tasks = new List<Task>();
|
||||
private IPEndPoint groupEP;
|
||||
|
||||
private bool isRunning = false;
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
private bool disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否正在工作
|
||||
/// </summary>
|
||||
public bool IsRunning { get { return isRunning; } }
|
||||
public bool IsRunning => cancellationTokenSource?.Token.IsCancellationRequested == false;
|
||||
|
||||
/// <summary> UDP 服务器的错误代码 </summary>
|
||||
public enum ErrorCode
|
||||
@@ -95,15 +114,27 @@ public class UDPServer
|
||||
/// Construct a udp server with fixed port
|
||||
/// </summary>
|
||||
/// <param name="port"> Device UDP Port </param>
|
||||
/// <param name="num"> UDP Client Num </param>
|
||||
/// <returns> UDPServer class </returns>
|
||||
public UDPServer(int port)
|
||||
public UDPServer(int port, int num)
|
||||
{
|
||||
// Construction
|
||||
listenPort = port;
|
||||
this.listenPort = port;
|
||||
try
|
||||
{
|
||||
listener = new UdpClient(listenPort);
|
||||
groupEP = new IPEndPoint(IPAddress.Any, listenPort);
|
||||
for (int i = 0; i < num; i++)
|
||||
{
|
||||
int currentPort = this.listenPort + i;
|
||||
if (IsPortInUse(currentPort))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"端口{currentPort}已被占用,无法启动UDP Server",
|
||||
nameof(port)
|
||||
);
|
||||
}
|
||||
listeners.Add(new UdpClient(currentPort));
|
||||
}
|
||||
this.groupEP = new IPEndPoint(IPAddress.Any, listenPort);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -115,10 +146,34 @@ public class UDPServer
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPortInUse(int port)
|
||||
{
|
||||
bool inUse = false;
|
||||
try
|
||||
{
|
||||
var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
|
||||
var udpListeners = ipGlobalProperties.GetActiveUdpListeners();
|
||||
foreach (var ep in udpListeners)
|
||||
{
|
||||
if (ep.Port == port)
|
||||
{
|
||||
inUse = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"Failed to check port usage for port {port}: {ex.Message}");
|
||||
}
|
||||
return inUse;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步寻找目标发送的内容
|
||||
/// </summary>
|
||||
/// <param name="ipAddr"> 目标IP地址 </param>
|
||||
/// <param name="taskID">[TODO:parameter]</param>
|
||||
/// <param name="timeout">超时时间</param>
|
||||
/// <param name="cycle">延迟时间</param>
|
||||
/// <param name="callerName">调用函数名称</param>
|
||||
@@ -129,39 +184,44 @@ public class UDPServer
|
||||
/// Optional 存在时,为最先收到的数据
|
||||
/// </returns>
|
||||
public async ValueTask<Optional<UDPData>> FindDataAsync(
|
||||
string ipAddr, int timeout = 1000, int cycle = 0,
|
||||
string ipAddr, int taskID, int timeout = 1000, int cycle = 0,
|
||||
[CallerMemberName] string callerName = "",
|
||||
[CallerLineNumber] int callerLineNum = 0)
|
||||
[CallerLineNumber] int callerLineNum = 0
|
||||
)
|
||||
{
|
||||
UDPData? data = null;
|
||||
|
||||
logger.Debug($"Caller \"{callerName}|{callerLineNum}\": Try to find {ipAddr} UDP Data");
|
||||
var key = $"{ipAddr}-{taskID}";
|
||||
|
||||
var startTime = DateTime.Now;
|
||||
var isTimeout = false;
|
||||
var timeleft = TimeSpan.FromMilliseconds(timeout);
|
||||
while (!isTimeout)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
isTimeout = elapsed >= TimeSpan.FromMilliseconds(timeout);
|
||||
if (isTimeout) break;
|
||||
|
||||
timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
|
||||
using (await udpData.AcquireWriteLockAsync(timeleft))
|
||||
try
|
||||
{
|
||||
if (udpData.ContainsKey(ipAddr) &&
|
||||
udpData.TryGetValue(ipAddr, out var dataQueue) &&
|
||||
dataQueue.Count > 0)
|
||||
using (await udpDataLock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(timeout)))
|
||||
{
|
||||
data = dataQueue.Dequeue();
|
||||
logger.Debug($"Find UDP Data: {data.ToString()}");
|
||||
break;
|
||||
if (udpData.TryGetValue(key, out var sortedList) && sortedList.Count > 0)
|
||||
{
|
||||
// 获取最早的数据(第一个元素)
|
||||
var firstKey = sortedList.Keys[0];
|
||||
data = sortedList[firstKey];
|
||||
sortedList.RemoveAt(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(cycle);
|
||||
catch
|
||||
{
|
||||
logger.Trace("Get nothing even after time out");
|
||||
return new(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (data is null)
|
||||
{
|
||||
logger.Trace("Get nothing even after time out");
|
||||
@@ -174,37 +234,45 @@ public class UDPServer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取还未被读取的数据列表
|
||||
/// 异步寻找目标发送的所有内容,并清空队列
|
||||
/// </summary>
|
||||
/// <param name="ipAddr">IP地址</param>
|
||||
/// <param name="ipAddr">目标IP地址</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="timeout">超时时间</param>
|
||||
/// <param name="cycle">延迟时间</param>
|
||||
/// <returns>数据列表</returns>
|
||||
public async ValueTask<Optional<List<UDPData>>> GetDataArrayAsync(string ipAddr, int timeout = 1000, int cycle = 0)
|
||||
/// <returns>异步Optional 数据包列表</returns>
|
||||
public async ValueTask<Optional<List<UDPData>>> FindDataArrayAsync(string ipAddr, int taskID, int timeout = 1000)
|
||||
{
|
||||
List<UDPData>? data = null;
|
||||
var key = $"{ipAddr}-{taskID}";
|
||||
|
||||
|
||||
var startTime = DateTime.Now;
|
||||
var isTimeout = false;
|
||||
var timeleft = TimeSpan.FromMilliseconds(timeout);
|
||||
while (!isTimeout)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
isTimeout = elapsed >= TimeSpan.FromMilliseconds(timeout);
|
||||
if (isTimeout) break;
|
||||
|
||||
timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
|
||||
using (await udpData.AcquireReadLockAsync(timeleft))
|
||||
try
|
||||
{
|
||||
if (udpData.ContainsKey(ipAddr) &&
|
||||
udpData.TryGetValue(ipAddr, out var dataQueue) &&
|
||||
dataQueue.Count > 0)
|
||||
using (await udpDataLock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(timeout)))
|
||||
{
|
||||
data = dataQueue.ToList();
|
||||
logger.Debug($"Find UDP Data Array: {JsonConvert.SerializeObject(data)}");
|
||||
break;
|
||||
if (udpData.TryGetValue(key, out var sortedList) && sortedList.Count > 0)
|
||||
{
|
||||
data = new List<UDPData>(sortedList.Values);
|
||||
// 输出数据
|
||||
// PrintDataArray(data);
|
||||
sortedList.Clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger.Trace("Get nothing even after time out");
|
||||
return new(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (data is null)
|
||||
@@ -218,17 +286,96 @@ public class UDPServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取还未被读取的数据列表
|
||||
/// </summary>
|
||||
/// <param name="ipAddr">IP地址</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="timeout">超时时间</param>
|
||||
/// <returns>数据列表</returns>
|
||||
public async ValueTask<Optional<List<UDPData>>> GetDataArrayAsync(string ipAddr, int taskID, int timeout = 1000)
|
||||
{
|
||||
List<UDPData>? data = null;
|
||||
|
||||
try
|
||||
{
|
||||
using (await udpDataLock.AcquireReadLockAsync(TimeSpan.FromMilliseconds(timeout)))
|
||||
{
|
||||
var key = $"{ipAddr}-{taskID}";
|
||||
if (udpData.TryGetValue(key, out var sortedList) && sortedList.Count > 0)
|
||||
{
|
||||
data = new List<UDPData>(sortedList.Values);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
logger.Trace("Failed to acquire read lock within timeout");
|
||||
return new(null);
|
||||
}
|
||||
|
||||
if (data is null)
|
||||
{
|
||||
logger.Trace("Get nothing even after time out");
|
||||
return new(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取指定IP和任务ID的数据队列长度
|
||||
/// </summary>
|
||||
/// <param name="ipAddr">IP地址</param>
|
||||
/// <param name="taskID">任务ID</param>
|
||||
/// <param name="timeout">超时时间</param>
|
||||
/// <returns>数据队列长度</returns>
|
||||
public async ValueTask<Optional<int>> GetDataCountAsync(string ipAddr, int taskID, int timeout = 1000)
|
||||
{
|
||||
int? count = null;
|
||||
|
||||
try
|
||||
{
|
||||
using (await udpDataLock.AcquireReadLockAsync(TimeSpan.FromMilliseconds(timeout)))
|
||||
{
|
||||
var key = $"{ipAddr}-{taskID}";
|
||||
if (udpData.TryGetValue(key, out var sortedList))
|
||||
{
|
||||
count = sortedList.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
logger.Trace("Failed to acquire read lock within timeout");
|
||||
return Optional<int>.None;
|
||||
}
|
||||
|
||||
if (count is null)
|
||||
{
|
||||
logger.Trace("Get nothing even after time out");
|
||||
return Optional<int>.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(count.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步等待写响应
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
/// <param name="taskID">[TODO:parameter]</param>
|
||||
/// <param name="port">UDP 端口</param>
|
||||
/// <param name="timeout">超时时间范围</param>
|
||||
/// <returns>接收响应包</returns>
|
||||
public async ValueTask<Result<WebProtocol.RecvRespPackage>> WaitForAckAsync
|
||||
(string address, int port = -1, int timeout = 1000)
|
||||
(string address, int taskID, int port = -1, int timeout = 1000)
|
||||
{
|
||||
var data = await FindDataAsync(address, timeout);
|
||||
var data = await FindDataAsync(address, taskID, timeout);
|
||||
if (!data.HasValue)
|
||||
return new(new Exception("Get None even after time out!"));
|
||||
|
||||
@@ -247,13 +394,14 @@ public class UDPServer
|
||||
/// 异步等待数据
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
/// <param name="taskID">[TODO:parameter]</param>
|
||||
/// <param name="port">UDP 端口</param>
|
||||
/// <param name="timeout">超时时间范围</param>
|
||||
/// <returns>接收数据包</returns>
|
||||
public async ValueTask<Result<RecvDataPackage>> WaitForDataAsync
|
||||
(string address, int port = -1, int timeout = 1000)
|
||||
(string address, int taskID, int port = -1, int timeout = 1000)
|
||||
{
|
||||
var data = await FindDataAsync(address, timeout);
|
||||
var data = await FindDataAsync(address, taskID, timeout);
|
||||
if (!data.HasValue)
|
||||
return new(new Exception("Get None even after time out!"));
|
||||
|
||||
@@ -268,75 +416,73 @@ public class UDPServer
|
||||
return retPack.Value;
|
||||
}
|
||||
|
||||
private void ReceiveHandler(IAsyncResult res)
|
||||
private async Task ReceiveHandler(byte[] data, IPEndPoint endPoint, DateTime time)
|
||||
{
|
||||
logger.Trace("Enter handler");
|
||||
var remoteEP = new IPEndPoint(IPAddress.Any, listenPort);
|
||||
byte[] bytes = listener.EndReceive(res, ref remoteEP);
|
||||
|
||||
// Handle RemoteEP
|
||||
if (remoteEP is null)
|
||||
// 异步锁保护 udpData
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
logger.Debug($"Receive Data from Unknown at {DateTime.Now.ToString()}:");
|
||||
logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
|
||||
goto BEGIN_RECEIVE;
|
||||
}
|
||||
try
|
||||
{
|
||||
// Handle RemoteEP
|
||||
if (endPoint is null)
|
||||
{
|
||||
logger.Debug($"Receive Data from Unknown at {DateTime.Now.ToString()}:");
|
||||
logger.Debug($" Original Data : {BitConverter.ToString(data).Replace("-", " ")}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Handle Package
|
||||
var udpData = RecordUDPData(bytes, remoteEP);
|
||||
PrintData(udpData);
|
||||
|
||||
BEGIN_RECEIVE:
|
||||
listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
|
||||
var udpDataObj = await RecordUDPData(data, endPoint, time, Convert.ToInt32(data[1 + 4]));
|
||||
// PrintData(udpDataObj);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Got Error when handle receive:{e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool SendBytes(IPEndPoint endPoint, byte[] buf)
|
||||
{
|
||||
var sendLen = listener.Send(buf, endPoint);
|
||||
|
||||
if (sendLen == buf.Length) { return true; }
|
||||
else { return false; }
|
||||
}
|
||||
|
||||
private bool SendString(IPEndPoint endPoint, string text)
|
||||
{
|
||||
byte[] buf = Encoding.ASCII.GetBytes(text);
|
||||
var sendLen = listener.Send(buf, endPoint);
|
||||
|
||||
if (sendLen == buf.Length) { return true; }
|
||||
else { return false; }
|
||||
}
|
||||
|
||||
private UDPData RecordUDPData(byte[] bytes, IPEndPoint remoteEP)
|
||||
private async Task<UDPData> RecordUDPData(byte[] bytes, IPEndPoint remoteEP, DateTime time, int taskID)
|
||||
{
|
||||
var remoteAddress = remoteEP.Address.ToString();
|
||||
var remotePort = remoteEP.Port;
|
||||
var data = new UDPData()
|
||||
{
|
||||
DateTime = time,
|
||||
Timestamp = Number.BytesToUInt32(bytes[..4]).Value,
|
||||
Address = remoteAddress,
|
||||
Port = remotePort,
|
||||
TaskID = taskID,
|
||||
Data = bytes,
|
||||
DateTime = DateTime.Now,
|
||||
HasRead = false,
|
||||
};
|
||||
|
||||
using (udpData.AcquireWriteLock())
|
||||
var key = $"{remoteAddress}-{taskID}";
|
||||
|
||||
try
|
||||
{
|
||||
// Record UDP Receive Data
|
||||
if (udpData.ContainsKey(remoteAddress) && udpData.TryGetValue(remoteAddress, out var dataQueue))
|
||||
using (await udpDataLock.AcquireWriteLockAsync(TimeSpan.FromMilliseconds(5000)))
|
||||
{
|
||||
dataQueue.Enqueue(data);
|
||||
logger.Trace("Receive data from old client");
|
||||
}
|
||||
else
|
||||
{
|
||||
var queue = new Queue<UDPData>();
|
||||
queue.Enqueue(data);
|
||||
udpData.Add(remoteAddress, queue);
|
||||
logger.Trace("Receive data from new client");
|
||||
var sortedList = udpData.GetOrAdd(key, _ => new SortedList<UInt32, UDPData>());
|
||||
|
||||
// 处理相同时间戳的情况,添加微小的时间差
|
||||
var uniqueTime = data.Timestamp;
|
||||
while (sortedList.ContainsKey(uniqueTime))
|
||||
{
|
||||
logger.Warn(
|
||||
$"Duplicate timestamp detected for {remoteAddress}:{remotePort} at {uniqueTime}.");
|
||||
uniqueTime += 1;
|
||||
}
|
||||
|
||||
sortedList.Add(uniqueTime, data);
|
||||
// 输出单个数据
|
||||
// PrintData(data);
|
||||
}
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
logger.Error($"Failed to acquire write lock for recording UDP data from {remoteAddress}:{remotePort}");
|
||||
throw;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -345,21 +491,12 @@ public class UDPServer
|
||||
/// 输出UDP Data到log中
|
||||
/// </summary>
|
||||
/// <param name="data">UDP数据</param>
|
||||
public void PrintData(UDPData data)
|
||||
public string PrintData(UDPData data)
|
||||
{
|
||||
var bytes = data.Data;
|
||||
var sign = bytes[0];
|
||||
var sign = bytes[4];
|
||||
string recvData = "";
|
||||
if (sign == (byte)WebProtocol.PackSign.SendAddr)
|
||||
{
|
||||
var resData = WebProtocol.SendAddrPackage.FromBytes(bytes);
|
||||
if (resData.IsSuccessful)
|
||||
recvData = resData.Value.ToString();
|
||||
else
|
||||
recvData = resData.Error.ToString();
|
||||
}
|
||||
else if (sign == (byte)WebProtocol.PackSign.SendData) { }
|
||||
else if (sign == (byte)WebProtocol.PackSign.RecvData)
|
||||
if (sign == (byte)WebProtocol.PackSign.RecvData)
|
||||
{
|
||||
var resData = WebProtocol.RecvDataPackage.FromBytes(bytes);
|
||||
if (resData.IsSuccessful)
|
||||
@@ -380,49 +517,75 @@ public class UDPServer
|
||||
recvData = Encoding.ASCII.GetString(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
logger.Debug($"Receive Data from {data.Address}:{data.Port} at {data.DateTime.ToString()}:");
|
||||
logger.Debug($"Receive Data from {data.Address}:{data.Port} at {data.DateTime.ToString()} - {data.Timestamp}:");
|
||||
logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
|
||||
if (recvData.Length != 0) logger.Debug($" Decoded Data : {recvData}");
|
||||
return $@"
|
||||
Receive Data from {data.Address}:{data.Port} at {data.DateTime.ToString()}:
|
||||
Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}
|
||||
Decoded Data : {recvData}
|
||||
";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 输出UDP Data数组到log中
|
||||
/// </summary>
|
||||
/// <param name="dataArray">UDP数据列表</param>
|
||||
public void PrintDataArray(IEnumerable<UDPData> dataArray)
|
||||
{
|
||||
foreach (var data in dataArray)
|
||||
{
|
||||
logger.Debug(PrintData(data));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 将所有数据输出到log中
|
||||
/// </summary>
|
||||
/// <returns> void </returns>
|
||||
public void PrintAllData()
|
||||
public async Task PrintAllDataAsync()
|
||||
{
|
||||
using (udpData.AcquireReadLock())
|
||||
{
|
||||
logger.Debug("Ready Data:");
|
||||
logger.Debug("Ready Data:");
|
||||
|
||||
foreach (var ip in udpData)
|
||||
try
|
||||
{
|
||||
using (await udpDataLock.AcquireReadLockAsync(TimeSpan.FromMilliseconds(5000)))
|
||||
{
|
||||
foreach (var data in ip.Value)
|
||||
foreach (var kvp in udpData)
|
||||
{
|
||||
logger.Debug(data.ToString());
|
||||
foreach (var data in kvp.Value.Values)
|
||||
{
|
||||
logger.Debug(PrintData(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
logger.Error("Failed to acquire read lock for printing all data");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空指定IP地址的数据
|
||||
/// </summary>
|
||||
/// <param name="ipAddr">IP地址</param>
|
||||
/// <param name="taskID">[TODO:parameter]</param>
|
||||
/// <returns>无</returns>
|
||||
public async Task ClearUDPData(string ipAddr)
|
||||
public void ClearUDPData(string ipAddr, int taskID)
|
||||
{
|
||||
using (await udpData.AcquireWriteLockAsync())
|
||||
var key = $"{ipAddr}-{taskID}";
|
||||
|
||||
using (udpDataLock.AcquireWriteLock())
|
||||
{
|
||||
if (udpData.ContainsKey(ipAddr) &&
|
||||
udpData.TryGetValue(ipAddr, out var dataQueue) &&
|
||||
dataQueue.Count > 0)
|
||||
if (udpData.TryGetValue(key, out var sortedList))
|
||||
{
|
||||
dataQueue.Clear();
|
||||
sortedList.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start UDP Server
|
||||
@@ -430,17 +593,57 @@ public class UDPServer
|
||||
/// <returns>None</returns>
|
||||
public void Start()
|
||||
{
|
||||
if (cancellationTokenSource != null && !cancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
logger.Warn("UDP Server is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
|
||||
try
|
||||
{
|
||||
this.listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
|
||||
foreach (var client in listeners)
|
||||
{
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用 CancellationToken 来取消接收操作
|
||||
var result = await client.ReceiveAsync(cancellationToken);
|
||||
_ = ReceiveHandler(result.Buffer, result.RemoteEndPoint, DateTime.Now);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
logger.Debug("UDP receive operation was cancelled");
|
||||
break;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
logger.Debug("UDP client was disposed");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
logger.Error($"Error in UDP receive: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, cancellationToken));
|
||||
}
|
||||
|
||||
logger.Info("UDP Server started successfully");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isRunning = true;
|
||||
logger.Error($"Failed to start UDP server: {e}");
|
||||
cancellationTokenSource?.Cancel();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,8 +653,71 @@ public class UDPServer
|
||||
/// <returns>None</returns>
|
||||
public void Stop()
|
||||
{
|
||||
this.listener.Close();
|
||||
this.isRunning = false;
|
||||
if (cancellationTokenSource == null || cancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
logger.Warn("UDP Server is not running or already stopped");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
logger.Info("Stopping UDP Server...");
|
||||
|
||||
// 取消所有操作
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// 等待所有任务完成,设置超时时间
|
||||
var waitTasks = Task.WhenAll(tasks);
|
||||
if (!waitTasks.Wait(TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
logger.Warn("Some tasks did not complete within timeout period");
|
||||
}
|
||||
|
||||
// 关闭所有UDP客户端
|
||||
foreach (var client in listeners)
|
||||
{
|
||||
try
|
||||
{
|
||||
client.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Warn($"Error closing UDP client: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 清理任务列表
|
||||
tasks.Clear();
|
||||
|
||||
logger.Info("UDP Server stopped successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Error stopping UDP server: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
cancellationTokenSource?.Dispose();
|
||||
cancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实现IDisposable接口,确保资源正确释放
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!disposed)
|
||||
{
|
||||
Stop();
|
||||
|
||||
foreach (var client in listeners)
|
||||
{
|
||||
client?.Dispose();
|
||||
}
|
||||
|
||||
udpDataLock?.Dispose();
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Common;
|
||||
using DotNext;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@@ -35,32 +36,32 @@ namespace WebProtocol
|
||||
/// 突发类型
|
||||
/// </summary>
|
||||
/// <example>0</example>
|
||||
public BurstType BurstType { get; set; }
|
||||
public required BurstType BurstType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务ID
|
||||
/// </summary>
|
||||
/// <example>1</example>
|
||||
public byte CommandID { get; set; }
|
||||
public required byte CommandID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标识写入还是读取
|
||||
/// </summary>
|
||||
/// <example>true</example>
|
||||
public bool IsWrite { get; set; }
|
||||
public required bool IsWrite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 突发长度:0是32bits,255是32bits x 256
|
||||
/// </summary>
|
||||
/// <example>255</example>
|
||||
public byte BurstLength { get; set; }
|
||||
public required byte BurstLength { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 目标地址
|
||||
/// </summary>
|
||||
/// <example>0</example>
|
||||
public UInt32 Address { get; set; }
|
||||
public required UInt32 Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 转换为Json格式字符串
|
||||
@@ -84,23 +85,29 @@ namespace WebProtocol
|
||||
WriteResp
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 时间戳
|
||||
/// </summary>
|
||||
/// <example>1234567</example>
|
||||
public required UInt32 Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据包类型
|
||||
/// </summary>
|
||||
/// <example>0</example>
|
||||
public PackType Type { get; set; }
|
||||
public required PackType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Task ID
|
||||
/// </summary>
|
||||
/// <example>0</example>
|
||||
public byte CommandID { get; set; }
|
||||
public required byte CommandID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether is succeed to finish command
|
||||
/// </summary>
|
||||
/// <example>true</example>
|
||||
public bool IsSuccess { get; set; }
|
||||
public required bool IsSuccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return Data
|
||||
@@ -214,12 +221,14 @@ namespace WebProtocol
|
||||
/// <returns> 字符串 </returns>
|
||||
public override string ToString()
|
||||
{
|
||||
var opts = new SendAddrPackOptions();
|
||||
opts.BurstType = (BurstType)(commandType >> 6);
|
||||
opts.CommandID = Convert.ToByte((commandType >> 4) & 0b0011);
|
||||
opts.IsWrite = Convert.ToBoolean(commandType & 0x01);
|
||||
opts.BurstLength = burstLength;
|
||||
opts.Address = address;
|
||||
var opts = new SendAddrPackOptions()
|
||||
{
|
||||
BurstType = (BurstType)(commandType >> 6),
|
||||
CommandID = Convert.ToByte((commandType >> 4) & 0b0011),
|
||||
IsWrite = Convert.ToBoolean(commandType & 0x01),
|
||||
BurstLength = burstLength,
|
||||
Address = address,
|
||||
};
|
||||
|
||||
return JsonConvert.SerializeObject(opts);
|
||||
}
|
||||
@@ -269,6 +278,9 @@ namespace WebProtocol
|
||||
if (bodyData.Length > 256 * (32 / 8))
|
||||
throw new Exception("The data of SendDataPackage can't over 256 * 32bits");
|
||||
|
||||
if (bodyData.Length % 4 != 0)
|
||||
throw new Exception("The data of SendDataPackage should be divided by 4");
|
||||
|
||||
this.bodyData = bodyData;
|
||||
|
||||
_ = _reserved;
|
||||
@@ -294,8 +306,9 @@ namespace WebProtocol
|
||||
}
|
||||
|
||||
/// <summary> FPGA->Server 读响应包 </summary>
|
||||
public struct RecvDataPackage
|
||||
public class RecvDataPackage
|
||||
{
|
||||
readonly UInt32 timestamp;
|
||||
readonly byte sign = (byte)PackSign.RecvData;
|
||||
readonly byte commandID;
|
||||
readonly byte resp;
|
||||
@@ -306,11 +319,13 @@ namespace WebProtocol
|
||||
/// FPGA->Server 读响应包
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="timestamp"> 时间戳 </param>
|
||||
/// <param name="commandID"> 任务ID号 </param>
|
||||
/// <param name="resp"> 读响应包响应 </param>
|
||||
/// <param name="bodyData"> 数据 </param>
|
||||
public RecvDataPackage(byte commandID, byte resp, byte[] bodyData)
|
||||
public RecvDataPackage(UInt32 timestamp, byte commandID, byte resp, byte[] bodyData)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.commandID = commandID;
|
||||
this.resp = resp;
|
||||
this.bodyData = bodyData;
|
||||
@@ -319,26 +334,13 @@ namespace WebProtocol
|
||||
_ = this._reserved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FPGA->Server 读响应包
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="commandID"> 任务ID号 </param>
|
||||
/// <param name="isSuccess">是否读取成功</param>
|
||||
/// <param name="bodyData"> 数据 </param>
|
||||
public RecvDataPackage(byte commandID, bool isSuccess, byte[] bodyData)
|
||||
{
|
||||
this.commandID = commandID;
|
||||
this.resp = Convert.ToByte(isSuccess);
|
||||
this.bodyData = bodyData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过接受包选项构建读响应包
|
||||
/// </summary>
|
||||
/// <param name="opts">接收包(读响应包和写响应包)选项</param>
|
||||
public RecvDataPackage(RecvPackOptions opts)
|
||||
{
|
||||
this.timestamp = opts.Timestamp;
|
||||
this.commandID = opts.CommandID;
|
||||
this.resp = Convert.ToByte(opts.IsSuccess ? 0b10 : 0b00);
|
||||
this.bodyData = opts.Data ?? (byte[])[0, 0, 0, 0];
|
||||
@@ -351,11 +353,14 @@ namespace WebProtocol
|
||||
{
|
||||
get
|
||||
{
|
||||
var opts = new RecvPackOptions();
|
||||
opts.Type = RecvPackOptions.PackType.ReadResp;
|
||||
opts.CommandID = commandID;
|
||||
opts.IsSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? false : true);
|
||||
opts.Data = bodyData;
|
||||
var opts = new RecvPackOptions()
|
||||
{
|
||||
Timestamp = this.timestamp,
|
||||
Type = RecvPackOptions.PackType.ReadResp,
|
||||
CommandID = this.commandID,
|
||||
IsSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? false : true),
|
||||
Data = this.bodyData,
|
||||
};
|
||||
|
||||
return opts;
|
||||
}
|
||||
@@ -366,7 +371,7 @@ namespace WebProtocol
|
||||
/// </summary>
|
||||
public bool IsSuccessful
|
||||
{
|
||||
get { return Convert.ToBoolean((resp >> 1) == 0b01 ? false : true); }
|
||||
get { return Convert.ToBoolean((this.resp >> 1) == 0b01 ? false : true); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -376,12 +381,26 @@ namespace WebProtocol
|
||||
/// <returns>读响应包</returns>
|
||||
public static Result<RecvDataPackage> FromBytes(byte[] bytes)
|
||||
{
|
||||
if (bytes[0] != (byte)PackSign.RecvData)
|
||||
if (bytes[4] != (byte)PackSign.RecvData)
|
||||
return new(new ArgumentException(
|
||||
$"The sign of bytes is not RecvData Package, Sign: 0x{BitConverter.ToString([bytes[0]])}",
|
||||
nameof(bytes)
|
||||
));
|
||||
return new RecvDataPackage(bytes[1], bytes[2], bytes[4..]);
|
||||
return new RecvDataPackage(
|
||||
Number.BytesToUInt32(bytes[..4]).Value,
|
||||
bytes[5],
|
||||
bytes[6],
|
||||
bytes[8..]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="bytes">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public static bool IsRecvDataPackage(byte[] bytes)
|
||||
{
|
||||
return bytes[4] == (byte)PackSign.RecvData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -391,13 +410,16 @@ namespace WebProtocol
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var bodyDataLen = bodyData.Length;
|
||||
var arr = new byte[4 + bodyDataLen];
|
||||
var arr = new byte[8 + bodyDataLen];
|
||||
|
||||
arr[0] = this.sign;
|
||||
arr[1] = this.commandID;
|
||||
arr[2] = this.resp;
|
||||
Buffer.BlockCopy(
|
||||
Number.UInt32ArrayToBytes([this.timestamp]).Value, 0, arr, 0, 4);
|
||||
arr[4] = this.sign;
|
||||
arr[5] = this.commandID;
|
||||
arr[6] = this.resp;
|
||||
arr[7] = this.resp;
|
||||
|
||||
Array.Copy(bodyData, 0, arr, 4, bodyDataLen);
|
||||
Array.Copy(bodyData, 0, arr, 8, bodyDataLen);
|
||||
|
||||
return arr;
|
||||
}
|
||||
@@ -405,8 +427,9 @@ namespace WebProtocol
|
||||
}
|
||||
|
||||
/// <summary> 写响应包 </summary>
|
||||
public struct RecvRespPackage
|
||||
public class RecvRespPackage
|
||||
{
|
||||
readonly UInt32 timestamp;
|
||||
readonly byte sign = (byte)PackSign.RecvResp;
|
||||
readonly byte commandID;
|
||||
readonly byte resp;
|
||||
@@ -415,10 +438,12 @@ namespace WebProtocol
|
||||
/// <summary>
|
||||
/// 构建写响应包
|
||||
/// </summary>
|
||||
/// <param name="timestamp">时间戳</param>
|
||||
/// <param name="commandID">任务ID</param>
|
||||
/// <param name="resp">写响应</param>
|
||||
public RecvRespPackage(byte commandID, byte resp)
|
||||
public RecvRespPackage(UInt32 timestamp, byte commandID, byte resp)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.commandID = commandID;
|
||||
this.resp = resp;
|
||||
|
||||
@@ -426,23 +451,13 @@ namespace WebProtocol
|
||||
_ = this._reserved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建写响应包
|
||||
/// </summary>
|
||||
/// <param name="commandID">任务ID</param>
|
||||
/// <param name="isSuccess">是否写成功</param>
|
||||
public RecvRespPackage(byte commandID, bool isSuccess)
|
||||
{
|
||||
this.commandID = commandID;
|
||||
this.resp = Convert.ToByte(isSuccess);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过接受包选项构建写响应包
|
||||
/// </summary>
|
||||
/// <param name="opts">接收包(读响应包和写响应包)选项</param>
|
||||
public RecvRespPackage(RecvPackOptions opts)
|
||||
{
|
||||
this.timestamp = opts.Timestamp;
|
||||
this.commandID = opts.CommandID;
|
||||
this.resp = Convert.ToByte(opts.IsSuccess ? 0b10 : 0b00);
|
||||
}
|
||||
@@ -454,11 +469,14 @@ namespace WebProtocol
|
||||
{
|
||||
get
|
||||
{
|
||||
var opts = new RecvPackOptions();
|
||||
opts.Type = RecvPackOptions.PackType.WriteResp;
|
||||
opts.CommandID = commandID;
|
||||
opts.IsSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? false : true);
|
||||
opts.Data = null;
|
||||
var opts = new RecvPackOptions()
|
||||
{
|
||||
Timestamp = this.timestamp,
|
||||
Type = RecvPackOptions.PackType.WriteResp,
|
||||
CommandID = commandID,
|
||||
IsSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? false : true),
|
||||
Data = null,
|
||||
};
|
||||
|
||||
return opts;
|
||||
}
|
||||
@@ -469,7 +487,7 @@ namespace WebProtocol
|
||||
/// </summary>
|
||||
public bool IsSuccessful
|
||||
{
|
||||
get { return Convert.ToBoolean((resp >> 1) == 0b01 ? false : true); }
|
||||
get { return Convert.ToBoolean((this.resp >> 1) == 0b01 ? false : true); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -479,12 +497,23 @@ namespace WebProtocol
|
||||
/// <returns>写响应包</returns>
|
||||
public static Result<RecvRespPackage> FromBytes(byte[] bytes)
|
||||
{
|
||||
if (bytes[0] != (byte)PackSign.RecvResp)
|
||||
if (bytes[4] != (byte)PackSign.RecvResp)
|
||||
return new(new ArgumentException(
|
||||
$"The sign of bytes is not RecvResp Package, Sign: 0x{BitConverter.ToString([bytes[0]])}",
|
||||
$"The sign of bytes is not RecvResp Package, Sign: 0x{BitConverter.ToString([bytes[4]])}",
|
||||
nameof(bytes)
|
||||
));
|
||||
return new RecvRespPackage(bytes[1], bytes[2]);
|
||||
var timestamp = Number.BytesToUInt32(bytes[..4]).Value;
|
||||
return new RecvRespPackage(timestamp, bytes[5], bytes[6]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="bytes">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public static bool IsRecvRespPackage(byte[] bytes)
|
||||
{
|
||||
return bytes[4] == (byte)PackSign.RecvResp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -493,11 +522,13 @@ namespace WebProtocol
|
||||
/// <returns>字节数组</returns>
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var arr = new byte[4];
|
||||
|
||||
arr[0] = this.sign;
|
||||
arr[1] = this.commandID;
|
||||
arr[2] = this.resp;
|
||||
var arr = new byte[8];
|
||||
Buffer.BlockCopy(
|
||||
Number.UInt32ArrayToBytes([this.timestamp]).Value, 0, arr, 0, 4);
|
||||
arr[4] = this.sign;
|
||||
arr[5] = this.commandID;
|
||||
arr[6] = this.resp;
|
||||
arr[7] = this._reserved;
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
7843
src/APIClient.ts
7843
src/APIClient.ts
File diff suppressed because it is too large
Load Diff
14
src/App.vue
14
src/App.vue
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import Navbar from "./components/Navbar.vue";
|
||||
import Dialog from "./components/Dialog.vue";
|
||||
import { Alert, useAlertProvider } from "./components/Alert";
|
||||
import { ref, provide, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
@@ -49,19 +50,26 @@ provide("theme", {
|
||||
const currentRoutePath = computed(() => {
|
||||
return router.currentRoute.value.path;
|
||||
});
|
||||
|
||||
useAlertProvider();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<header class="relative">
|
||||
<Navbar></Navbar>
|
||||
<Dialog></Dialog>
|
||||
<Navbar />
|
||||
<Dialog />
|
||||
<Alert />
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<RouterView />
|
||||
</main>
|
||||
<footer v-if="currentRoutePath != '/project'" class="footer footer-center p-4 bg-base-300 text-base-content">
|
||||
|
||||
<footer
|
||||
v-if="currentRoutePath != '/project'"
|
||||
class="footer footer-center p-4 bg-base-300 text-base-content"
|
||||
>
|
||||
<div>
|
||||
<p>Copyright © 2023 - All right reserved by OurEDA</p>
|
||||
</div>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1741694797806" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2622" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M192 192m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" p-id="2623"></path><path d="M192 512m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" p-id="2624"></path><path d="M192 832m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" p-id="2625"></path><path d="M864 160H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 480H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32zM864 800H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h512c17.7 0 32-14.3 32-32s-14.3-32-32-32z" p-id="2626"></path></svg>
|
||||
|
Before Width: | Height: | Size: 870 B |
@@ -1 +0,0 @@
|
||||
<svg t="1741522876251" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3628" width="200" height="200"><path d="M327.04 85.333333h369.92C841.472 85.333333 938.666667 186.794667 938.666667 337.749333v348.501334C938.666667 837.205333 841.472 938.666667 696.874667 938.666667h-229.546667a32 32 0 0 1 0-64h229.546667c107.989333 0 177.792-73.941333 177.792-188.416V337.749333c0-114.474667-69.802667-188.416-177.749334-188.416H327.04C219.093333 149.333333 149.333333 223.274667 149.333333 337.749333v348.501334c0 114.474667 69.76 188.416 177.706667 188.416a32 32 0 0 1 0 64C182.442667 938.666667 85.333333 837.205333 85.333333 686.250667V337.749333C85.333333 186.794667 182.442667 85.333333 327.04 85.333333z m-51.114667 381.098667a31.914667 31.914667 0 0 1 42.325334-16.042667 31.914667 31.914667 0 0 1 16.042666 42.282667A47.061333 47.061333 0 1 0 424.192 512c0-25.898667-21.077333-46.933333-47.018667-46.933333a32 32 0 0 1 0-64c50.048 0 91.904 33.408 105.728 78.933333h242.858667a32 32 0 0 1 32 32v79.018667a32 32 0 0 1-64 0V544h-56.704v47.018667a32 32 0 0 1-64 0V544h-90.154667a110.72 110.72 0 0 1-105.728 79.018667 111.104 111.104 0 0 1-101.248-156.544z" fill="#200E32" p-id="3629"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg t="1741522263287" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2626" width="200" height="200"><path d="M511.913993 941.605241c-255.612968 0-385.311608-57.452713-385.311608-170.810012 0-80.846632 133.654964-133.998992 266.621871-151.88846L393.224257 602.049387c-79.986561-55.904586-118.86175-153.436587-118.86175-297.240383 0-139.33143 87.211154-222.586259 233.423148-222.586259l7.912649 0c146.211994 0 233.423148 83.254829 233.423148 222.586259 0 54.184445 0 214.67361-117.829666 297.412397l-0.344028 16.685369c132.966907 18.061482 266.105829 71.041828 266.105829 151.716445C897.225601 884.152528 767.526961 941.605241 511.913993 941.605241zM507.957668 141.567613c-79.470519 0-174.250294 28.382328-174.250294 163.241391 0 129.698639 34.230808 213.469511 104.584579 255.784982 8.944734 5.332437 14.277171 14.965228 14.277171 25.286074l0 59.344868c0 15.309256-11.524945 28.0383-26.662187 29.414413-144.319839 14.449185-239.959684 67.429531-239.959684 95.983874 0 92.199563 177.346548 111.637158 325.966739 111.637158 148.792206 0 325.966739-19.26558 325.966739-111.637158 0-28.726356-95.639845-81.534688-239.959684-95.983874-15.48127-1.548127-27.006215-14.621199-26.662187-30.102469l1.376113-59.344868c0.172014-10.148833 5.676466-19.437594 14.277171-24.770032 70.525785-42.487485 103.208466-123.678145 103.208466-255.784982 0-135.031077-94.779775-163.241391-174.250294-163.241391L507.957668 141.567613 507.957668 141.567613z" fill="#575B66" p-id="2627"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
103
src/components/Alert/Alert.vue
Normal file
103
src/components/Alert/Alert.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="fixed left-1/2 top-30 z-[9999] -translate-x-1/2">
|
||||
<transition
|
||||
name="alert"
|
||||
enter-active-class="alert-enter-active"
|
||||
leave-active-class="alert-leave-active"
|
||||
enter-from-class="alert-enter-from"
|
||||
enter-to-class="alert-enter-to"
|
||||
leave-from-class="alert-leave-from"
|
||||
leave-to-class="alert-leave-to"
|
||||
>
|
||||
<div
|
||||
v-if="alertStore?.alertState.value.visible"
|
||||
:class="alertClasses"
|
||||
class="alert"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Icons for different alert types -->
|
||||
<CheckCircle
|
||||
v-if="alertStore?.alertState.value.type === 'success'"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
/>
|
||||
<XCircle
|
||||
v-else-if="alertStore?.alertState.value.type === 'error'"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
/>
|
||||
<AlertTriangle
|
||||
v-else-if="alertStore?.alertState.value.type === 'warning'"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
/>
|
||||
<Info v-else class="h-6 w-6 shrink-0 stroke-current" />
|
||||
<span>{{ alertStore?.alertState.value.message }}</span>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-ghost"
|
||||
@click="alertStore?.hide"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { CheckCircle, XCircle, AlertTriangle, Info, X } from "lucide-vue-next";
|
||||
import { useAlertStore } from ".";
|
||||
import { useRequiredInjection } from "@/utils/Common";
|
||||
|
||||
const alertStore = useRequiredInjection(useAlertStore);
|
||||
|
||||
// Computed classes for different alert types
|
||||
const alertClasses = computed(() => {
|
||||
const baseClasses = "shadow-lg max-w-sm";
|
||||
|
||||
switch (alertStore.alertState.value.type) {
|
||||
case "success":
|
||||
return `${baseClasses} alert-success`;
|
||||
case "error":
|
||||
return `${baseClasses} alert-error`;
|
||||
case "warning":
|
||||
return `${baseClasses} alert-warning`;
|
||||
case "info":
|
||||
default:
|
||||
return `${baseClasses} alert-info`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 进入和离开的过渡动画持续时间 */
|
||||
.alert-enter-active,
|
||||
.alert-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
/* 进入的起始状态 */
|
||||
.alert-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
|
||||
/* 进入的结束状态 */
|
||||
.alert-enter-to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
/* 离开的起始状态 */
|
||||
.alert-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
/* 离开的结束状态 */
|
||||
.alert-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.98);
|
||||
}
|
||||
</style>
|
||||
82
src/components/Alert/index.ts
Normal file
82
src/components/Alert/index.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { ref, computed } from "vue";
|
||||
import Alert from "./Alert.vue";
|
||||
import { createInjectionState } from "@vueuse/core";
|
||||
|
||||
export interface AlertState {
|
||||
visible: boolean;
|
||||
message: string;
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
}
|
||||
|
||||
// create injectivon state using vueuse
|
||||
const [useAlertProvider, useAlertStore] = createInjectionState(() => {
|
||||
const alertState = ref<AlertState>({
|
||||
visible: false,
|
||||
message: "",
|
||||
type: "info",
|
||||
});
|
||||
|
||||
let timeoutId: number | null = null;
|
||||
|
||||
function show(
|
||||
message: string,
|
||||
type: AlertState["type"] = "info",
|
||||
duration = 2000,
|
||||
) {
|
||||
// Clear existing timeout
|
||||
if (timeoutId) {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
alertState.value = {
|
||||
visible: true,
|
||||
message,
|
||||
type,
|
||||
};
|
||||
|
||||
// Auto hide after duration
|
||||
if (duration > 0) {
|
||||
timeoutId = window.setTimeout(() => {
|
||||
hide();
|
||||
}, duration);
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
alertState.value.visible = false;
|
||||
if (timeoutId) {
|
||||
window.clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods for different alert types
|
||||
function error(message: string, duration = 2000) {
|
||||
show(message, "error", duration);
|
||||
}
|
||||
|
||||
function info(message: string, duration = 2000) {
|
||||
show(message, "info", duration);
|
||||
}
|
||||
|
||||
function warn(message: string, duration = 2000) {
|
||||
show(message, "warning", duration);
|
||||
}
|
||||
|
||||
function success(message: string, duration = 2000) {
|
||||
show(message, "success", duration);
|
||||
}
|
||||
|
||||
return {
|
||||
alertState,
|
||||
show,
|
||||
hide,
|
||||
error,
|
||||
info,
|
||||
warn,
|
||||
success,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
export { Alert, useAlertProvider, useAlertStore };
|
||||
94
src/components/InputField/BaseInputField.vue
Normal file
94
src/components/InputField/BaseInputField.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="form-control">
|
||||
<label class="label" v-if="label || icon">
|
||||
<component :is="icon" class="w-4 h-4" v-if="icon" />
|
||||
<span class="label-text" v-if="label">{{ label }}</span>
|
||||
</label>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:class="inputClasses"
|
||||
:value="modelValue"
|
||||
@input="handleInput"
|
||||
@blur="handleBlur"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<slot name="suffix"></slot>
|
||||
</div>
|
||||
|
||||
<label class="label" v-if="error">
|
||||
<span class="label-text-alt text-error">{{ error }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string | number
|
||||
label?: string
|
||||
placeholder?: string
|
||||
error?: string
|
||||
type?: 'text' | 'number' | 'email' | 'password'
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg'
|
||||
variant?: 'default' | 'bordered' | 'ghost'
|
||||
icon?: any
|
||||
disabled?: boolean
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'text',
|
||||
size: 'md',
|
||||
variant: 'bordered'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number]
|
||||
'blur': [event: FocusEvent]
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const inputClasses = computed(() => {
|
||||
const baseClasses = ['input', 'flex-1']
|
||||
|
||||
// 添加变体样式
|
||||
if (props.variant === 'bordered') baseClasses.push('input-bordered')
|
||||
else if (props.variant === 'ghost') baseClasses.push('input-ghost')
|
||||
|
||||
// 添加尺寸样式
|
||||
if (props.size === 'xs') baseClasses.push('input-xs')
|
||||
else if (props.size === 'sm') baseClasses.push('input-sm')
|
||||
else if (props.size === 'lg') baseClasses.push('input-lg')
|
||||
|
||||
// 添加错误样式
|
||||
if (props.error) baseClasses.push('input-error')
|
||||
|
||||
// 添加状态样式
|
||||
if (props.disabled) baseClasses.push('input-disabled')
|
||||
|
||||
return baseClasses
|
||||
})
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
let value: string | number = target.value
|
||||
|
||||
// 如果是数字类型,转换为数字
|
||||
if (props.type === 'number' && value !== '') {
|
||||
value = Number(value)
|
||||
}
|
||||
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const handleBlur = (event: FocusEvent) => {
|
||||
emit('blur', event)
|
||||
}
|
||||
</script>
|
||||
75
src/components/InputField/IpInputField.vue
Normal file
75
src/components/InputField/IpInputField.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<BaseInputField
|
||||
v-model="value"
|
||||
:label="label"
|
||||
:placeholder="placeholder || '192.168.1.100'"
|
||||
:error="validationError"
|
||||
:icon="icon || Globe"
|
||||
type="text"
|
||||
v-bind="$attrs"
|
||||
@blur="validateOnBlur"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { z } from 'zod'
|
||||
import { Globe } from 'lucide-vue-next'
|
||||
import BaseInputField from './BaseInputField.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
label?: string
|
||||
placeholder?: string
|
||||
icon?: any
|
||||
required?: boolean
|
||||
validateOnBlur?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
label: 'IP 地址',
|
||||
validateOnBlur: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const hasBlurred = ref(false)
|
||||
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// IP地址验证模式
|
||||
const ipSchema = z.string().ip({
|
||||
version: 'v4',
|
||||
message: '请输入有效的IPv4地址'
|
||||
})
|
||||
|
||||
const validationError = computed(() => {
|
||||
// 如果是必填且为空
|
||||
if (props.required && !props.modelValue) {
|
||||
return '请输入IP地址'
|
||||
}
|
||||
|
||||
// 如果有值但格式不正确,并且设置了在失焦时验证且已经失焦过
|
||||
if (props.modelValue && (!props.validateOnBlur || hasBlurred.value)) {
|
||||
const result = ipSchema.safeParse(props.modelValue)
|
||||
return result.success ? '' : result.error.errors[0]?.message || '无效的IP地址'
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
const validateOnBlur = () => {
|
||||
if (props.validateOnBlur) {
|
||||
hasBlurred.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
81
src/components/InputField/PortInputField.vue
Normal file
81
src/components/InputField/PortInputField.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<BaseInputField
|
||||
v-model="value"
|
||||
:label="label"
|
||||
:placeholder="placeholder || '8080'"
|
||||
:error="validationError"
|
||||
:icon="icon || Network"
|
||||
type="number"
|
||||
v-bind="$attrs"
|
||||
@blur="validateOnBlur"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { z } from 'zod'
|
||||
import { Network } from 'lucide-vue-next'
|
||||
import BaseInputField from './BaseInputField.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: number
|
||||
label?: string
|
||||
placeholder?: string
|
||||
icon?: any
|
||||
required?: boolean
|
||||
validateOnBlur?: boolean
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
label: '端口',
|
||||
validateOnBlur: true,
|
||||
min: 1,
|
||||
max: 65535
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number]
|
||||
}>()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const hasBlurred = ref(false)
|
||||
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', Number(val))
|
||||
})
|
||||
|
||||
// 端口验证模式
|
||||
const portSchema = computed(() =>
|
||||
z.number()
|
||||
.int('端口必须是整数')
|
||||
.min(props.min, `端口必须大于等于${props.min}`)
|
||||
.max(props.max, `端口必须小于等于${props.max}`)
|
||||
)
|
||||
|
||||
const validationError = computed(() => {
|
||||
// 如果是必填且为空
|
||||
if (props.required && (!props.modelValue && props.modelValue !== 0)) {
|
||||
return '请输入端口号'
|
||||
}
|
||||
|
||||
// 如果有值但格式不正确,并且设置了在失焦时验证且已经失焦过
|
||||
if ((props.modelValue || props.modelValue === 0) && (!props.validateOnBlur || hasBlurred.value)) {
|
||||
const result = portSchema.value.safeParse(props.modelValue)
|
||||
return result.success ? '' : result.error.errors[0]?.message || '无效的端口号'
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
const validateOnBlur = () => {
|
||||
if (props.validateOnBlur) {
|
||||
hasBlurred.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
3
src/components/InputField/index.ts
Normal file
3
src/components/InputField/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as BaseInputField } from './BaseInputField.vue'
|
||||
export { default as IpInputField } from './IpInputField.vue'
|
||||
export { default as PortInputField } from './PortInputField.vue'
|
||||
@@ -2,177 +2,105 @@
|
||||
<div>
|
||||
<!-- 元器件选择菜单 (Drawer) -->
|
||||
<div class="drawer drawer-end z-50">
|
||||
<input id="component-drawer" type="checkbox" class="drawer-toggle" v-model="showComponentsMenu" />
|
||||
<input
|
||||
id="component-drawer"
|
||||
type="checkbox"
|
||||
class="drawer-toggle"
|
||||
v-model="showComponentsMenu"
|
||||
/>
|
||||
<div class="drawer-side">
|
||||
<label for="component-drawer" aria-label="close sidebar" class="drawer-overlay !bg-opacity-50"></label>
|
||||
<div class="menu p-0 w-[460px] min-h-full bg-base-100 shadow-xl flex flex-col">
|
||||
<label
|
||||
for="component-drawer"
|
||||
aria-label="close sidebar"
|
||||
class="drawer-overlay !bg-opacity-50"
|
||||
></label>
|
||||
<div
|
||||
class="menu p-0 w-[460px] min-h-full bg-base-100 shadow-xl flex flex-col"
|
||||
>
|
||||
<!-- 菜单头部 -->
|
||||
<div class="p-6 border-b border-base-300 flex justify-between items-center">
|
||||
<div
|
||||
class="p-6 border-b bg-base-200 border-base-300 flex justify-between items-center"
|
||||
>
|
||||
<h3 class="text-xl font-bold text-primary flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="text-primary">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 8v8"></path>
|
||||
<path d="M8 12h8"></path>
|
||||
</svg>
|
||||
<Plus :size="20" class="text-primary" />
|
||||
添加元器件
|
||||
</h3>
|
||||
<label for="component-drawer" class="btn btn-ghost btn-sm btn-circle" @click="closeMenu">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</label>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm btn-circle"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<X :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<div class="tabs tabs-boxed bg-base-200 mx-6 mt-4 rounded-box">
|
||||
<a class="tab" :class="{ 'tab-active': activeTab === 'components' }"
|
||||
@click="activeTab = 'components'">元器件</a>
|
||||
<a class="tab" :class="{ 'tab-active': activeTab === 'templates' }" @click="activeTab = 'templates'">模板</a>
|
||||
<a class="tab" :class="{ 'tab-active': activeTab === 'virtual' }" @click="activeTab = 'virtual'">虚拟外设</a>
|
||||
<a
|
||||
class="tab"
|
||||
:class="{ 'tab-active': activeTab === 'components' }"
|
||||
@click="activeTab = 'components'"
|
||||
>元器件</a
|
||||
>
|
||||
<a
|
||||
class="tab"
|
||||
:class="{ 'tab-active': activeTab === 'templates' }"
|
||||
@click="activeTab = 'templates'"
|
||||
>模板</a
|
||||
>
|
||||
<a
|
||||
class="tab"
|
||||
:class="{ 'tab-active': activeTab === 'virtual' }"
|
||||
@click="activeTab = 'virtual'"
|
||||
>虚拟外设</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="px-6 py-4 border-b border-base-300">
|
||||
<div class="join w-full">
|
||||
<div class="join-item flex-1 relative">
|
||||
<input type="text" placeholder="搜索..." class="input input-bordered input-sm w-full pl-10"
|
||||
v-model="searchQuery" @keyup.enter="searchComponents" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content opacity-60">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<button class="btn btn-sm join-item" @click="searchComponents">
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 元器件列表 (组件选项卡) -->
|
||||
<div v-if="activeTab === 'components'" class="px-6 py-4 overflow-auto flex-1">
|
||||
<div v-if="filteredComponents.length > 0" class="grid grid-cols-2 gap-4">
|
||||
<div v-for="(component, index) in filteredComponents" :key="index"
|
||||
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
|
||||
@click="addComponent(component)">
|
||||
<div class="card-body p-3 items-center text-center">
|
||||
<div
|
||||
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2">
|
||||
<!-- 直接使用组件作为预览 -->
|
||||
<component v-if="componentModules[component.type]" :is="componentModules[component.type].default"
|
||||
class="component-preview" :size="getPreviewSize(component.type)" />
|
||||
<!-- 加载中状态 -->
|
||||
<span v-else class="text-xs text-gray-400">加载中...</span>
|
||||
</div>
|
||||
<h3 class="card-title text-sm mt-2">{{ component.name }}</h3>
|
||||
<p class="text-xs opacity-70">{{ component.type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 无搜索结果 -->
|
||||
<div v-else class="py-16 text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="mx-auto text-base-300 mb-3">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
<line x1="8" y1="11" x2="14" y2="11"></line>
|
||||
</svg>
|
||||
<p class="text-base-content opacity-70">没有找到匹配的元器件</p>
|
||||
<button class="btn btn-sm btn-ghost mt-3" @click="searchQuery = ''">
|
||||
清除搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模板列表 (模板选项卡) -->
|
||||
<div v-if="activeTab === 'templates'" class="px-6 py-4 overflow-auto flex-1">
|
||||
<div v-if="filteredTemplates.length > 0" class="grid grid-cols-2 gap-4">
|
||||
<div v-for="(template, index) in filteredTemplates" :key="index"
|
||||
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
|
||||
@click="addTemplate(template)">
|
||||
<div class="card-body p-3 items-center text-center">
|
||||
<div
|
||||
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2">
|
||||
<img :src="template.thumbnailUrl || '/placeholder-template.png'
|
||||
" alt="Template thumbnail" class="max-h-full max-w-full object-contain" />
|
||||
</div>
|
||||
<h3 class="card-title text-sm mt-2">{{ template.name }}</h3>
|
||||
<p class="text-xs opacity-70">
|
||||
{{ template.description || "模板" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 无搜索结果 -->
|
||||
<div v-else class="py-16 text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="mx-auto text-base-300 mb-3">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
<line x1="8" y1="11" x2="14" y2="11"></line>
|
||||
</svg>
|
||||
<p class="text-base-content opacity-70">没有找到匹配的模板</p>
|
||||
<button class="btn btn-sm btn-ghost mt-3" @click="searchQuery = ''">
|
||||
清除搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 虚拟外设列表 (虚拟外设选项卡) -->
|
||||
<div v-if="activeTab === 'virtual'" class="px-6 py-4 overflow-auto flex-1">
|
||||
<div v-if="filteredVirtualDevices.length > 0" class="grid grid-cols-2 gap-4">
|
||||
<div v-for="(device, index) in filteredVirtualDevices" :key="index"
|
||||
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
|
||||
@click="addComponent(device)">
|
||||
<div class="card-body p-3 items-center text-center">
|
||||
<div
|
||||
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2">
|
||||
<!-- 直接使用组件作为预览 -->
|
||||
<component v-if="componentModules[device.type]" :is="componentModules[device.type].default"
|
||||
class="component-preview" :size="getPreviewSize(device.type)" />
|
||||
<!-- 加载中状态 -->
|
||||
<span v-else class="text-xs text-gray-400">加载中...</span>
|
||||
</div>
|
||||
<h3 class="card-title text-sm mt-2">{{ device.name }}</h3>
|
||||
<p class="text-xs opacity-70">{{ device.type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 无搜索结果 -->
|
||||
<div v-else class="py-16 text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="mx-auto text-base-300 mb-3">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
<line x1="8" y1="11" x2="14" y2="11"></line>
|
||||
</svg>
|
||||
<p class="text-base-content opacity-70">没有找到匹配的虚拟外设</p>
|
||||
<button class="btn btn-sm btn-ghost mt-3" @click="searchQuery = ''">
|
||||
清除搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<div class="p-4 border-t border-base-300 bg-base-200 flex justify-between">
|
||||
<label for="component-drawer" class="btn btn-sm btn-ghost" @click="closeMenu">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
返回
|
||||
</label>
|
||||
<label for="component-drawer" class="btn btn-sm btn-primary" @click="closeMenu">
|
||||
完成
|
||||
<div class="px-6 py-4 w-full">
|
||||
<label class="input w-full">
|
||||
<Search :size="16" class="h-[1em] opacity-50" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索..."
|
||||
class="grow"
|
||||
v-model="searchQuery"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 统一的项目列表 -->
|
||||
<ItemList
|
||||
v-if="activeTab === 'components'"
|
||||
:items="availableComponents"
|
||||
:search-query="searchQuery"
|
||||
:component-modules="componentModules"
|
||||
:no-results-message="'没有找到匹配的元器件'"
|
||||
item-type="component"
|
||||
@item-click="addComponent"
|
||||
@clear-search="searchQuery = ''"
|
||||
/>
|
||||
|
||||
<ItemList
|
||||
v-if="activeTab === 'templates'"
|
||||
:items="availableTemplates"
|
||||
:search-query="searchQuery"
|
||||
:component-modules="componentModules"
|
||||
:no-results-message="'没有找到匹配的模板'"
|
||||
item-type="template"
|
||||
@item-click="addTemplate"
|
||||
@clear-search="searchQuery = ''"
|
||||
/>
|
||||
|
||||
<ItemList
|
||||
v-if="activeTab === 'virtual'"
|
||||
:items="availableVirtualDevices"
|
||||
:search-query="searchQuery"
|
||||
:component-modules="componentModules"
|
||||
:no-results-message="'没有找到匹配的虚拟外设'"
|
||||
item-type="virtual"
|
||||
@item-click="addComponent"
|
||||
@clear-search="searchQuery = ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,8 +109,19 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, shallowRef, onMounted } from "vue";
|
||||
import motherboardSvg from "@/components/equipments/svg/motherboard.svg";
|
||||
import buttonSvg from "@/components//equipments/svg/button.svg";
|
||||
import { Plus, X, Search } from "lucide-vue-next";
|
||||
import ItemList from "./ItemList.vue";
|
||||
import { useAlertStore } from "@/components/Alert";
|
||||
import {
|
||||
availableComponents,
|
||||
availableVirtualDevices,
|
||||
availableTemplates,
|
||||
getAllComponentTypes,
|
||||
type ComponentConfig,
|
||||
type VirtualDeviceConfig,
|
||||
type TemplateConfig,
|
||||
useComponentManager, // 导入 componentManager
|
||||
} from "./index.ts";
|
||||
|
||||
// Props 定义
|
||||
interface Props {
|
||||
@@ -191,59 +130,24 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
// 定义组件发出的事件
|
||||
// 定义组件发出的事件(保留部分必要的事件)
|
||||
const emit = defineEmits([
|
||||
"close",
|
||||
"add-component",
|
||||
"add-template",
|
||||
"update:open",
|
||||
]);
|
||||
|
||||
// 使用 componentManager
|
||||
const componentManager = useComponentManager();
|
||||
|
||||
// 使用 Alert 系统
|
||||
const alert = useAlertStore();
|
||||
|
||||
// 当前激活的选项卡
|
||||
const activeTab = ref("components");
|
||||
|
||||
// --- 搜索功能 ---
|
||||
const searchQuery = ref("");
|
||||
|
||||
// --- 可用元器件列表 ---
|
||||
const availableComponents = [
|
||||
{ type: "MechanicalButton", name: "机械按钮" },
|
||||
{ type: "Switch", name: "开关" },
|
||||
{ type: "Pin", name: "引脚" },
|
||||
{ type: "SMT_LED", name: "贴片LED" },
|
||||
{ type: "SevenSegmentDisplay", name: "数码管" },
|
||||
{ type: "HDMI", name: "HDMI接口" },
|
||||
{ type: "DDR", name: "DDR内存" },
|
||||
{ type: "ETH", name: "以太网接口" },
|
||||
{ type: "SD", name: "SD卡插槽" },
|
||||
{ type: "SFP", name: "SFP光纤模块" },
|
||||
{ type: "SMA", name: "SMA连接器" },
|
||||
{ type: "MotherBoard", name: "主板" },
|
||||
{ type: "PG2L100H_FBG676", name: "PG2L100H FBG676芯片" },
|
||||
{ type: "BaseBoard", name: "通用底板" },
|
||||
];
|
||||
|
||||
// --- 可用虚拟外设列表 ---
|
||||
const availableVirtualDevices = [{ type: "DDS", name: "信号发生器" }];
|
||||
|
||||
// --- 可用模板列表 ---
|
||||
const availableTemplates = ref([
|
||||
{
|
||||
name: "PG2L100H 基础开发板",
|
||||
id: "PG2L100H_Pango100pro",
|
||||
description: "包含主板和两个LED的基本设置",
|
||||
path: "/EquipmentTemplates/PG2L100H_Pango100pro.json",
|
||||
thumbnailUrl: motherboardSvg,
|
||||
},
|
||||
{
|
||||
name: "矩阵键盘",
|
||||
id: "MatrixKey",
|
||||
description: "包含4x4,共16个按键的矩阵键盘",
|
||||
path: "/EquipmentTemplates/MatrixKey.json",
|
||||
thumbnailUrl: buttonSvg,
|
||||
},
|
||||
]);
|
||||
|
||||
// 显示/隐藏组件菜单
|
||||
const showComponentsMenu = computed({
|
||||
get: () => props.open,
|
||||
@@ -277,62 +181,32 @@ async function loadComponentModule(type: string) {
|
||||
|
||||
// 预加载组件模块
|
||||
async function preloadComponentModules() {
|
||||
// 加载基础组件
|
||||
for (const component of availableComponents) {
|
||||
const allTypes = getAllComponentTypes();
|
||||
|
||||
for (const type of allTypes) {
|
||||
try {
|
||||
await loadComponentModule(component.type);
|
||||
await loadComponentModule(type);
|
||||
} catch (error) {
|
||||
console.error(`Failed to preload component ${component.type}:`, error);
|
||||
console.error(`Failed to preload component ${type}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载虚拟外设组件
|
||||
for (const device of availableVirtualDevices) {
|
||||
try {
|
||||
await loadComponentModule(device.type);
|
||||
} catch (error) {
|
||||
console.error(`Failed to preload virtual device ${device.type}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取组件预览时适合的尺寸
|
||||
function getPreviewSize(componentType: string): number {
|
||||
// 根据组件类型返回适当的预览尺寸
|
||||
const previewSizes: Record<string, number> = {
|
||||
MechanicalButton: 0.4, // 按钮较大,需要更小尺寸
|
||||
Switch: 0.35, // 开关较大,需要更小尺寸
|
||||
Pin: 0.8, // 引脚较小,可以大一些
|
||||
SMT_LED: 0.7, // LED可以保持适中
|
||||
SevenSegmentDisplay: 0.4, // 数码管较大,需要较小尺寸
|
||||
HDMI: 0.5, // HDMI接口较大
|
||||
DDR: 0.5, // DDR内存较大
|
||||
ETH: 0.5, // 以太网接口较大
|
||||
SD: 0.6, // SD卡插槽适中
|
||||
SFP: 0.4, // SFP光纤模块较大
|
||||
SMA: 0.7, // SMA连接器可以适中
|
||||
MotherBoard: 0.13, // 主板最大,需要最小尺寸
|
||||
DDS: 0.3, // 信号发生器较大,需要较小尺寸
|
||||
};
|
||||
|
||||
// 返回对应尺寸,如果没有特定配置则返回默认值0.5
|
||||
return previewSizes[componentType] || 0.5;
|
||||
}
|
||||
|
||||
// 搜索组件
|
||||
function searchComponents() {
|
||||
// 根据用户输入过滤可用组件列表
|
||||
// 实际逻辑已经在 filteredComponents 计算属性中实现
|
||||
}
|
||||
|
||||
// 关闭菜单
|
||||
function closeMenu() {
|
||||
showComponentsMenu.value = false;
|
||||
emit("update:open", false);
|
||||
emit("close");
|
||||
}
|
||||
|
||||
// 添加新元器件
|
||||
async function addComponent(componentTemplate: { type: string; name: string }) {
|
||||
// 添加新元器件 - 使用 componentManager
|
||||
async function addComponent(
|
||||
componentTemplate: ComponentConfig | VirtualDeviceConfig,
|
||||
) {
|
||||
if (!componentManager) {
|
||||
console.error("ComponentManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// 先加载组件模块
|
||||
const moduleRef = await loadComponentModule(componentTemplate.type);
|
||||
let defaultProps: Record<string, any> = {};
|
||||
@@ -353,19 +227,32 @@ async function addComponent(componentTemplate: { type: string; name: string }) {
|
||||
console.log(`Failed to load module for ${componentTemplate.type}`);
|
||||
}
|
||||
|
||||
// 发送添加组件事件给父组件
|
||||
emit("add-component", {
|
||||
type: componentTemplate.type,
|
||||
name: componentTemplate.name,
|
||||
props: defaultProps,
|
||||
});
|
||||
try {
|
||||
// 使用 componentManager 添加组件
|
||||
await componentManager.addComponent({
|
||||
type: componentTemplate.type,
|
||||
name: componentTemplate.name,
|
||||
props: defaultProps,
|
||||
});
|
||||
|
||||
// 关闭菜单
|
||||
closeMenu();
|
||||
// 显示成功消息
|
||||
alert?.success(`成功添加元器件: ${componentTemplate.name}`);
|
||||
|
||||
// 关闭菜单
|
||||
closeMenu();
|
||||
} catch (error) {
|
||||
console.error("添加元器件失败:", error);
|
||||
alert?.error("添加元器件失败,请检查控制台错误信息");
|
||||
}
|
||||
}
|
||||
|
||||
// 添加模板
|
||||
async function addTemplate(template: any) {
|
||||
// 添加模板 - 使用 componentManager
|
||||
async function addTemplate(template: TemplateConfig) {
|
||||
if (!componentManager) {
|
||||
console.error("ComponentManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 加载模板JSON文件
|
||||
const response = await fetch(template.path);
|
||||
@@ -376,62 +263,30 @@ async function addTemplate(template: any) {
|
||||
const templateData = await response.json();
|
||||
console.log("加载模板:", templateData);
|
||||
|
||||
// 发出事件,将模板数据传递给父组件
|
||||
emit("add-template", {
|
||||
// 使用 componentManager 添加模板
|
||||
const result = await componentManager.addTemplate({
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
template: templateData,
|
||||
capsPage: template.capsPage
|
||||
});
|
||||
|
||||
if (result) {
|
||||
// 使用 Alert 显示结果消息
|
||||
if (result.success) {
|
||||
alert?.success(result.message);
|
||||
} else {
|
||||
alert?.error(result.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭菜单
|
||||
closeMenu();
|
||||
} catch (error) {
|
||||
console.error("加载模板出错:", error);
|
||||
alert("无法加载模板文件,请检查控制台错误信息");
|
||||
alert?.error("无法加载模板文件,请检查控制台错误信息");
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤后的元器件列表 (用于菜单)
|
||||
const filteredComponents = computed(() => {
|
||||
if (!searchQuery.value || activeTab.value !== "components") {
|
||||
return availableComponents;
|
||||
}
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return availableComponents.filter(
|
||||
(component) =>
|
||||
component.name.toLowerCase().includes(query) ||
|
||||
component.type.toLowerCase().includes(query),
|
||||
);
|
||||
});
|
||||
|
||||
// 过滤后的模板列表 (用于菜单)
|
||||
const filteredTemplates = computed(() => {
|
||||
if (!searchQuery.value || activeTab.value !== "templates") {
|
||||
return availableTemplates.value;
|
||||
}
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return availableTemplates.value.filter(
|
||||
(template) =>
|
||||
template.name.toLowerCase().includes(query) ||
|
||||
(template.description &&
|
||||
template.description.toLowerCase().includes(query)),
|
||||
);
|
||||
});
|
||||
|
||||
// 过滤后的虚拟外设列表 (用于菜单)
|
||||
const filteredVirtualDevices = computed(() => {
|
||||
if (!searchQuery.value || activeTab.value !== "virtual") {
|
||||
return availableVirtualDevices;
|
||||
}
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return availableVirtualDevices.filter(
|
||||
(device) =>
|
||||
device.name.toLowerCase().includes(query) ||
|
||||
device.type.toLowerCase().includes(query),
|
||||
);
|
||||
});
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
// 预加载组件模块
|
||||
@@ -440,13 +295,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 组件预览样式 */
|
||||
.component-preview {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.animate-slideUp {
|
||||
animation: slideUp 0.3s ease-out forwards;
|
||||
|
||||
@@ -1,70 +1,100 @@
|
||||
<template>
|
||||
<div class="flex-1 h-full w-full bg-base-200 relative overflow-hidden diagram-container" ref="canvasContainer"
|
||||
@mousedown="handleCanvasMouseDown" @mousedown.middle.prevent="startMiddleDrag" @wheel.prevent="onZoom"
|
||||
@contextmenu.prevent="handleContextMenu">
|
||||
<div
|
||||
class="flex-1 h-full w-full bg-base-200 relative overflow-hidden diagram-container flex flex-col select-none"
|
||||
ref="canvasContainer"
|
||||
@mousedown="handleCanvasMouseDown"
|
||||
@mousedown.middle.prevent="startMiddleDrag"
|
||||
@wheel.prevent="onZoom"
|
||||
@contextmenu.prevent="handleContextMenu"
|
||||
>
|
||||
<!-- 工具栏 -->
|
||||
<div class="absolute top-2 right-2 flex gap-2 z-30">
|
||||
<button class="btn btn-sm btn-primary" @click="openDiagramFileSelector">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<FolderOpen class="h-4 w-4 mr-1" />
|
||||
导入
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" @click="exportDiagram">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
<Download class="h-4 w-4 mr-1" />
|
||||
导出
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" @click="emit('open-components')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<Plus class="h-4 w-4 mr-1" />
|
||||
添加组件
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary" @click="emit('toggle-doc-panel')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<FileText class="h-4 w-4 mr-1" />
|
||||
{{ props.showDocPanel ? "属性面板" : "文档" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input type="file" ref="fileInput" class="hidden" accept=".json" @change="handleFileSelected" />
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
class="hidden"
|
||||
accept=".json"
|
||||
@change="handleFileSelected"
|
||||
/>
|
||||
|
||||
<div ref="canvas" class="diagram-canvas" :style="{
|
||||
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
||||
}">
|
||||
<div
|
||||
ref="canvas"
|
||||
class="diagram-canvas relative select-none"
|
||||
:style="{
|
||||
transform: `translate(${componentManager.canvasPosition.x}px, ${componentManager.canvasPosition.y}px) scale(${componentManager.canvasScale.value})`,
|
||||
}"
|
||||
>
|
||||
<!-- 渲染连线 -->
|
||||
<svg class="wires-layer" width="10000" height="10000">
|
||||
<svg
|
||||
class="wires-layer absolute top-0 left-0 w-full h-full z-50"
|
||||
width="10000"
|
||||
height="10000"
|
||||
>
|
||||
<!-- 已完成的连线 -->
|
||||
<WireComponent v-for="(wire, index) in wireItems" :key="wire.id" :id="wire.id" :start-x="wire.startX"
|
||||
:start-y="wire.startY" :end-x="wire.endX" :end-y="wire.endY" :stroke-color="wire.color || '#4a5568'"
|
||||
:stroke-width="wire.strokeWidth" :is-active="false" :start-component-id="wire.startComponentId"
|
||||
:start-pin-id="wire.startPinId" :end-component-id="wire.endComponentId" :end-pin-id="wire.endPinId"
|
||||
:routing-mode="wire.routingMode" :path-commands="wire.pathCommands" />
|
||||
<WireComponent
|
||||
v-for="(wire, index) in wireItems"
|
||||
:key="wire.id"
|
||||
:id="wire.id"
|
||||
:start-x="wire.startX"
|
||||
:start-y="wire.startY"
|
||||
:end-x="wire.endX"
|
||||
:end-y="wire.endY"
|
||||
:stroke-color="wire.color || '#4a5568'"
|
||||
:stroke-width="wire.strokeWidth"
|
||||
:is-active="false"
|
||||
:start-component-id="wire.startComponentId"
|
||||
:start-pin-id="wire.startPinId"
|
||||
:end-component-id="wire.endComponentId"
|
||||
:end-pin-id="wire.endPinId"
|
||||
:routing-mode="wire.routingMode"
|
||||
:path-commands="wire.pathCommands"
|
||||
/>
|
||||
|
||||
<!-- 正在创建的连线 -->
|
||||
<WireComponent v-if="isCreatingWire" id="temp-wire" :start-x="creatingWireStart.x"
|
||||
:start-y="creatingWireStart.y" :end-x="mousePosition.x" :end-y="mousePosition.y" stroke-color="#3182ce"
|
||||
:stroke-width="2" :is-active="true" />
|
||||
<WireComponent
|
||||
v-if="isCreatingWire"
|
||||
id="temp-wire"
|
||||
:start-x="creatingWireStart.x"
|
||||
:start-y="creatingWireStart.y"
|
||||
:end-x="mousePosition.x"
|
||||
:end-y="mousePosition.y"
|
||||
stroke-color="#3182ce"
|
||||
:stroke-width="2"
|
||||
:is-active="true"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- 渲染画布上的组件 -->
|
||||
<div v-for="component in diagramParts" :key="component.id" class="component-wrapper" :class="{
|
||||
'component-hover': hoveredComponent === component.id,
|
||||
'component-selected': selectedComponentId === component.id,
|
||||
'component-disabled': !component.isOn,
|
||||
'component-hidepins': component.hidepins,
|
||||
}" :style="{
|
||||
<div
|
||||
v-for="component in diagramParts"
|
||||
:key="component.id"
|
||||
class="component-wrapper absolute p-0 inline-block overflow-visible select-none"
|
||||
:class="{
|
||||
'component-hover': hoveredComponent === component.id,
|
||||
'component-selected': selectedComponentId === component.id,
|
||||
'cursor-not-allowed grayscale-70 opacity-60': !component.isOn,
|
||||
'component-hidepins': component.hidepins,
|
||||
}"
|
||||
:style="{
|
||||
top: component.y + 'px',
|
||||
left: component.x + 'px',
|
||||
zIndex: component.index ?? 0,
|
||||
@@ -73,57 +103,70 @@
|
||||
: 'none',
|
||||
opacity: component.isOn ? 1 : 0.6,
|
||||
display: 'block',
|
||||
}" @mousedown.left.stop="startComponentDrag($event, component)" @mouseover="
|
||||
(event) => {
|
||||
hoveredComponent = component.id;
|
||||
}
|
||||
" @mouseleave="
|
||||
(event) => {
|
||||
hoveredComponent = null;
|
||||
}
|
||||
">
|
||||
}"
|
||||
@mousedown.left.stop="startComponentDrag($event, component)"
|
||||
@mouseover="hoveredComponent = component.id"
|
||||
@mouseleave="hoveredComponent = null"
|
||||
>
|
||||
<!-- 动态渲染组件 -->
|
||||
<component :is="componentManager.getComponentDefinition(component.type)"
|
||||
v-if="componentManager.componentModules.value[component.type] && componentManager.getComponentDefinition(component.type)"
|
||||
v-bind="componentManager.prepareComponentProps(component.attrs || {}, component.id)" @update:bindKey="
|
||||
<component
|
||||
:is="componentManager.getComponentDefinition(component.type)"
|
||||
v-if="
|
||||
componentManager.componentModules.value[component.type] &&
|
||||
componentManager.getComponentDefinition(component.type)
|
||||
"
|
||||
v-bind="
|
||||
componentManager.prepareComponentProps(
|
||||
component.attrs || {},
|
||||
component.id,
|
||||
props.examId,
|
||||
)
|
||||
"
|
||||
@update:bindKey="
|
||||
(value: string) =>
|
||||
updateComponentProp(component.id, 'bindKey', value)
|
||||
" @pin-click="
|
||||
"
|
||||
@pin-click="
|
||||
(pinInfo: any) =>
|
||||
handlePinClick(component.id, pinInfo, pinInfo.originalEvent)
|
||||
" :ref="(el: any) => componentManager.setComponentRef(component.id, el)" />
|
||||
"
|
||||
:ref="(el: any) => componentManager.setComponentRef(component.id, el)"
|
||||
/>
|
||||
|
||||
<!-- Fallback if component module not loaded yet -->
|
||||
<div v-else
|
||||
class="p-2 text-xs text-gray-400 border border-dashed border-gray-400 flex items-center justify-center">
|
||||
<div
|
||||
v-else
|
||||
class="p-2 text-xs text-gray-400 border border-dashed border-gray-400 flex items-center justify-center"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="loading loading-spinner loading-xs mb-1"></div>
|
||||
<span>Loading {{ component.type }}...</span>
|
||||
<small class="mt-1 text-xs">{{ componentManager.componentModules.value[component.type] ? 'Module loaded but invalid' : 'Module not found' }}</small>
|
||||
<small class="mt-1 text-xs">{{
|
||||
componentManager.componentModules.value[component.type]
|
||||
? "Module loaded but invalid"
|
||||
: "Module not found"
|
||||
}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通知组件 -->
|
||||
<div v-if="showNotification" class="toast toast-top toast-center z-50 w-fit-content">
|
||||
<div :class="`alert ${notificationType === 'success'
|
||||
? 'alert-success'
|
||||
: notificationType === 'error'
|
||||
? 'alert-error'
|
||||
: 'alert-info'
|
||||
}`">
|
||||
<span>{{ notificationMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 加载指示器 -->
|
||||
<div v-if="isLoading" class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="loading loading-spinner loading-lg text-primary"></div>
|
||||
</div>
|
||||
|
||||
<!-- 缩放指示器 -->
|
||||
<div class="absolute bottom-4 right-4 bg-base-100 px-3 py-1.5 rounded-md shadow-md z-20" style="opacity: 0.9">
|
||||
<span class="text-sm font-medium">{{ Math.round(scale * 100) }}%</span>
|
||||
<div
|
||||
class="absolute bottom-4 right-4 bg-base-100 px-3 py-1.5 rounded-md shadow-md z-20"
|
||||
style="opacity: 0.9"
|
||||
>
|
||||
<span class="text-sm font-medium"
|
||||
>{{ Math.round(componentManager?.canvasScale.value * 100) }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -133,13 +176,13 @@ import {
|
||||
ref,
|
||||
reactive,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
computed,
|
||||
watch,
|
||||
provide,
|
||||
} from "vue";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import { FolderOpen, Download, Plus, FileText } from "lucide-vue-next";
|
||||
import WireComponent from "@/components/equipments/Wire.vue";
|
||||
import { useAlertStore } from "@/components/Alert";
|
||||
|
||||
// 导入 diagram 管理器
|
||||
import {
|
||||
@@ -147,8 +190,6 @@ import {
|
||||
saveDiagramData,
|
||||
updatePartPosition,
|
||||
updatePartAttribute,
|
||||
deletePart,
|
||||
deleteConnection,
|
||||
parseConnectionPin,
|
||||
connectionArrayToWireItem,
|
||||
validateDiagramData,
|
||||
@@ -170,30 +211,28 @@ function handleContextMenu(e: MouseEvent) {
|
||||
}
|
||||
|
||||
// 定义组件发出的事件
|
||||
const emit = defineEmits([
|
||||
"diagram-updated",
|
||||
"toggle-doc-panel",
|
||||
"wire-created",
|
||||
"wire-deleted",
|
||||
"open-components",
|
||||
]);
|
||||
const emit = defineEmits(["toggle-doc-panel", "open-components"]);
|
||||
|
||||
// 定义组件接受的属性
|
||||
const props = defineProps<{
|
||||
showDocPanel?: boolean; // 添加属性接收文档面板的显示状态
|
||||
examId?: string; // 新增examId属性
|
||||
}>();
|
||||
|
||||
// 获取componentManager实例
|
||||
const componentManager = useComponentManager();
|
||||
if (!componentManager) {
|
||||
throw new Error("DiagramCanvas must be used within a component manager provider");
|
||||
throw new Error(
|
||||
"DiagramCanvas must be used within a component manager provider",
|
||||
);
|
||||
}
|
||||
|
||||
// 获取Alert store实例
|
||||
const alertStore = useAlertStore();
|
||||
|
||||
// --- 画布状态 ---
|
||||
const canvasContainer = ref<HTMLElement | null>(null);
|
||||
const canvas = ref<HTMLElement | null>(null);
|
||||
const position = reactive({ x: 0, y: 0 });
|
||||
const scale = ref(1);
|
||||
const isDragging = ref(false);
|
||||
const isMiddleDragging = ref(false);
|
||||
const dragStart = reactive({ x: 0, y: 0 });
|
||||
@@ -214,9 +253,6 @@ const diagramData = ref<DiagramData>({
|
||||
connections: [],
|
||||
});
|
||||
|
||||
// 组件引用跟踪(保留以便向后兼容)
|
||||
const componentRefs = computed(() => componentManager?.componentRefs.value || {});
|
||||
|
||||
// 计算属性:从 diagramData 中提取组件列表,并按index属性排序
|
||||
const diagramParts = computed<DiagramPart[]>(() => {
|
||||
// 克隆原始数组以避免直接修改原始数据
|
||||
@@ -320,13 +356,6 @@ const mousePosition = reactive({ x: 0, y: 0 });
|
||||
// 加载状态
|
||||
const isLoading = ref(false);
|
||||
|
||||
// 通知状态
|
||||
const showNotification = ref(false);
|
||||
const notificationMessage = ref("");
|
||||
const notificationType = ref<"success" | "error" | "info">("info");
|
||||
// 保存toast定时器ID
|
||||
const toastTimers: number[] = [];
|
||||
|
||||
// 文件选择引用
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
@@ -337,9 +366,9 @@ const isWireCreationEventActive = ref(false);
|
||||
|
||||
// 使用VueUse设置事件监听器
|
||||
// 画布拖拽事件
|
||||
useEventListener(document, 'mousemove', (e: MouseEvent) => {
|
||||
useEventListener(document, "mousemove", (e: MouseEvent) => {
|
||||
if (isDragEventActive.value) {
|
||||
onDrag(e);
|
||||
onCanvasDrag(e);
|
||||
}
|
||||
if (isComponentDragEventActive.value) {
|
||||
onComponentDrag(e);
|
||||
@@ -349,7 +378,7 @@ useEventListener(document, 'mousemove', (e: MouseEvent) => {
|
||||
}
|
||||
});
|
||||
|
||||
useEventListener(document, 'mouseup', () => {
|
||||
useEventListener(document, "mouseup", () => {
|
||||
if (isDragEventActive.value) {
|
||||
stopDrag();
|
||||
}
|
||||
@@ -359,7 +388,7 @@ useEventListener(document, 'mouseup', () => {
|
||||
});
|
||||
|
||||
// 键盘事件
|
||||
useEventListener(window, 'keydown', handleKeyDown);
|
||||
useEventListener(window, "keydown", handleKeyDown);
|
||||
|
||||
// --- 缩放功能 ---
|
||||
const MIN_SCALE = 0.2;
|
||||
@@ -377,25 +406,13 @@ function onZoom(e: WheelEvent) {
|
||||
const mouseX = e.clientX - containerRect.left;
|
||||
const mouseY = e.clientY - containerRect.top;
|
||||
|
||||
// 计算鼠标在画布坐标系中的位置
|
||||
const mouseXCanvas = (mouseX - position.x) / scale.value;
|
||||
const mouseYCanvas = (mouseY - position.y) / scale.value;
|
||||
|
||||
// 计算缩放值
|
||||
const zoomFactor = 1.1; // 每次放大/缩小10%
|
||||
const direction = e.deltaY > 0 ? -1 : 1;
|
||||
const finalZoomFactor = direction > 0 ? zoomFactor : 1 / zoomFactor;
|
||||
|
||||
// 计算新的缩放值
|
||||
let newScale =
|
||||
direction > 0 ? scale.value * zoomFactor : scale.value / zoomFactor;
|
||||
newScale = Math.max(MIN_SCALE, Math.min(newScale, MAX_SCALE));
|
||||
|
||||
// 计算新的位置,使鼠标指针位置在缩放前后保持不变
|
||||
position.x = mouseX - mouseXCanvas * newScale;
|
||||
position.y = mouseY - mouseYCanvas * newScale;
|
||||
|
||||
// 更新缩放值
|
||||
scale.value = newScale;
|
||||
// 使用componentManager的缩放方法
|
||||
componentManager?.zoomAtPosition(mouseX, mouseY, finalZoomFactor);
|
||||
}
|
||||
|
||||
// --- 画布交互逻辑 ---
|
||||
@@ -426,8 +443,10 @@ function startDrag(e: MouseEvent) {
|
||||
|
||||
isDragging.value = true;
|
||||
isMiddleDragging.value = false;
|
||||
dragStart.x = e.clientX - position.x;
|
||||
dragStart.y = e.clientY - position.y;
|
||||
const currentPosition = componentManager?.getCanvasPosition();
|
||||
if (!currentPosition) return;
|
||||
dragStart.x = e.clientX - currentPosition.x;
|
||||
dragStart.y = e.clientY - currentPosition.y;
|
||||
|
||||
isDragEventActive.value = true;
|
||||
e.preventDefault();
|
||||
@@ -441,19 +460,24 @@ function startMiddleDrag(e: MouseEvent) {
|
||||
isDragging.value = false;
|
||||
draggingComponentId.value = null;
|
||||
|
||||
dragStart.x = e.clientX - position.x;
|
||||
dragStart.y = e.clientY - position.y;
|
||||
const currentPosition = componentManager?.getCanvasPosition();
|
||||
if (!currentPosition) return;
|
||||
dragStart.x = e.clientX - currentPosition.x;
|
||||
dragStart.y = e.clientY - currentPosition.y;
|
||||
|
||||
isDragEventActive.value = true;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// 拖拽画布过程
|
||||
function onDrag(e: MouseEvent) {
|
||||
function onCanvasDrag(e: MouseEvent) {
|
||||
if (!isDragging.value && !isMiddleDragging.value) return;
|
||||
|
||||
position.x = e.clientX - dragStart.x;
|
||||
position.y = e.clientY - dragStart.y;
|
||||
const newX = e.clientX - dragStart.x;
|
||||
const newY = e.clientY - dragStart.y;
|
||||
|
||||
// 使用componentManager设置画布位置
|
||||
componentManager?.setCanvasPosition(newX, newY);
|
||||
}
|
||||
|
||||
// 停止拖拽画布
|
||||
@@ -505,15 +529,16 @@ function startComponentDrag(e: MouseEvent, component: DiagramPart) {
|
||||
if (!canvasContainer.value) return;
|
||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||
|
||||
// 计算鼠标在画布坐标系中的位置
|
||||
const mouseX_canvas =
|
||||
(e.clientX - containerRect.left - position.x) / scale.value;
|
||||
const mouseY_canvas =
|
||||
(e.clientY - containerRect.top - position.y) / scale.value;
|
||||
// 使用componentManager的屏幕坐标转画布坐标方法
|
||||
const mouseCanvasPos = componentManager?.screenToCanvas(
|
||||
e.clientX - containerRect.left,
|
||||
e.clientY - containerRect.top,
|
||||
);
|
||||
if (!mouseCanvasPos) return;
|
||||
|
||||
// 计算鼠标相对于组件左上角的偏移量
|
||||
componentDragOffset.x = mouseX_canvas - component.x;
|
||||
componentDragOffset.y = mouseY_canvas - component.y;
|
||||
componentDragOffset.x = mouseCanvasPos.x - component.x;
|
||||
componentDragOffset.y = mouseCanvasPos.y - component.y;
|
||||
|
||||
// 激活组件拖拽事件监听
|
||||
isComponentDragEventActive.value = true;
|
||||
@@ -529,15 +554,16 @@ function onComponentDrag(e: MouseEvent) {
|
||||
|
||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||
|
||||
// 计算鼠标在画布坐标系中的位置
|
||||
const mouseX_canvas =
|
||||
(e.clientX - containerRect.left - position.x) / scale.value;
|
||||
const mouseY_canvas =
|
||||
(e.clientY - containerRect.top - position.y) / scale.value;
|
||||
// 使用componentManager的屏幕坐标转画布坐标方法
|
||||
const mouseCanvasPos = componentManager?.screenToCanvas(
|
||||
e.clientX - containerRect.left,
|
||||
e.clientY - containerRect.top,
|
||||
);
|
||||
if (!mouseCanvasPos) return;
|
||||
|
||||
// 计算组件新位置
|
||||
const newX = mouseX_canvas - componentDragOffset.x;
|
||||
const newY = mouseY_canvas - componentDragOffset.y;
|
||||
const newX = mouseCanvasPos.x - componentDragOffset.x;
|
||||
const newY = mouseCanvasPos.y - componentDragOffset.y;
|
||||
|
||||
// 获取当前拖动的组件
|
||||
const draggedComponent = diagramParts.value.find(
|
||||
@@ -554,8 +580,7 @@ function onComponentDrag(e: MouseEvent) {
|
||||
const groupComponents = diagramParts.value.filter(
|
||||
(p) =>
|
||||
p.group === draggedComponent.group &&
|
||||
p.id !== draggingComponentId.value &&
|
||||
!p.positionlock,
|
||||
p.id !== draggingComponentId.value,
|
||||
);
|
||||
|
||||
// 更新这些组件的位置
|
||||
@@ -577,25 +602,18 @@ function onComponentDrag(e: MouseEvent) {
|
||||
y: Math.round(newY),
|
||||
});
|
||||
}
|
||||
|
||||
// 通知图表已更新
|
||||
emit("diagram-updated", diagramData.value);
|
||||
}
|
||||
|
||||
// 停止拖拽组件
|
||||
function stopComponentDrag() {
|
||||
// 如果有组件被拖拽,保存当前状态
|
||||
if (draggingComponentId.value) {
|
||||
console.log(`组件拖拽结束: ${draggingComponentId.value}`);
|
||||
|
||||
// 保存图表数据
|
||||
saveDiagramData(diagramData.value);
|
||||
|
||||
// 清除拖动状态
|
||||
draggingComponentId.value = null;
|
||||
}
|
||||
|
||||
isComponentDragEventActive.value = false;
|
||||
|
||||
saveDiagramData(diagramData.value);
|
||||
}
|
||||
|
||||
// 更新组件属性
|
||||
@@ -615,8 +633,6 @@ function updateComponentProp(
|
||||
propName,
|
||||
value,
|
||||
);
|
||||
emit("diagram-updated", diagramData.value);
|
||||
saveDiagramData(diagramData.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,9 +641,19 @@ function updateComponentProp(
|
||||
function updateMousePosition(e: MouseEvent) {
|
||||
if (!canvasContainer.value) return;
|
||||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||
mousePosition.x = (e.clientX - containerRect.left - position.x) / scale.value;
|
||||
mousePosition.y = (e.clientY - containerRect.top - position.y) / scale.value;
|
||||
} // 处理引脚点击
|
||||
|
||||
// 使用componentManager的屏幕坐标转画布坐标方法
|
||||
const canvasPos = componentManager?.screenToCanvas(
|
||||
e.clientX - containerRect.left,
|
||||
e.clientY - containerRect.top,
|
||||
);
|
||||
if (!canvasPos) return;
|
||||
|
||||
mousePosition.x = canvasPos.x;
|
||||
mousePosition.y = canvasPos.y;
|
||||
}
|
||||
|
||||
// 处理引脚点击
|
||||
function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||
if (!canvasContainer.value) return;
|
||||
updateMousePosition(event);
|
||||
@@ -784,13 +810,6 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
|
||||
connections: [...diagramData.value.connections, newConnection],
|
||||
};
|
||||
|
||||
// 通知连线创建
|
||||
emit("wire-created", newConnection);
|
||||
emit("diagram-updated", diagramData.value);
|
||||
|
||||
// 保存图表数据
|
||||
saveDiagramData(diagramData.value);
|
||||
|
||||
// 重置连线创建状态
|
||||
resetWireCreation();
|
||||
isWireCreationEventActive.value = false;
|
||||
@@ -834,23 +853,12 @@ function onCreatingWireMouseMove(e: MouseEvent) {
|
||||
updateMousePosition(e);
|
||||
}
|
||||
|
||||
// 删除连线
|
||||
function deleteWire(wireIndex: number) {
|
||||
diagramData.value = deleteConnection(diagramData.value, wireIndex);
|
||||
emit("wire-deleted", wireIndex);
|
||||
emit("diagram-updated", diagramData.value);
|
||||
saveDiagramData(diagramData.value);
|
||||
}
|
||||
|
||||
// 删除组件
|
||||
function deleteComponent(componentId: string) {
|
||||
diagramData.value = deletePart(diagramData.value, componentId);
|
||||
// 直接通过componentManager删除组件
|
||||
if (componentManager) {
|
||||
componentManager.deleteComponent(componentId);
|
||||
}
|
||||
emit("diagram-updated", diagramData.value);
|
||||
saveDiagramData(diagramData.value);
|
||||
|
||||
// 清除选中状态
|
||||
if (selectedComponentId.value === componentId) {
|
||||
@@ -893,7 +901,7 @@ function handleFileSelected(event: Event) {
|
||||
const validation = validateDiagramData(parsed);
|
||||
|
||||
if (!validation.isValid) {
|
||||
showToast(
|
||||
alertStore?.show(
|
||||
`不是有效的diagram.json格式: ${validation.errors.join("; ")}`,
|
||||
"error",
|
||||
);
|
||||
@@ -904,17 +912,11 @@ function handleFileSelected(event: Event) {
|
||||
// 更新画布数据
|
||||
diagramData.value = parsed as DiagramData;
|
||||
|
||||
// 保存到本地文件
|
||||
saveDiagramData(diagramData.value);
|
||||
|
||||
// 发出更新事件
|
||||
emit("diagram-updated", diagramData.value);
|
||||
|
||||
showToast(`成功导入diagram文件`, "success");
|
||||
alertStore?.show(`成功导入diagram文件`, "success");
|
||||
} catch (error) {
|
||||
console.error("解析JSON文件出错:", error);
|
||||
if (document.body.contains(canvasContainer.value)) {
|
||||
showToast("解析文件出错,请确认是有效的JSON格式", "error");
|
||||
alertStore?.show("解析文件出错,请确认是有效的JSON格式", "error");
|
||||
}
|
||||
} finally {
|
||||
// 检查组件是否仍然挂载
|
||||
@@ -930,7 +932,7 @@ function handleFileSelected(event: Event) {
|
||||
reader.onerror = () => {
|
||||
// 检查组件是否仍然挂载
|
||||
if (document.body.contains(canvasContainer.value)) {
|
||||
showToast("读取文件时出错", "error");
|
||||
alertStore?.show("读取文件时出错", "error");
|
||||
isLoading.value = false;
|
||||
}
|
||||
// 清除文件输入
|
||||
@@ -956,69 +958,27 @@ function exportDiagram() {
|
||||
a.download = "diagram.json";
|
||||
a.click();
|
||||
// 释放URL对象
|
||||
const timerId = setTimeout(() => {
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
// 检查组件是否仍然挂载
|
||||
if (document.body.contains(canvasContainer.value)) {
|
||||
isLoading.value = false;
|
||||
showToast("成功导出diagram文件", "success");
|
||||
alertStore?.show("成功导出diagram文件", "success");
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 将定时器ID保存起来,以便在组件卸载时清除
|
||||
toastTimers.push(timerId);
|
||||
} catch (error) {
|
||||
console.error("导出diagram文件时出错:", error);
|
||||
showToast("导出diagram文件时出错", "error");
|
||||
alertStore?.show("导出diagram文件时出错", "error");
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
function showToast(
|
||||
message: string,
|
||||
type: "success" | "error" | "info" = "info",
|
||||
duration = 3000,
|
||||
) {
|
||||
notificationMessage.value = message;
|
||||
notificationType.value = type;
|
||||
showNotification.value = true;
|
||||
|
||||
// 保存定时器ID以便清除
|
||||
const timerId = setTimeout(() => {
|
||||
// 检查组件是否仍然挂载
|
||||
if (document.body.contains(canvasContainer.value)) {
|
||||
showNotification.value = false;
|
||||
}
|
||||
}, duration);
|
||||
|
||||
// 将定时器ID保存起来,以便在组件卸载时清除
|
||||
toastTimers.push(timerId);
|
||||
}
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(async () => {
|
||||
// 设置componentManager的画布引用
|
||||
if (componentManager) {
|
||||
// 创建一个包含必要方法的画布API对象
|
||||
const canvasAPI = {
|
||||
getDiagramData: () => diagramData.value,
|
||||
updateDiagramDataDirectly: (data: DiagramData) => {
|
||||
diagramData.value = data;
|
||||
saveDiagramData(data);
|
||||
emit("diagram-updated", data);
|
||||
},
|
||||
getCanvasPosition: () => ({ x: position.x, y: position.y }),
|
||||
getScale: () => scale.value,
|
||||
$el: canvasContainer.value,
|
||||
showToast
|
||||
};
|
||||
componentManager.setCanvasRef(canvasAPI);
|
||||
}
|
||||
|
||||
// 加载图表数据
|
||||
try {
|
||||
diagramData.value = await loadDiagramData();
|
||||
// 传入examId参数,让diagramManager处理动态加载
|
||||
diagramData.value = await loadDiagramData(props.examId);
|
||||
|
||||
// 预加载所有组件模块
|
||||
const componentTypes = new Set<string>();
|
||||
@@ -1033,16 +993,20 @@ onMounted(async () => {
|
||||
|
||||
// 直接通过componentManager预加载组件模块
|
||||
if (componentManager) {
|
||||
await componentManager.preloadComponentModules(Array.from(componentTypes));
|
||||
await componentManager.preloadComponentModules(
|
||||
Array.from(componentTypes),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载图表数据失败:", error);
|
||||
}
|
||||
// 初始化中心位置
|
||||
if (canvasContainer.value) {
|
||||
|
||||
// 初始化中心位置 - 使用componentManager设置
|
||||
if (canvasContainer.value && componentManager) {
|
||||
// 修改为将画布中心点放在容器中心点
|
||||
position.x = canvasContainer.value.clientWidth / 2 - 5000; // 画布宽度的一半
|
||||
position.y = canvasContainer.value.clientHeight / 2 - 5000; // 画布高度的一半
|
||||
const centerX = canvasContainer.value.clientWidth / 2 - 5000; // 画布宽度的一半
|
||||
const centerY = canvasContainer.value.clientHeight / 2 - 5000; // 画布高度的一半
|
||||
componentManager.setCanvasPosition(centerX, centerY);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1061,16 +1025,6 @@ function handleKeyDown(e: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清除所有toast定时器
|
||||
toastTimers.forEach((timerId) => clearTimeout(timerId));
|
||||
|
||||
// 重置事件状态
|
||||
isDragEventActive.value = false;
|
||||
isComponentDragEventActive.value = false;
|
||||
isWireCreationEventActive.value = false;
|
||||
});
|
||||
|
||||
// 无加载动画的数据更新方法
|
||||
function updateDiagramDataDirectly(data: DiagramData) {
|
||||
// 检查组件是否仍然挂载
|
||||
@@ -1079,112 +1033,38 @@ function updateDiagramDataDirectly(data: DiagramData) {
|
||||
}
|
||||
|
||||
diagramData.value = data;
|
||||
saveDiagramData(data);
|
||||
|
||||
// 发出diagram-updated事件
|
||||
emit("diagram-updated", data);
|
||||
}
|
||||
|
||||
// 暴露方法给父组件 - 简化版本,主要用于数据访问
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
// 基本数据操作
|
||||
getDiagramData: () => diagramData.value,
|
||||
updateDiagramDataDirectly,
|
||||
setDiagramData: (data: DiagramData) => {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!document.body.contains(canvasContainer.value)) {
|
||||
return; // 如果组件已经卸载,不执行后续操作
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
// 使用requestAnimationFrame确保UI更新
|
||||
window.requestAnimationFrame(() => {
|
||||
// 再次检查组件是否仍然挂载
|
||||
if (!document.body.contains(canvasContainer.value)) {
|
||||
return; // 如果组件已经卸载,不执行后续操作
|
||||
}
|
||||
|
||||
diagramData.value = data;
|
||||
saveDiagramData(data);
|
||||
|
||||
// 发出diagram-updated事件
|
||||
emit("diagram-updated", data);
|
||||
|
||||
// 短暂延迟后结束加载状态,以便UI能更新
|
||||
const timerId = setTimeout(() => {
|
||||
// 检查组件是否仍然挂载
|
||||
if (document.body.contains(canvasContainer.value)) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
// 将定时器ID保存起来,以便在组件卸载时清除
|
||||
toastTimers.push(timerId);
|
||||
});
|
||||
},
|
||||
|
||||
// 画布状态
|
||||
getCanvasPosition: () => ({ x: position.x, y: position.y }),
|
||||
getScale: () => scale.value,
|
||||
|
||||
// 通知系统
|
||||
showToast,
|
||||
});
|
||||
|
||||
// 监视器 - 当图表数据更改时保存
|
||||
watch(
|
||||
diagramData,
|
||||
(newData) => {
|
||||
saveDiagramData(newData);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基础容器样式 - 使用 Tailwind 类替代 */
|
||||
.diagram-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-size:
|
||||
20px 20px,
|
||||
20px 20px,
|
||||
100px 100px,
|
||||
100px 100px;
|
||||
background-position: 0 0;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 画布样式 - 部分保留自定义属性 */
|
||||
.diagram-canvas {
|
||||
position: relative;
|
||||
width: 10000px;
|
||||
height: 10000px;
|
||||
transform-origin: 0 0;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* 连线层样式 */
|
||||
.wires-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: auto;
|
||||
/* 修复:允许线被点击 */
|
||||
z-index: 50;
|
||||
overflow: visible;
|
||||
/* 确保超出SVG范围的内容也能显示 */
|
||||
}
|
||||
|
||||
.wires-layer path {
|
||||
@@ -1192,74 +1072,40 @@ watch(
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 元器件容器样式 */
|
||||
/* 组件容器样式 */
|
||||
.component-wrapper {
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
/* 移除内边距,确保元素大小与内容完全匹配 */
|
||||
box-sizing: content-box;
|
||||
/* 使用content-box确保内容尺寸不受padding影响 */
|
||||
display: inline-block;
|
||||
overflow: visible;
|
||||
/* 允许内容溢出(用于显示边框) */
|
||||
cursor: move;
|
||||
/* 显示移动光标 */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* 悬停状态 - 使用outline而非伪元素 */
|
||||
/* 悬停状态 */
|
||||
.component-hover {
|
||||
outline: 2px dashed #3498db;
|
||||
outline-offset: 2px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 选中状态 - 使用outline而非伪元素 */
|
||||
/* 选中状态 */
|
||||
.component-selected {
|
||||
outline: 3px dashed;
|
||||
outline-color: #e74c3c #f39c12 #3498db #2ecc71;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* 禁用状态 */
|
||||
.component-disabled {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(70%);
|
||||
}
|
||||
|
||||
/* 隐藏引脚状态 */
|
||||
.component-hidepins :deep([data-pin-wrapper]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 为黑暗模式设置不同的网格线颜色 */
|
||||
/* :root[data-theme="dark"] .diagram-container {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px),
|
||||
linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px);
|
||||
} */
|
||||
|
||||
/* 深度选择器 - 默认阻止SVG内部元素的鼠标事件,但允许SVG本身和特定交互元素 */
|
||||
/* SVG 交互样式 */
|
||||
.component-wrapper :deep(svg) {
|
||||
pointer-events: auto;
|
||||
/* 确保SVG本身可以接收鼠标事件用于拖拽 */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.component-wrapper :deep(svg *:not([class*="interactive"]):not(rect.glow):not(circle[fill-opacity]):not([fill-opacity])) {
|
||||
.component-wrapper
|
||||
:deep(
|
||||
svg
|
||||
*:not([class*="interactive"]):not(rect.glow):not(
|
||||
circle[fill-opacity]
|
||||
):not([fill-opacity])
|
||||
) {
|
||||
pointer-events: none;
|
||||
/* 非交互元素不接收鼠标事件 */
|
||||
}
|
||||
|
||||
/* 允许特定SVG元素接收鼠标事件,用于交互 */
|
||||
.component-wrapper :deep(svg circle[fill-opacity]),
|
||||
.component-wrapper :deep(svg rect[fill-opacity]),
|
||||
.component-wrapper :deep(svg rect[class*="glow"]),
|
||||
|
||||
100
src/components/LabCanvas/ItemList.vue
Normal file
100
src/components/LabCanvas/ItemList.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="px-6 py-4 overflow-auto flex-1">
|
||||
<div v-if="filteredItems.length > 0" class="grid grid-cols-2 gap-4">
|
||||
<div
|
||||
v-for="(item, index) in filteredItems"
|
||||
:key="index"
|
||||
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<div class="card-body p-3 items-center text-center">
|
||||
<div
|
||||
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2"
|
||||
>
|
||||
<!-- 组件预览 -->
|
||||
<component
|
||||
v-if="item.type && componentModules[item.type]"
|
||||
:is="componentModules[item.type].default"
|
||||
class="component-preview"
|
||||
:size="getPreviewSize(item.type)"
|
||||
/>
|
||||
<!-- 模板预览 -->
|
||||
<img
|
||||
v-else-if="item.thumbnailUrl"
|
||||
:src="item.thumbnailUrl || '/placeholder-template.png'"
|
||||
alt="Template thumbnail"
|
||||
class="max-h-full max-w-full object-contain"
|
||||
/>
|
||||
<!-- 加载中状态 -->
|
||||
<span v-else class="text-xs text-gray-400">加载中...</span>
|
||||
</div>
|
||||
<h3 class="card-title text-sm mt-2">{{ item.name }}</h3>
|
||||
<p class="text-xs opacity-70">
|
||||
{{ item.description || item.type || getItemSubtitle(item) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 无搜索结果 -->
|
||||
<div v-else class="py-16 text-center">
|
||||
<SearchX :size="48" class="mx-auto text-base-300 mb-3" />
|
||||
<p class="text-base-content opacity-70">{{ noResultsMessage }}</p>
|
||||
<button class="btn btn-sm btn-ghost mt-3" @click="$emit('clear-search')">
|
||||
清除搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { SearchX } from "lucide-vue-next";
|
||||
import { getPreviewSize } from "./index.ts";
|
||||
|
||||
interface Props {
|
||||
items: any[];
|
||||
searchQuery: string;
|
||||
componentModules: Record<string, any>;
|
||||
noResultsMessage: string;
|
||||
itemType: "component" | "template" | "virtual";
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits(["item-click", "clear-search"]);
|
||||
|
||||
// 过滤后的项目列表
|
||||
const filteredItems = computed(() => {
|
||||
if (!props.searchQuery) {
|
||||
return props.items;
|
||||
}
|
||||
const query = props.searchQuery.toLowerCase();
|
||||
return props.items.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(query) ||
|
||||
(item.type && item.type.toLowerCase().includes(query)) ||
|
||||
(item.description && item.description.toLowerCase().includes(query)),
|
||||
);
|
||||
});
|
||||
|
||||
// 获取项目副标题
|
||||
function getItemSubtitle(item: any): string {
|
||||
if (props.itemType === "template") {
|
||||
return "模板";
|
||||
}
|
||||
return item.type || "";
|
||||
}
|
||||
|
||||
// 处理项目点击
|
||||
function handleItemClick(item: any) {
|
||||
emit("item-click", item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.component-preview {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ref, shallowRef, computed } from "vue";
|
||||
import { ref, shallowRef, computed, reactive } from "vue";
|
||||
import { createInjectionState } from "@vueuse/core";
|
||||
import type { DiagramData, DiagramPart } from "./diagramManager";
|
||||
import {
|
||||
saveDiagramData,
|
||||
type DiagramData,
|
||||
type DiagramPart,
|
||||
} from "./diagramManager";
|
||||
import type { PropertyConfig } from "@/components/equipments/componentConfig";
|
||||
import {
|
||||
generatePropertyConfigs,
|
||||
@@ -24,10 +28,16 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
// --- 状态管理 ---
|
||||
const componentModules = ref<Record<string, ComponentModule>>({});
|
||||
const selectedComponentId = ref<string | null>(null);
|
||||
const selectedComponentConfig = shallowRef<{ props: PropertyConfig[] } | null>(null);
|
||||
const selectedComponentConfig = shallowRef<{
|
||||
props: PropertyConfig[];
|
||||
} | null>(null);
|
||||
const diagramCanvas = ref<any>(null);
|
||||
const componentRefs = ref<Record<string, any>>({});
|
||||
|
||||
// 新增:直接管理canvas的位置和缩放
|
||||
const canvasPosition = reactive({ x: 0, y: 0 });
|
||||
const canvasScale = ref(1);
|
||||
|
||||
// 计算当前选中的组件数据
|
||||
const selectedComponentData = computed(() => {
|
||||
if (!diagramCanvas.value || !selectedComponentId.value) return null;
|
||||
@@ -35,11 +45,130 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (canvasInstance && canvasInstance.getDiagramData) {
|
||||
const data = canvasInstance.getDiagramData();
|
||||
return data.parts.find((p: DiagramPart) => p.id === selectedComponentId.value) || null;
|
||||
return (
|
||||
data.parts.find(
|
||||
(p: DiagramPart) => p.id === selectedComponentId.value,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// --- Canvas 控制方法 ---
|
||||
|
||||
/**
|
||||
* 设置canvas位置
|
||||
*/
|
||||
function setCanvasPosition(x: number, y: number) {
|
||||
canvasPosition.x = x;
|
||||
canvasPosition.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新canvas位置(相对偏移)
|
||||
*/
|
||||
function updateCanvasPosition(deltaX: number, deltaY: number) {
|
||||
canvasPosition.x += deltaX;
|
||||
canvasPosition.y += deltaY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置canvas缩放
|
||||
*/
|
||||
function setCanvasScale(scale: number) {
|
||||
canvasScale.value = Math.max(0.2, Math.min(scale, 10.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取canvas位置
|
||||
*/
|
||||
function getCanvasPosition() {
|
||||
return { x: canvasPosition.x, y: canvasPosition.y };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取canvas缩放
|
||||
*/
|
||||
function getCanvasScale() {
|
||||
return canvasScale.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缩放到指定位置(以鼠标位置为中心)
|
||||
*/
|
||||
function zoomAtPosition(
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
zoomFactor: number,
|
||||
) {
|
||||
// 计算鼠标在画布坐标系中的位置
|
||||
const mouseXCanvas = (mouseX - canvasPosition.x) / canvasScale.value;
|
||||
const mouseYCanvas = (mouseY - canvasPosition.y) / canvasScale.value;
|
||||
|
||||
// 计算新的缩放值
|
||||
const newScale = Math.max(
|
||||
0.2,
|
||||
Math.min(canvasScale.value * zoomFactor, 10.0),
|
||||
);
|
||||
|
||||
// 计算新的位置,使鼠标指针位置在缩放前后保持不变
|
||||
canvasPosition.x = mouseX - mouseXCanvas * newScale;
|
||||
canvasPosition.y = mouseY - mouseYCanvas * newScale;
|
||||
canvasScale.value = newScale;
|
||||
|
||||
return {
|
||||
scale: newScale,
|
||||
position: { x: canvasPosition.x, y: canvasPosition.y },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将屏幕坐标转换为画布坐标
|
||||
*/
|
||||
function screenToCanvas(screenX: number, screenY: number) {
|
||||
return {
|
||||
x: (screenX - canvasPosition.x) / canvasScale.value,
|
||||
y: (screenY - canvasPosition.y) / canvasScale.value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将画布坐标转换为屏幕坐标
|
||||
*/
|
||||
function canvasToScreen(canvasX: number, canvasY: number) {
|
||||
return {
|
||||
x: canvasX * canvasScale.value + canvasPosition.x,
|
||||
y: canvasY * canvasScale.value + canvasPosition.y,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 居中显示指定区域
|
||||
*/
|
||||
function centerView(
|
||||
bounds: { x: number; y: number; width: number; height: number },
|
||||
containerWidth: number,
|
||||
containerHeight: number,
|
||||
) {
|
||||
// 计算合适的缩放比例
|
||||
const scaleX = containerWidth / bounds.width;
|
||||
const scaleY = containerHeight / bounds.height;
|
||||
const newScale = Math.min(scaleX, scaleY, 1) * 0.8; // 留一些边距
|
||||
|
||||
// 计算居中位置
|
||||
const centerX = bounds.x + bounds.width / 2;
|
||||
const centerY = bounds.y + bounds.height / 2;
|
||||
|
||||
canvasScale.value = newScale;
|
||||
canvasPosition.x = containerWidth / 2 - centerX * newScale;
|
||||
canvasPosition.y = containerHeight / 2 - centerY * newScale;
|
||||
|
||||
return {
|
||||
scale: newScale,
|
||||
position: { x: canvasPosition.x, y: canvasPosition.y },
|
||||
};
|
||||
}
|
||||
|
||||
// --- 组件模块管理 ---
|
||||
|
||||
/**
|
||||
@@ -78,7 +207,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
async function preloadComponentModules(componentTypes: string[]) {
|
||||
console.log("Preloading component modules:", componentTypes);
|
||||
await Promise.all(
|
||||
componentTypes.map((type) => loadComponentModule(type))
|
||||
componentTypes.map((type) => loadComponentModule(type)),
|
||||
);
|
||||
console.log("All component modules loaded");
|
||||
}
|
||||
@@ -111,23 +240,19 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
console.log(`组件模块加载成功: ${componentData.type}`, componentModule);
|
||||
|
||||
// 获取画布位置信息
|
||||
// 使用内部管理的位置和缩放信息
|
||||
let position = { x: 100, y: 100 };
|
||||
let scale = 1;
|
||||
|
||||
try {
|
||||
if (canvasInstance.getCanvasPosition && canvasInstance.getScale) {
|
||||
position = canvasInstance.getCanvasPosition();
|
||||
scale = canvasInstance.getScale();
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
if (canvasContainer) {
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
if (canvasContainer) {
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
|
||||
position.x = (viewportWidth / 2 - position.x) / scale;
|
||||
position.y = (viewportHeight / 2 - position.y) / scale;
|
||||
}
|
||||
position.x =
|
||||
(viewportWidth / 2 - canvasPosition.x) / canvasScale.value;
|
||||
position.y =
|
||||
(viewportHeight / 2 - canvasPosition.y) / canvasScale.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取画布位置时出错:", error);
|
||||
@@ -168,17 +293,21 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
};
|
||||
|
||||
// 通过画布实例添加组件
|
||||
if (canvasInstance.getDiagramData && canvasInstance.updateDiagramDataDirectly) {
|
||||
if (
|
||||
canvasInstance.getDiagramData &&
|
||||
canvasInstance.updateDiagramDataDirectly
|
||||
) {
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
currentData.parts.push(newComponent);
|
||||
|
||||
// 使用 updateDiagramDataDirectly 避免触发加载状态
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
saveDiagramData(currentData);
|
||||
|
||||
console.log("组件添加完成:", newComponent);
|
||||
|
||||
// 等待Vue的下一个tick,确保组件模块已经更新
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +322,10 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
console.log("添加模板:", templateData);
|
||||
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
if (
|
||||
!canvasInstance?.getDiagramData ||
|
||||
!canvasInstance?.updateDiagramDataDirectly
|
||||
) {
|
||||
console.error("没有可用的画布实例添加模板");
|
||||
return;
|
||||
}
|
||||
@@ -205,20 +337,18 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
const idPrefix = `template-${Date.now()}-`;
|
||||
|
||||
if (templateData.template?.parts) {
|
||||
// 获取视口中心位置
|
||||
// 使用内部管理的位置和缩放信息获取视口中心位置
|
||||
let viewportCenter = { x: 300, y: 200 };
|
||||
try {
|
||||
if (canvasInstance.getCanvasPosition && canvasInstance.getScale) {
|
||||
const position = canvasInstance.getCanvasPosition();
|
||||
const scale = canvasInstance.getScale();
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
const canvasContainer = canvasInstance.$el as HTMLElement;
|
||||
|
||||
if (canvasContainer) {
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
viewportCenter.x = (viewportWidth / 2 - position.x) / scale;
|
||||
viewportCenter.y = (viewportHeight / 2 - position.y) / scale;
|
||||
}
|
||||
if (canvasContainer) {
|
||||
const viewportWidth = canvasContainer.clientWidth;
|
||||
const viewportHeight = canvasContainer.clientHeight;
|
||||
viewportCenter.x =
|
||||
(viewportWidth / 2 - canvasPosition.x) / canvasScale.value;
|
||||
viewportCenter.y =
|
||||
(viewportHeight / 2 - canvasPosition.y) / canvasScale.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取视口中心位置时出错:", error);
|
||||
@@ -247,7 +377,10 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
|
||||
// 计算新位置
|
||||
if (typeof newPart.x === "number" && typeof newPart.y === "number") {
|
||||
if (
|
||||
typeof newPart.x === "number" &&
|
||||
typeof newPart.y === "number"
|
||||
) {
|
||||
const relativeX = part.x - mainPart.x;
|
||||
const relativeY = part.y - mainPart.y;
|
||||
newPart.x = viewportCenter.x + relativeX;
|
||||
@@ -255,7 +388,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
|
||||
return newPart;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
currentData.parts.push(...newParts);
|
||||
@@ -267,32 +400,38 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
idMap[part.id] = `${idPrefix}${part.id}`;
|
||||
});
|
||||
|
||||
const newConnections = templateData.template.connections.map((conn: any) => {
|
||||
if (Array.isArray(conn)) {
|
||||
const [from, to, type, path] = conn;
|
||||
const fromParts = from.split(":");
|
||||
const toParts = to.split(":");
|
||||
const newConnections = templateData.template.connections.map(
|
||||
(conn: any) => {
|
||||
if (Array.isArray(conn)) {
|
||||
const [from, to, type, path] = conn;
|
||||
const fromParts = from.split(":");
|
||||
const toParts = to.split(":");
|
||||
|
||||
if (fromParts.length === 2 && toParts.length === 2) {
|
||||
const fromComponentId = fromParts[0];
|
||||
const fromPinId = fromParts[1];
|
||||
const toComponentId = toParts[0];
|
||||
const toPinId = toParts[1];
|
||||
if (fromParts.length === 2 && toParts.length === 2) {
|
||||
const fromComponentId = fromParts[0];
|
||||
const fromPinId = fromParts[1];
|
||||
const toComponentId = toParts[0];
|
||||
const toPinId = toParts[1];
|
||||
|
||||
const newFrom = `${idMap[fromComponentId] || fromComponentId}:${fromPinId}`;
|
||||
const newTo = `${idMap[toComponentId] || toComponentId}:${toPinId}`;
|
||||
const newFrom = `${idMap[fromComponentId] || fromComponentId}:${fromPinId}`;
|
||||
const newTo = `${idMap[toComponentId] || toComponentId}:${toPinId}`;
|
||||
|
||||
return [newFrom, newTo, type, path];
|
||||
return [newFrom, newTo, type, path];
|
||||
}
|
||||
}
|
||||
}
|
||||
return conn;
|
||||
});
|
||||
return conn;
|
||||
},
|
||||
);
|
||||
|
||||
currentData.connections.push(...newConnections);
|
||||
}
|
||||
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log("=== 更新图表数据完成,新组件数量:", currentData.parts.length);
|
||||
console.log(
|
||||
"=== 更新图表数据完成,新组件数量:",
|
||||
currentData.parts.length,
|
||||
);
|
||||
saveDiagramData(currentData);
|
||||
|
||||
return { success: true, message: `已添加 ${templateData.name} 模板` };
|
||||
} else {
|
||||
@@ -306,12 +445,17 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
*/
|
||||
function deleteComponent(componentId: string) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
if (
|
||||
!canvasInstance?.getDiagramData ||
|
||||
!canvasInstance?.updateDiagramDataDirectly
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const component = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||
const component = currentData.parts.find(
|
||||
(p: DiagramPart) => p.id === componentId,
|
||||
);
|
||||
|
||||
if (!component) return;
|
||||
|
||||
@@ -320,34 +464,47 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
// 处理组件组
|
||||
if (component.group && component.group !== "") {
|
||||
const groupMembers = currentData.parts.filter(
|
||||
(p: DiagramPart) => p.group === component.group && p.id !== componentId
|
||||
(p: DiagramPart) =>
|
||||
p.group === component.group && p.id !== componentId,
|
||||
);
|
||||
componentsToDelete.push(...groupMembers.map((p: DiagramPart) => p.id));
|
||||
console.log(`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`);
|
||||
console.log(
|
||||
`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`,
|
||||
);
|
||||
}
|
||||
|
||||
// 删除组件
|
||||
currentData.parts = currentData.parts.filter(
|
||||
(p: DiagramPart) => !componentsToDelete.includes(p.id)
|
||||
(p: DiagramPart) => !componentsToDelete.includes(p.id),
|
||||
);
|
||||
|
||||
// 删除相关连接
|
||||
currentData.connections = currentData.connections.filter((connection: any) => {
|
||||
for (const id of componentsToDelete) {
|
||||
if (connection[0].startsWith(`${id}:`) || connection[1].startsWith(`${id}:`)) {
|
||||
return false;
|
||||
currentData.connections = currentData.connections.filter(
|
||||
(connection: any) => {
|
||||
for (const id of componentsToDelete) {
|
||||
if (
|
||||
connection[0].startsWith(`${id}:`) ||
|
||||
connection[1].startsWith(`${id}:`)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
// 清除选中状态
|
||||
if (selectedComponentId.value && componentsToDelete.includes(selectedComponentId.value)) {
|
||||
if (
|
||||
selectedComponentId.value &&
|
||||
componentsToDelete.includes(selectedComponentId.value)
|
||||
) {
|
||||
selectedComponentId.value = null;
|
||||
selectedComponentConfig.value = null;
|
||||
}
|
||||
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
|
||||
saveDiagramData(currentData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,16 +535,18 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
// 添加组件直接属性
|
||||
const directPropConfigs = generatePropertyConfigs(componentData);
|
||||
const newDirectProps = directPropConfigs.filter(
|
||||
(config) => !addedProps.has(config.name)
|
||||
(config) => !addedProps.has(config.name),
|
||||
);
|
||||
propConfigs.push(...newDirectProps);
|
||||
|
||||
// 添加 attrs 中的属性
|
||||
if (componentData.attrs) {
|
||||
const attrPropConfigs = generatePropsFromAttrs(componentData.attrs);
|
||||
const attrPropConfigs = generatePropsFromAttrs(
|
||||
componentData.attrs,
|
||||
);
|
||||
attrPropConfigs.forEach((attrConfig) => {
|
||||
const existingIndex = propConfigs.findIndex(
|
||||
(p) => p.name === attrConfig.name
|
||||
(p) => p.name === attrConfig.name,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
propConfigs[existingIndex] = attrConfig;
|
||||
@@ -398,9 +557,15 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
|
||||
selectedComponentConfig.value = { props: propConfigs };
|
||||
console.log(`Built config for ${componentData.type}:`, selectedComponentConfig.value);
|
||||
console.log(
|
||||
`Built config for ${componentData.type}:`,
|
||||
selectedComponentConfig.value,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Error building config for ${componentData.type}:`, error);
|
||||
console.error(
|
||||
`Error building config for ${componentData.type}:`,
|
||||
error,
|
||||
);
|
||||
selectedComponentConfig.value = { props: [] };
|
||||
}
|
||||
} else {
|
||||
@@ -413,9 +578,16 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
/**
|
||||
* 更新组件属性
|
||||
*/
|
||||
function updateComponentProp(componentId: string, propName: string, value: any) {
|
||||
function updateComponentProp(
|
||||
componentId: string,
|
||||
propName: string,
|
||||
value: any,
|
||||
) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
if (
|
||||
!canvasInstance?.getDiagramData ||
|
||||
!canvasInstance?.updateDiagramDataDirectly
|
||||
) {
|
||||
console.error("没有可用的画布实例进行属性更新");
|
||||
return;
|
||||
}
|
||||
@@ -426,7 +598,9 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||
const part = currentData.parts.find(
|
||||
(p: DiagramPart) => p.id === componentId,
|
||||
);
|
||||
|
||||
if (part) {
|
||||
if (propName in part) {
|
||||
@@ -439,27 +613,44 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log(`更新组件${componentId}的属性${propName}为:`, value, typeof value);
|
||||
console.log(
|
||||
`更新组件${componentId}的属性${propName}为:`,
|
||||
value,
|
||||
typeof value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组件直接属性
|
||||
*/
|
||||
function updateComponentDirectProp(componentId: string, propName: string, value: any) {
|
||||
function updateComponentDirectProp(
|
||||
componentId: string,
|
||||
propName: string,
|
||||
value: any,
|
||||
) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
if (
|
||||
!canvasInstance?.getDiagramData ||
|
||||
!canvasInstance?.updateDiagramDataDirectly
|
||||
) {
|
||||
console.error("没有可用的画布实例进行属性更新");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
|
||||
const part = currentData.parts.find(
|
||||
(p: DiagramPart) => p.id === componentId,
|
||||
);
|
||||
|
||||
if (part) {
|
||||
(part as any)[propName] = value;
|
||||
canvasInstance.updateDiagramDataDirectly(currentData);
|
||||
console.log(`更新组件${componentId}的直接属性${propName}为:`, value, typeof value);
|
||||
console.log(
|
||||
`更新组件${componentId}的直接属性${propName}为:`,
|
||||
value,
|
||||
typeof value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,12 +659,17 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
*/
|
||||
function moveComponent(moveData: { id: string; x: number; y: number }) {
|
||||
const canvasInstance = diagramCanvas.value as any;
|
||||
if (!canvasInstance?.getDiagramData || !canvasInstance?.updateDiagramDataDirectly) {
|
||||
if (
|
||||
!canvasInstance?.getDiagramData ||
|
||||
!canvasInstance?.updateDiagramDataDirectly
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentData = canvasInstance.getDiagramData();
|
||||
const part = currentData.parts.find((p: DiagramPart) => p.id === moveData.id);
|
||||
const part = currentData.parts.find(
|
||||
(p: DiagramPart) => p.id === moveData.id,
|
||||
);
|
||||
if (part) {
|
||||
part.x = moveData.x;
|
||||
part.y = moveData.y;
|
||||
@@ -514,7 +710,13 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
if (canvasInstance && canvasInstance.getDiagramData) {
|
||||
return canvasInstance.getDiagramData();
|
||||
}
|
||||
return { parts: [], connections: [], version: 1, author: "admin", editor: "me" };
|
||||
return {
|
||||
parts: [],
|
||||
connections: [],
|
||||
version: 1,
|
||||
author: "admin",
|
||||
editor: "me",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -527,29 +729,6 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取画布位置和缩放信息
|
||||
*/
|
||||
function getCanvasInfo() {
|
||||
const canvasInstance = diagramCanvas.value;
|
||||
if (!canvasInstance) return { position: { x: 0, y: 0 }, scale: 1 };
|
||||
|
||||
const position = canvasInstance.getCanvasPosition ? canvasInstance.getCanvasPosition() : { x: 0, y: 0 };
|
||||
const scale = canvasInstance.getScale ? canvasInstance.getScale() : 1;
|
||||
|
||||
return { position, scale };
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通知
|
||||
*/
|
||||
function showToast(message: string, type: "success" | "error" | "info" = "info") {
|
||||
const canvasInstance = diagramCanvas.value;
|
||||
if (canvasInstance && canvasInstance.showToast) {
|
||||
canvasInstance.showToast(message, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件定义
|
||||
*/
|
||||
@@ -584,11 +763,15 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
function prepareComponentProps(
|
||||
attrs: Record<string, any>,
|
||||
componentId?: string,
|
||||
examId?: string,
|
||||
): Record<string, any> {
|
||||
const result: Record<string, any> = { ...attrs };
|
||||
if (componentId) {
|
||||
result.componentId = componentId;
|
||||
}
|
||||
if (examId) {
|
||||
result.examId = examId;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -619,6 +802,10 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
selectedComponentConfig,
|
||||
componentRefs,
|
||||
|
||||
// Canvas控制状态
|
||||
canvasPosition,
|
||||
canvasScale,
|
||||
|
||||
// 方法
|
||||
loadComponentModule,
|
||||
preloadComponentModules,
|
||||
@@ -634,13 +821,22 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
|
||||
getComponentRef,
|
||||
getDiagramData,
|
||||
updateDiagramData,
|
||||
getCanvasInfo,
|
||||
showToast,
|
||||
getComponentDefinition,
|
||||
prepareComponentProps,
|
||||
initialize,
|
||||
|
||||
// Canvas控制方法
|
||||
setCanvasPosition,
|
||||
updateCanvasPosition,
|
||||
setCanvasScale,
|
||||
getCanvasPosition,
|
||||
getCanvasScale,
|
||||
zoomAtPosition,
|
||||
screenToCanvas,
|
||||
canvasToScreen,
|
||||
centerView,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export { useProvideComponentManager, useComponentManager };
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface DiagramPart {
|
||||
// 连接类型定义 - 使用元组类型表示四元素数组
|
||||
export type ConnectionArray = [string, string, number, string[]];
|
||||
|
||||
import { AuthManager } from '@/utils/AuthManager';
|
||||
|
||||
// 解析连接字符串为组件ID和引脚ID
|
||||
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
|
||||
const [componentId, pinId] = connectionPin.split(':');
|
||||
@@ -80,22 +82,72 @@ export interface WireItem {
|
||||
showLabel: boolean;
|
||||
}
|
||||
|
||||
// 从本地存储加载图表数据
|
||||
export async function loadDiagramData(): Promise<DiagramData> {
|
||||
// 从本地存储或动态API加载图表数据
|
||||
export async function loadDiagramData(examId?: string): Promise<DiagramData> {
|
||||
try {
|
||||
// 先尝试从本地存储加载
|
||||
const savedData = localStorage.getItem('diagramData');
|
||||
if (savedData) {
|
||||
return JSON.parse(savedData);
|
||||
// 如果提供了examId,优先从API加载实验的diagram
|
||||
if (examId) {
|
||||
try {
|
||||
const examClient = AuthManager.createAuthenticatedExamClient();
|
||||
|
||||
// 获取diagram类型的资源列表
|
||||
const resources = await examClient.getExamResourceList(examId, 'canvas');
|
||||
|
||||
if (resources && resources.length > 0) {
|
||||
// 获取第一个diagram资源
|
||||
const diagramResource = resources[0];
|
||||
|
||||
// 使用动态API获取资源文件内容
|
||||
const response = await examClient.getExamResourceById(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 savedData = localStorage.getItem('diagramData');
|
||||
if (savedData) {
|
||||
const data = JSON.parse(savedData);
|
||||
const validation = validateDiagramData(data);
|
||||
if (validation.isValid) {
|
||||
return data;
|
||||
} else {
|
||||
console.warn('本地存储的diagram数据格式无效:', validation.errors);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果本地存储也没有,从静态文件加载(作为最后的备选)
|
||||
const response = await fetch('/src/components/diagram.json');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load diagram.json: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
|
||||
// 验证静态文件数据
|
||||
const validation = validateDiagramData(data);
|
||||
if (validation.isValid) {
|
||||
return data;
|
||||
} else {
|
||||
console.warn('静态diagram文件数据格式无效:', validation.errors);
|
||||
throw new Error('所有diagram数据源都无效');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading diagram data:', error);
|
||||
// 返回空的默认数据结构
|
||||
@@ -123,14 +175,6 @@ export function saveDiagramData(data: DiagramData): void {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新组件到图表数据
|
||||
export function addPart(data: DiagramData, part: DiagramPart): DiagramData {
|
||||
return {
|
||||
...data,
|
||||
parts: [...data.parts, part]
|
||||
};
|
||||
}
|
||||
|
||||
// 更新组件位置
|
||||
export function updatePartPosition(
|
||||
data: DiagramData,
|
||||
@@ -171,42 +215,6 @@ export function updatePartAttribute(
|
||||
};
|
||||
}
|
||||
|
||||
// 删除组件及同组组件
|
||||
export function deletePart(data: DiagramData, partId: string): DiagramData {
|
||||
// 首先找到要删除的组件
|
||||
const component = data.parts.find(part => part.id === partId);
|
||||
if (!component) return data;
|
||||
|
||||
// 收集需要删除的组件ID列表
|
||||
const componentsToDelete: string[] = [partId];
|
||||
|
||||
// 如果组件属于一个组,则找出所有同组的组件
|
||||
if (component.group && component.group !== '') {
|
||||
const groupMembers = data.parts.filter(
|
||||
p => p.group === component.group && p.id !== partId
|
||||
);
|
||||
|
||||
// 将同组组件ID添加到删除列表
|
||||
componentsToDelete.push(...groupMembers.map(p => p.id));
|
||||
console.log(`删除组件 ${partId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`);
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
// 删除所有标记的组件
|
||||
parts: data.parts.filter(part => !componentsToDelete.includes(part.id)),
|
||||
// 删除与这些组件相关的所有连接
|
||||
connections: data.connections.filter(conn => {
|
||||
const [startPin, endPin] = conn;
|
||||
const startCompId = startPin.split(':')[0];
|
||||
const endCompId = endPin.split(':')[0];
|
||||
|
||||
// 检查连接两端的组件是否在删除列表中
|
||||
return !componentsToDelete.includes(startCompId) && !componentsToDelete.includes(endCompId);
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// 添加连接
|
||||
export function addConnection(
|
||||
data: DiagramData,
|
||||
@@ -256,25 +264,6 @@ export function findConnectionsByPart(
|
||||
});
|
||||
}
|
||||
|
||||
// 基于组的移动相关组件
|
||||
export function moveGroupComponents(
|
||||
data: DiagramData,
|
||||
groupId: string,
|
||||
deltaX: number,
|
||||
deltaY: number
|
||||
): DiagramData {
|
||||
if (!groupId) return data;
|
||||
|
||||
return {
|
||||
...data,
|
||||
parts: data.parts.map(part =>
|
||||
part.group === groupId
|
||||
? { ...part, x: part.x + deltaX, y: part.y + deltaY }
|
||||
: part
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
// 添加验证diagram.json文件的函数
|
||||
export function validateDiagramData(data: any): { isValid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
@@ -1,8 +1,110 @@
|
||||
import motherboardSvg from "@/components/equipments/svg/motherboard.svg";
|
||||
import buttonSvg from "@/components/equipments/svg/button.svg";
|
||||
|
||||
// 元器件配置接口
|
||||
export interface ComponentConfig {
|
||||
type: string;
|
||||
name: string;
|
||||
previewSize?: number;
|
||||
}
|
||||
|
||||
// 虚拟外设配置接口
|
||||
export interface VirtualDeviceConfig {
|
||||
type: string;
|
||||
name: string;
|
||||
previewSize?: number;
|
||||
}
|
||||
|
||||
// 模板配置接口
|
||||
export interface TemplateConfig {
|
||||
name: string;
|
||||
id: string;
|
||||
description: string;
|
||||
path: string;
|
||||
thumbnailUrl: string;
|
||||
capsPage?: string;
|
||||
}
|
||||
|
||||
// 预览尺寸配置
|
||||
export const previewSizes: Record<string, number> = {
|
||||
MechanicalButton: 0.4,
|
||||
Switch: 0.35,
|
||||
Pin: 0.8,
|
||||
SMT_LED: 0.7,
|
||||
SevenSegmentDisplay: 0.4,
|
||||
HDMI: 0.5,
|
||||
DDR: 0.5,
|
||||
ETH: 0.5,
|
||||
SD: 0.6,
|
||||
SFP: 0.4,
|
||||
SMA: 0.7,
|
||||
MotherBoard: 0.13,
|
||||
PG2L100H_FBG676: 0.2,
|
||||
BaseBoard: 0.15,
|
||||
DDS: 0.3,
|
||||
};
|
||||
|
||||
// 可用元器件列表
|
||||
export const availableComponents: ComponentConfig[] = [
|
||||
{ type: "MechanicalButton", name: "机械按钮" },
|
||||
{ type: "Switch", name: "开关" },
|
||||
{ type: "Pin", name: "引脚" },
|
||||
{ type: "SMT_LED", name: "贴片LED" },
|
||||
{ type: "SevenSegmentDisplay", name: "数码管" },
|
||||
{ type: "HDMI", name: "HDMI接口" },
|
||||
{ type: "DDR", name: "DDR内存" },
|
||||
{ type: "ETH", name: "以太网接口" },
|
||||
{ type: "SD", name: "SD卡插槽" },
|
||||
{ type: "SFP", name: "SFP光纤模块" },
|
||||
{ type: "SMA", name: "SMA连接器" },
|
||||
{ type: "MotherBoard", name: "主板" },
|
||||
{ type: "PG2L100H_FBG676", name: "PG2L100H FBG676芯片" },
|
||||
{ type: "BaseBoard", name: "通用底板" },
|
||||
];
|
||||
|
||||
// 可用虚拟外设列表
|
||||
export const availableVirtualDevices: VirtualDeviceConfig[] = [
|
||||
{ type: "DDS", name: "信号发生器" },
|
||||
];
|
||||
|
||||
// 可用模板列表
|
||||
export const availableTemplates: TemplateConfig[] = [
|
||||
{
|
||||
name: "PG2L100H 基础开发板",
|
||||
id: "PG2L100H_Pango100pro",
|
||||
description: "包含主板和两个LED的基本设置",
|
||||
path: "/EquipmentTemplates/PG2L100H_Pango100pro.json",
|
||||
thumbnailUrl: motherboardSvg,
|
||||
},
|
||||
{
|
||||
name: "矩阵键盘",
|
||||
id: "MatrixKey",
|
||||
description: "包含4x4,共16个按键的矩阵键盘",
|
||||
path: "/EquipmentTemplates/MatrixKey.json",
|
||||
thumbnailUrl: buttonSvg,
|
||||
},
|
||||
];
|
||||
|
||||
// 获取组件预览尺寸的工具函数
|
||||
export function getPreviewSize(componentType: string): number {
|
||||
return previewSizes[componentType] || 0.5;
|
||||
}
|
||||
|
||||
// 获取所有组件类型(用于预加载)
|
||||
export function getAllComponentTypes(): string[] {
|
||||
const componentTypes = availableComponents.map((c) => c.type);
|
||||
const virtualDeviceTypes = availableVirtualDevices.map((d) => d.type);
|
||||
return [...componentTypes, ...virtualDeviceTypes];
|
||||
}
|
||||
|
||||
// 导出组件管理器服务
|
||||
export { useProvideComponentManager, useComponentManager } from './composable/componentManager';
|
||||
export {
|
||||
useProvideComponentManager,
|
||||
useComponentManager,
|
||||
} from "./composable/componentManager";
|
||||
|
||||
// 导出图表管理器
|
||||
export type { DiagramData, DiagramPart } from './composable/diagramManager';
|
||||
export type { DiagramData, DiagramPart } from "./composable/diagramManager";
|
||||
|
||||
// 导出连线管理器
|
||||
export type { WireItem } from './composable/wireManager';
|
||||
export type { WireItem } from "./composable/wireManager";
|
||||
|
||||
736
src/components/LogicAnalyzer/LogicAnalyzerManager.ts
Normal file
736
src/components/LogicAnalyzer/LogicAnalyzerManager.ts
Normal file
@@ -0,0 +1,736 @@
|
||||
import { createInjectionState } from "@vueuse/core";
|
||||
import { shallowRef, reactive, ref, computed } from "vue";
|
||||
import { Mutex } from "async-mutex";
|
||||
import {
|
||||
CaptureConfig,
|
||||
CaptureStatus,
|
||||
LogicAnalyzerClient,
|
||||
GlobalCaptureMode,
|
||||
SignalOperator,
|
||||
SignalTriggerConfig,
|
||||
SignalValue,
|
||||
AnalyzerChannelDiv,
|
||||
} from "@/APIClient";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
import { useAlertStore } from "@/components/Alert";
|
||||
import { useRequiredInjection } from "@/utils/Common";
|
||||
|
||||
export type LogicDataType = {
|
||||
x: number[];
|
||||
y: number[][]; // 8 channels of digital data (0 or 1)
|
||||
xUnit: "s" | "ms" | "us" | "ns";
|
||||
};
|
||||
|
||||
// 通道接口定义
|
||||
export type Channel = {
|
||||
enabled: boolean;
|
||||
label: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
// 全局模式选项
|
||||
const globalModes = [
|
||||
{
|
||||
value: GlobalCaptureMode.AND,
|
||||
label: "AND",
|
||||
description: "所有条件都满足时触发",
|
||||
},
|
||||
{
|
||||
value: GlobalCaptureMode.OR,
|
||||
label: "OR",
|
||||
description: "任一条件满足时触发",
|
||||
},
|
||||
{ value: GlobalCaptureMode.NAND, label: "NAND", description: "AND的非" },
|
||||
{ value: GlobalCaptureMode.NOR, label: "NOR", description: "OR的非" },
|
||||
];
|
||||
|
||||
// 操作符选项
|
||||
const operators = [
|
||||
{ value: SignalOperator.Equal, label: "=" },
|
||||
{ value: SignalOperator.NotEqual, label: "≠" },
|
||||
{ value: SignalOperator.LessThan, label: "<" },
|
||||
{ value: SignalOperator.LessThanOrEqual, label: "≤" },
|
||||
{ value: SignalOperator.GreaterThan, label: ">" },
|
||||
{ value: SignalOperator.GreaterThanOrEqual, label: "≥" },
|
||||
];
|
||||
|
||||
// 信号值选项
|
||||
const signalValues = [
|
||||
{ value: SignalValue.Logic0, label: "0" },
|
||||
{ value: SignalValue.Logic1, label: "1" },
|
||||
{ value: SignalValue.NotCare, label: "X" },
|
||||
{ value: SignalValue.Rise, label: "↑" },
|
||||
{ value: SignalValue.Fall, label: "↓" },
|
||||
{ value: SignalValue.RiseOrFall, label: "↕" },
|
||||
{ value: SignalValue.NoChange, 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 = [
|
||||
"#FF5733",
|
||||
"#33FF57",
|
||||
"#3357FF",
|
||||
"#FF33F5",
|
||||
"#F5FF33",
|
||||
"#33FFF5",
|
||||
"#FF8C33",
|
||||
"#8C33FF",
|
||||
];
|
||||
|
||||
// 添加逻辑分析仪频率常量
|
||||
const LOGIC_ANALYZER_FREQUENCY = 125_000_000; // 125MHz
|
||||
const SAMPLE_PERIOD_NS = 1_000_000_000 / LOGIC_ANALYZER_FREQUENCY; // 采样周期,单位:纳秒
|
||||
|
||||
const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
||||
() => {
|
||||
const logicData = shallowRef<LogicDataType>();
|
||||
const alert = useRequiredInjection(useAlertStore);
|
||||
|
||||
// 添加互斥锁
|
||||
const operationMutex = new Mutex();
|
||||
|
||||
// 触发设置相关状态
|
||||
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 isCapturing = ref(false); // 添加捕获状态标识
|
||||
|
||||
// 通道配置
|
||||
const channels = reactive<Channel[]>(
|
||||
Array.from({ length: 32 }, (_, index) => ({
|
||||
enabled: index < 8, // 默认启用前8个通道
|
||||
label: `CH${index}`,
|
||||
color: defaultColors[index % defaultColors.length], // 使用模运算避免数组越界
|
||||
})),
|
||||
);
|
||||
|
||||
// 32个信号通道的配置
|
||||
const signalConfigs = reactive<SignalTriggerConfig[]>(
|
||||
Array.from(
|
||||
{ length: 32 },
|
||||
(_, index) =>
|
||||
new SignalTriggerConfig({
|
||||
signalIndex: index,
|
||||
operator: SignalOperator.Equal,
|
||||
value: SignalValue.NotCare,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// 计算启用的通道数量
|
||||
const enabledChannelCount = computed(
|
||||
() => channels.filter((channel) => channel.enabled).length,
|
||||
);
|
||||
|
||||
// 添加计算属性:获取通道名称数组
|
||||
const channelNames = computed(() =>
|
||||
channels.map((channel) => channel.label),
|
||||
);
|
||||
|
||||
// 添加计算属性:获取启用通道的名称数组
|
||||
const enabledChannels = computed(() =>
|
||||
channels.filter((channel) => channel.enabled),
|
||||
);
|
||||
|
||||
// 转换通道数字到枚举值
|
||||
const getChannelDivEnum = (channelCount: number): AnalyzerChannelDiv => {
|
||||
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 setChannelDiv = (channelCount: number) => {
|
||||
// 验证通道数量是否有效
|
||||
if (!channelDivOptions.find(option => option.value === channelCount)) {
|
||||
console.error(`无效的通道组设置: ${channelCount}`);
|
||||
return;
|
||||
}
|
||||
currentChannelDiv.value = channelCount;
|
||||
|
||||
// 禁用所有通道
|
||||
channels.forEach((channel) => {
|
||||
channel.enabled = false;
|
||||
});
|
||||
|
||||
// 启用指定数量的通道(从CH0开始)
|
||||
for (let i = 0; i < channelCount && i < channels.length; i++) {
|
||||
channels[i].enabled = true;
|
||||
}
|
||||
|
||||
const option = channelDivOptions.find(opt => opt.value === channelCount);
|
||||
alert?.success(`已设置为${option?.label}`, 2000);
|
||||
};
|
||||
|
||||
const setGlobalMode = (mode: GlobalCaptureMode) => {
|
||||
currentGlobalMode.value = mode;
|
||||
const modeOption = globalModes.find((m) => m.value === mode);
|
||||
alert?.info(`全局触发模式已设置为 ${modeOption?.label}`, 2000);
|
||||
};
|
||||
|
||||
const resetConfiguration = () => {
|
||||
currentGlobalMode.value = GlobalCaptureMode.AND;
|
||||
currentChannelDiv.value = 8; // 重置为默认的8通道
|
||||
setChannelDiv(8); // 重置为默认的8通道
|
||||
|
||||
signalConfigs.forEach((signal) => {
|
||||
signal.operator = SignalOperator.Equal;
|
||||
signal.value = SignalValue.NotCare;
|
||||
});
|
||||
|
||||
alert?.info("配置已重置", 2000);
|
||||
};
|
||||
|
||||
// 添加设置逻辑数据的方法
|
||||
const setLogicData = (data: LogicDataType) => {
|
||||
logicData.value = data;
|
||||
};
|
||||
|
||||
const getCaptureData = async () => {
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
||||
// 获取捕获数据,使用当前设置的捕获长度
|
||||
const base64Data = await client.getCaptureData(captureLength.value);
|
||||
|
||||
// 将base64数据转换为bytes
|
||||
const binaryString = atob(base64Data);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
// 根据当前通道数量解析数据
|
||||
const channelCount = currentChannelDiv.value;
|
||||
const timeStepNs = SAMPLE_PERIOD_NS;
|
||||
|
||||
let sampleCount: number;
|
||||
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 },
|
||||
(_, i) => (i * timeStepNs) / 1000,
|
||||
); // 转换为微秒
|
||||
|
||||
// 创建8个通道的数据
|
||||
y = Array.from(
|
||||
{ length: 8 },
|
||||
() => new Array(sampleCount),
|
||||
);
|
||||
|
||||
// 解析每个字节的8个位到对应通道
|
||||
for (let i = 0; i < sampleCount; i++) {
|
||||
const byte = bytes[i];
|
||||
for (let channel = 0; channel < 8; channel++) {
|
||||
// bit0对应ch0, bit1对应ch1, ..., bit7对应ch7
|
||||
y[channel][i] = (byte >> channel) & 1;
|
||||
}
|
||||
}
|
||||
} else if (channelCount === 16) {
|
||||
// 16通道:每2个字节包含1个时间单位的16个通道数据
|
||||
sampleCount = bytes.length / 2;
|
||||
|
||||
// 创建时间轴
|
||||
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 = {
|
||||
x,
|
||||
y,
|
||||
xUnit: "us", // 微秒单位
|
||||
};
|
||||
|
||||
setLogicData(logicData);
|
||||
} catch (error) {
|
||||
console.error("获取捕获数据失败:", error);
|
||||
alert?.error("获取捕获数据失败", 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const startCapture = async () => {
|
||||
// 检查是否有其他操作正在进行
|
||||
if (operationMutex.isLocked()) {
|
||||
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
isCapturing.value = true;
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
||||
|
||||
// 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);
|
||||
if (!captureStarted) {
|
||||
throw new Error("无法启动捕获");
|
||||
}
|
||||
|
||||
alert?.info("开始捕获信号...", 2000);
|
||||
|
||||
// 3. 轮询捕获状态
|
||||
let captureCompleted = false;
|
||||
while (isCapturing.value) {
|
||||
const status = await client.getCaptureStatus();
|
||||
|
||||
// 检查是否捕获完成
|
||||
if (status === CaptureStatus.CaptureDone) {
|
||||
captureCompleted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查是否仍在捕获中
|
||||
if (
|
||||
status === CaptureStatus.CaptureBusy ||
|
||||
status === CaptureStatus.CaptureOn ||
|
||||
status === CaptureStatus.CaptureForce
|
||||
) {
|
||||
// 等待500毫秒后继续轮询
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 其他状态视为错误
|
||||
// throw new Error(`捕获状态异常: ${status}`);
|
||||
}
|
||||
|
||||
// 如果捕获被停止,不继续处理数据
|
||||
if (!captureCompleted) {
|
||||
alert?.info("捕获已停止", 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
await getCaptureData();
|
||||
alert.success(`捕获完成!`, 3000);
|
||||
} catch (error) {
|
||||
console.error("捕获失败:", error);
|
||||
alert?.error(
|
||||
`捕获失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
3000,
|
||||
);
|
||||
} finally {
|
||||
isCapturing.value = false;
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
const stopCapture = async () => {
|
||||
// 检查是否正在捕获
|
||||
if (!isCapturing.value) {
|
||||
alert.warn("当前没有正在进行的捕获操作", 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置捕获状态为false,这会使轮询停止
|
||||
isCapturing.value = false;
|
||||
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
||||
|
||||
// 执行强制捕获来停止当前捕获
|
||||
const forceSuccess = await client.setCaptureMode(false, false);
|
||||
if (!forceSuccess) {
|
||||
throw new Error("无法停止捕获");
|
||||
}
|
||||
|
||||
alert.info("已停止强制捕获...", 2000);
|
||||
} catch (error) {
|
||||
console.error("停止捕获失败:", error);
|
||||
alert.error(
|
||||
`停止捕获失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
3000,
|
||||
);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
const forceCapture = async () => {
|
||||
// 检查是否正在捕获
|
||||
if (!isCapturing.value) {
|
||||
alert.warn("当前没有正在进行的捕获操作", 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
|
||||
|
||||
// 执行强制捕获来停止当前捕获
|
||||
const forceSuccess = await client.setCaptureMode(true, true);
|
||||
if (!forceSuccess) {
|
||||
throw new Error("无法执行强制捕获");
|
||||
}
|
||||
|
||||
await getCaptureData();
|
||||
alert.success(`强制捕获完成!`, 3000);
|
||||
} catch (error) {
|
||||
console.error("强制捕获失败:", error);
|
||||
alert.error(
|
||||
`强制捕获失败: ${error instanceof Error ? error.message : "未知错误"}`,
|
||||
3000,
|
||||
);
|
||||
} finally{
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
// 添加检查操作状态的计算属性
|
||||
const isOperationInProgress = computed(
|
||||
() => isApplying.value || isCapturing.value || operationMutex.isLocked(),
|
||||
);
|
||||
|
||||
// 添加生成测试数据的方法
|
||||
const generateTestData = () => {
|
||||
const sampleRate = LOGIC_ANALYZER_FREQUENCY; // 使用实际的逻辑分析仪频率
|
||||
const duration = 0.001; // 1ms的数据
|
||||
const points = Math.floor(sampleRate * duration);
|
||||
|
||||
const x = Array.from(
|
||||
{ length: points },
|
||||
(_, i) => (i * SAMPLE_PERIOD_NS) / 1000, // 时间轴,单位:微秒
|
||||
);
|
||||
|
||||
// Generate 8 channels with different digital patterns
|
||||
const y = [
|
||||
// Channel 0: Clock signal 1MHz
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => Math.floor((1_000_000 * i) / sampleRate) % 2,
|
||||
),
|
||||
// Channel 1: Clock/2 signal 500kHz
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => Math.floor((500_000 * i) / sampleRate) % 2,
|
||||
),
|
||||
// Channel 2: Clock/4 signal 250kHz
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => Math.floor((250_000 * i) / sampleRate) % 2,
|
||||
),
|
||||
// Channel 3: Clock/8 signal 125kHz
|
||||
Array.from(
|
||||
{ length: points },
|
||||
(_, i) => Math.floor((125_000 * i) / sampleRate) % 2,
|
||||
),
|
||||
// Channel 4: Data signal (pseudo-random pattern)
|
||||
Array.from({ length: points }, (_, i) =>
|
||||
Math.abs(Math.floor(Math.sin(i * 0.001) * 10) % 2),
|
||||
),
|
||||
// Channel 5: Enable signal (periodic pulse)
|
||||
Array.from({ length: points }, (_, i) =>
|
||||
Math.floor(i / 250) % 10 < 3 ? 1 : 0,
|
||||
),
|
||||
// Channel 6: Reset signal (occasional pulse)
|
||||
Array.from({ length: points }, (_, i) =>
|
||||
Math.floor(i / 1000) % 20 === 0 ? 1 : 0,
|
||||
),
|
||||
// Channel 7: Status signal (slow changing)
|
||||
Array.from({ length: points }, (_, i) => Math.floor(i / 5000) % 2),
|
||||
];
|
||||
|
||||
// 同时更新通道标签为更有意义的名称
|
||||
const testChannelNames = [
|
||||
"CLK",
|
||||
"CLK/2",
|
||||
"CLK/4",
|
||||
"CLK/8",
|
||||
"PWM",
|
||||
"ENABLE",
|
||||
"RESET",
|
||||
"STATUS",
|
||||
];
|
||||
|
||||
channels.forEach((channel, index) => {
|
||||
channel.label = testChannelNames[index];
|
||||
});
|
||||
|
||||
// 设置逻辑数据
|
||||
setChannelDiv(8);
|
||||
setLogicData({ x, y, xUnit: "us" }); // 改为微秒单位
|
||||
|
||||
alert?.success("测试数据生成成功", 2000);
|
||||
};
|
||||
|
||||
return {
|
||||
// 原有的逻辑数据
|
||||
logicData,
|
||||
|
||||
// 触发设置状态
|
||||
currentGlobalMode,
|
||||
currentChannelDiv, // 导出当前通道组状态
|
||||
captureLength, // 导出捕获深度
|
||||
preCaptureLength, // 导出预捕获深度
|
||||
isApplying,
|
||||
isCapturing, // 导出捕获状态
|
||||
isOperationInProgress, // 导出操作进行状态
|
||||
channels,
|
||||
signalConfigs,
|
||||
enabledChannelCount,
|
||||
channelNames,
|
||||
enabledChannels,
|
||||
|
||||
// 选项数据
|
||||
globalModes,
|
||||
operators,
|
||||
signalValues,
|
||||
channelDivOptions, // 导出通道组选项
|
||||
captureLengthOptions, // 导出捕获深度选项
|
||||
preCaptureLengthOptions, // 导出预捕获深度选项
|
||||
|
||||
// 触发设置方法
|
||||
setChannelDiv, // 导出设置通道组方法
|
||||
setGlobalMode,
|
||||
resetConfiguration,
|
||||
setLogicData,
|
||||
startCapture,
|
||||
forceCapture,
|
||||
stopCapture,
|
||||
generateTestData,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export { useProvideLogicAnalyzer, useLogicAnalyzerState };
|
||||
238
src/components/LogicAnalyzer/LogicalWaveFormDisplay.vue
Normal file
238
src/components/LogicAnalyzer/LogicalWaveFormDisplay.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full"
|
||||
:class="{
|
||||
'h-48': !analyzer.logicData.value,
|
||||
'h-150': analyzer.logicData.value,
|
||||
}"
|
||||
>
|
||||
<v-chart
|
||||
v-if="analyzer.logicData.value"
|
||||
class="w-full h-full"
|
||||
:option="option"
|
||||
autoresize
|
||||
:update-options="updateOptions"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex flex-col gap-6 items-center justify-center"
|
||||
>
|
||||
<div class="text-center">
|
||||
<h3 class="text-xl font-semibold text-slate-600 mb-2">
|
||||
暂无逻辑分析数据
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, shallowRef } from "vue";
|
||||
import VChart from "vue-echarts";
|
||||
|
||||
// Echarts
|
||||
import { use } from "echarts/core";
|
||||
import { LineChart } from "echarts/charts";
|
||||
import {
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
AxisPointerComponent,
|
||||
ToolboxComponent,
|
||||
} from "echarts/components";
|
||||
import { CanvasRenderer } from "echarts/renderers";
|
||||
import type { ComposeOption } from "echarts/core";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import type {
|
||||
AxisPointerComponentOption,
|
||||
TooltipComponentOption,
|
||||
GridComponentOption,
|
||||
DataZoomComponentOption,
|
||||
} from "echarts/components";
|
||||
import type {
|
||||
ToolboxComponentOption,
|
||||
XAXisOption,
|
||||
YAXisOption,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { useLogicAnalyzerState } from "./LogicAnalyzerManager";
|
||||
import { useRequiredInjection } from "@/utils/Common";
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
use([
|
||||
TooltipComponent,
|
||||
ToolboxComponent,
|
||||
GridComponent,
|
||||
AxisPointerComponent,
|
||||
DataZoomComponent,
|
||||
LineChart,
|
||||
CanvasRenderer,
|
||||
]);
|
||||
|
||||
type EChartsOption = ComposeOption<
|
||||
| AxisPointerComponentOption
|
||||
| TooltipComponentOption
|
||||
| ToolboxComponentOption
|
||||
| GridComponentOption
|
||||
| DataZoomComponentOption
|
||||
| LineSeriesOption
|
||||
>;
|
||||
|
||||
const analyzer = useRequiredInjection(useLogicAnalyzerState);
|
||||
|
||||
// 添加更新选项来减少重绘
|
||||
const updateOptions = shallowRef({
|
||||
notMerge: false,
|
||||
lazyUpdate: true,
|
||||
silent: false,
|
||||
});
|
||||
|
||||
const option = computed((): EChartsOption => {
|
||||
if (isUndefined(analyzer.logicData.value)) return {};
|
||||
|
||||
// 只获取启用的通道
|
||||
const enabledChannels = analyzer.enabledChannels.value;
|
||||
const enabledChannelIndices = analyzer.channels
|
||||
.map((channel, index) => (channel.enabled ? index : -1))
|
||||
.filter((index) => index !== -1);
|
||||
|
||||
const channelCount = enabledChannels.length;
|
||||
const channelSpacing = 2; // 每个通道之间的间距
|
||||
|
||||
// 如果没有启用的通道,返回空配置
|
||||
if (channelCount === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 使用单个网格
|
||||
const grids: GridComponentOption[] = [
|
||||
{
|
||||
left: "5%",
|
||||
right: "5%",
|
||||
top: "5%",
|
||||
bottom: "15%",
|
||||
},
|
||||
];
|
||||
|
||||
// 单个X轴
|
||||
const xAxis: XAXisOption[] = [
|
||||
{
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: analyzer.logicData.value.x.map((x) => x.toFixed(3)),
|
||||
axisLabel: {
|
||||
formatter: (value: string) =>
|
||||
analyzer.logicData.value
|
||||
? `${value}${analyzer.logicData.value.xUnit}`
|
||||
: `${value}`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 单个Y轴,范围根据启用通道数量调整
|
||||
const yAxis: YAXisOption[] = [
|
||||
{
|
||||
type: "value",
|
||||
min: -0.5,
|
||||
max: channelCount * channelSpacing - 0.5,
|
||||
interval: channelSpacing,
|
||||
axisLabel: {
|
||||
formatter: (value: number) => {
|
||||
const channelIndex = Math.round(value / channelSpacing);
|
||||
return channelIndex < channelCount
|
||||
? enabledChannels[channelIndex].label
|
||||
: "";
|
||||
},
|
||||
},
|
||||
splitLine: { show: false },
|
||||
},
|
||||
];
|
||||
|
||||
// 创建系列数据,只包含启用的通道
|
||||
const series: LineSeriesOption[] = enabledChannelIndices.map(
|
||||
(originalIndex: number, displayIndex: number) => ({
|
||||
name: enabledChannels[displayIndex].label,
|
||||
type: "line",
|
||||
data: analyzer.logicData.value!.y[originalIndex].map(
|
||||
(value: number) => value + displayIndex * channelSpacing + 0.2,
|
||||
),
|
||||
step: "end",
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: enabledChannels[displayIndex].color,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.3,
|
||||
origin: displayIndex * channelSpacing,
|
||||
color: enabledChannels[displayIndex].color,
|
||||
},
|
||||
symbol: "none",
|
||||
// 优化性能配置
|
||||
sampling: "lttb",
|
||||
// 减少动画以避免闪烁
|
||||
animation: false,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
// 全局动画配置
|
||||
animation: false,
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "line",
|
||||
label: {
|
||||
backgroundColor: "#6a7985",
|
||||
},
|
||||
// 减少axisPointer的动画
|
||||
animation: false,
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
if (Array.isArray(params) && params.length > 0) {
|
||||
const timeValue = analyzer.logicData.value!.x[params[0].dataIndex];
|
||||
const dataIndex = params[0].dataIndex;
|
||||
|
||||
let tooltip = `Time: ${timeValue.toFixed(3)}${analyzer.logicData.value!.xUnit}<br/>`;
|
||||
|
||||
// 只显示启用通道在当前时间点的原始数值(0或1)
|
||||
enabledChannelIndices.forEach(
|
||||
(originalIndex: number, displayIndex: number) => {
|
||||
const channelName = enabledChannels[displayIndex].label;
|
||||
const originalValue =
|
||||
analyzer.logicData.value!.y[originalIndex][dataIndex];
|
||||
tooltip += `${channelName}: ${originalValue}<br/>`;
|
||||
},
|
||||
);
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
// 优化tooltip性能
|
||||
hideDelay: 100,
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
restore: {},
|
||||
},
|
||||
},
|
||||
grid: grids,
|
||||
xAxis: xAxis,
|
||||
yAxis: yAxis,
|
||||
dataZoom: [
|
||||
{
|
||||
show: true,
|
||||
realtime: true,
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
type: "inside",
|
||||
realtime: true,
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
series: series,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
202
src/components/LogicAnalyzer/TriggerSettings.vue
Normal file
202
src/components/LogicAnalyzer/TriggerSettings.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 通道配置 -->
|
||||
<div class="form-control">
|
||||
<!-- 全局触发模式选择和通道组配置 -->
|
||||
<div class="flex flex-col lg:flex-row justify-between gap-4 my-4 mx-2">
|
||||
<!-- 左侧:全局触发模式和通道组选择 -->
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<label class="label">
|
||||
<span class="label-text text-sm">全局触发逻辑</span>
|
||||
</label>
|
||||
<select
|
||||
v-model="currentGlobalMode"
|
||||
@change="setGlobalMode(currentGlobalMode)"
|
||||
class="select select-sm select-bordered"
|
||||
>
|
||||
<option
|
||||
v-for="mode in globalModes"
|
||||
:key="mode.value"
|
||||
:value="mode.value"
|
||||
>
|
||||
{{ mode.label }} - {{ mode.description }}
|
||||
</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="currentChannelDiv"
|
||||
@change="setChannelDiv(currentChannelDiv)"
|
||||
class="select select-sm select-bordered"
|
||||
>
|
||||
<option
|
||||
v-for="option in channelDivOptions"
|
||||
: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="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>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 通道列表 -->
|
||||
<div class="space-y-2">
|
||||
<!-- 表头 -->
|
||||
<div
|
||||
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-32">标签</span>
|
||||
<span class="w-16">颜色</span>
|
||||
<span class="w-32">触发操作</span>
|
||||
<span class="w-32">触发值</span>
|
||||
</div>
|
||||
|
||||
<!-- 通道配置网格 - 根据当前通道组动态显示 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="(channel, index) in channels.filter(ch => ch.enabled)"
|
||||
:key="index"
|
||||
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{{ channels.indexOf(channel) }}</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-32">
|
||||
<input
|
||||
type="text"
|
||||
v-model="channel.label"
|
||||
:placeholder="`通道 ${channels.indexOf(channel)}`"
|
||||
class="input input-sm input-bordered w-full"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 触发操作符选择 -->
|
||||
<select
|
||||
v-model="signalConfigs[channels.indexOf(channel)].operator"
|
||||
class="select select-sm select-bordered w-32"
|
||||
>
|
||||
<option
|
||||
v-for="op in operators"
|
||||
:key="op.value"
|
||||
:value="op.value"
|
||||
>
|
||||
{{ op.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- 触发信号值选择 -->
|
||||
<select
|
||||
v-model="signalConfigs[channels.indexOf(channel)].value"
|
||||
class="select select-sm select-bordered w-32"
|
||||
>
|
||||
<option
|
||||
v-for="val in signalValues"
|
||||
:key="val.value"
|
||||
:value="val.value"
|
||||
>
|
||||
{{ val.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当没有启用通道时的提示 -->
|
||||
<div v-if="enabledChannelCount === 0" class="text-center py-8 text-base-content/60">
|
||||
<p class="text-lg font-medium">未启用任何通道</p>
|
||||
<p class="text-sm">请选择通道组来配置逻辑分析仪</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRequiredInjection } from "@/utils/Common";
|
||||
import { useLogicAnalyzerState } from "./LogicAnalyzerManager";
|
||||
|
||||
const {
|
||||
currentGlobalMode,
|
||||
currentChannelDiv,
|
||||
captureLength,
|
||||
preCaptureLength,
|
||||
isApplying,
|
||||
channels,
|
||||
signalConfigs,
|
||||
enabledChannelCount,
|
||||
globalModes,
|
||||
operators,
|
||||
signalValues,
|
||||
channelDivOptions,
|
||||
captureLengthOptions,
|
||||
preCaptureLengthOptions,
|
||||
setChannelDiv,
|
||||
setGlobalMode,
|
||||
resetConfiguration,
|
||||
} = useRequiredInjection(useLogicAnalyzerState);
|
||||
</script>
|
||||
5
src/components/LogicAnalyzer/index.ts
Normal file
5
src/components/LogicAnalyzer/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import LogicalWaveFormDisplay from "./LogicalWaveFormDisplay.vue";
|
||||
|
||||
export { LogicalWaveFormDisplay };
|
||||
export { default as TriggerSettings } from './TriggerSettings.vue'
|
||||
export { useProvideLogicAnalyzer , useLogicAnalyzerState } from './LogicAnalyzerManager.ts'
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div class="card card-dash h-80 w-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title place-self-center my-3 text-2xl">User Login</h1>
|
||||
<div class="flex flex-col w-full h-full">
|
||||
<label class="input w-full my-3">
|
||||
<img class="h-[1em] opacity-50" src="@/assets/user.svg" alt="User img" />
|
||||
<input type="text" class="grow" placeholder="用户名" />
|
||||
</label>
|
||||
<label class="input w-full my-3">
|
||||
<img class="h-[1em] opacity-50" src="@/assets/pwd.svg" alt="User img" />
|
||||
<input type="text" class="grow" placeholder="密码" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<RouterLink class="flex justify-end mx-3" to="/">忘记密码?</RouterLink>
|
||||
</div>
|
||||
<div class="card-actions flex items-end my-3">
|
||||
<button class="btn flex-1">注册</button>
|
||||
<button class="btn btn-primary flex-3">登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped>
|
||||
@import "@/assets/main.css";
|
||||
</style>
|
||||
@@ -6,6 +6,9 @@ import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github.css'; // 亮色主题
|
||||
// 导入主题存储
|
||||
import { useThemeStore } from '@/stores/theme';
|
||||
// 导入ExamClient用于获取图片资源
|
||||
import { ExamClient } from '@/APIClient';
|
||||
import { AuthManager } from '@/utils/AuthManager';
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
@@ -15,6 +18,10 @@ const props = defineProps({
|
||||
removeFirstH1: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
examId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
@@ -23,6 +30,42 @@ const themeStore = useThemeStore();
|
||||
// 使用 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.createAuthenticatedExamClient();
|
||||
const resources = await client.getExamResourceList(examId, 'images');
|
||||
|
||||
// 查找匹配的图片资源
|
||||
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.createAuthenticatedExamClient();
|
||||
const response = await client.getExamResourceById(parseInt(resourceId));
|
||||
|
||||
if (response && response.data) {
|
||||
return URL.createObjectURL(response.data);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('获取图片数据失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听主题变化
|
||||
watch(() => themeStore.currentTheme, () => {
|
||||
// 主题变化时更新代码高亮样式
|
||||
@@ -54,6 +97,27 @@ const renderedContent = computed(() => {
|
||||
// 创建自定义渲染器
|
||||
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) => {
|
||||
// 确保语言参数是字符串
|
||||
@@ -67,14 +131,60 @@ const renderedContent = computed(() => {
|
||||
return `<pre class="hljs" data-language="${validLanguage}"><code class="language-${validLanguage}">${highlightedCode}</code></pre>`;
|
||||
};
|
||||
|
||||
// 设置 marked 选项
|
||||
marked.use({
|
||||
// 设置 marked 选项并解析内容
|
||||
let html = marked.parse(processedContent, {
|
||||
renderer: renderer,
|
||||
gfm: 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;
|
||||
});
|
||||
|
||||
// 页面挂载后,确保应用正确的主题样式
|
||||
|
||||
@@ -2,74 +2,61 @@
|
||||
<div class="navbar bg-base-100 shadow-xl">
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button"
|
||||
class="btn btn-ghost hover:bg-primary hover:bg-opacity-20 transition-all duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" />
|
||||
</svg>
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="btn btn-ghost hover:bg-primary hover:bg-opacity-20 transition-all duration-300"
|
||||
>
|
||||
<MenuIcon />
|
||||
</div>
|
||||
<ul tabindex="0"
|
||||
class="menu menu-sm dropdown-content bg-base-100 rounded-lg z-50 mt-3 w-52 p-2 shadow-lg transition-all duration-300 ease-in-out">
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu menu-sm dropdown-content bg-base-200 rounded-lg z-50 mt-3 w-52 p-2 shadow-lg transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<li class="my-1 hover:translate-x-1 transition-all duration-300">
|
||||
<router-link to="/" class="text-base font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
<polyline points="9 22 9 12 15 12 15 22" />
|
||||
</svg>
|
||||
<House class="icon" />
|
||||
首页
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="my-1 hover:translate-x-1 transition-all duration-300">
|
||||
<router-link to="/user" class="text-base font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
<User class="icon" />
|
||||
用户界面
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="my-1 hover:translate-x-1 transition-all duration-300">
|
||||
<router-link to="/project" class="text-base font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
|
||||
</svg>
|
||||
<PencilRuler class="icon" />
|
||||
工程界面
|
||||
</router-link>
|
||||
</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">
|
||||
<router-link to="/test" class="text-base font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
<FlaskConical class="icon" />
|
||||
测试功能
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="my-1 hover:translate-x-1 transition-all duration-300">
|
||||
<router-link to="/markdown-test" class="text-base font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||||
<polyline points="10 9 9 9 8 9"></polyline>
|
||||
</svg>
|
||||
<FileText class="icon" />
|
||||
Markdown测试
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="my-1 hover:translate-x-1 transition-all duration-300">
|
||||
<a href="http://localhost:5000/swagger" target="_self" rel="noopener noreferrer"
|
||||
class="text-base font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 3h18v18H3z"></path>
|
||||
<path d="M8 8h8v8H8z" fill="currentColor"></path>
|
||||
</svg>
|
||||
<a
|
||||
href="http://localhost:5000/swagger"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
class="text-base font-medium"
|
||||
>
|
||||
<BookOpenText class="icon" />
|
||||
OpenAPI文档
|
||||
</a>
|
||||
</li>
|
||||
@@ -77,15 +64,64 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-center lg:flex">
|
||||
<router-link to="/" class="btn btn-ghost text-xl font-bold transition-all duration-300 hover:scale-105">
|
||||
<router-link
|
||||
to="/"
|
||||
class="btn btn-ghost text-xl font-bold transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<span class="text-primary">FPGA</span> Web Lab
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<router-link to="/login"
|
||||
class="btn btn-primary text-base-100 transition-all duration-300 hover:scale-105 hover:shadow-lg mr-3">
|
||||
登录
|
||||
</router-link>
|
||||
<!-- 未登录状态 -->
|
||||
<template v-if="!isLoggedIn">
|
||||
<router-link
|
||||
to="/login"
|
||||
class="btn btn-primary text-base-100 transition-all duration-300 hover:scale-105 hover:shadow-lg mr-3"
|
||||
>
|
||||
登录
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<!-- 已登录状态 -->
|
||||
<template v-else>
|
||||
<div class="dropdown dropdown-end mr-3">
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="btn btn-ghost hover:bg-primary hover:bg-opacity-20 transition-all duration-300 flex items-center gap-2"
|
||||
>
|
||||
<User class="h-5 w-5" />
|
||||
<span class="font-medium">{{ userName }}</span>
|
||||
<ChevronDownIcon
|
||||
class="icon transition-transform duration-300 dropdown-icon"
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu menu-sm dropdown-content bg-base-200 rounded-lg z-50 mt-3 w-48 p-2 shadow-lg transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<li class="my-1 hover:translate-x-1 transition-all duration-300">
|
||||
<router-link
|
||||
to="/user"
|
||||
class="text-base font-medium flex items-center gap-2"
|
||||
>
|
||||
<User class="icon" />
|
||||
用户中心
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="my-1 hover:translate-x-1 transition-all duration-300">
|
||||
<button
|
||||
@click="handleLogout"
|
||||
class="text-base font-medium flex items-center gap-2 w-full text-left hover:bg-error hover:text-error-content"
|
||||
>
|
||||
<LogOutIcon class="icon" />
|
||||
退出登录
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="ml-2 transition-all duration-500 hover:rotate-12">
|
||||
<ThemeControlButton />
|
||||
</div>
|
||||
@@ -94,9 +130,78 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onBeforeRouteUpdate, useRouter } from "vue-router";
|
||||
import ThemeControlButton from "./ThemeControlButton.vue";
|
||||
import {
|
||||
MenuIcon,
|
||||
FileText,
|
||||
BookOpenText,
|
||||
FlaskConical,
|
||||
House,
|
||||
User,
|
||||
PencilRuler,
|
||||
LogOutIcon,
|
||||
ChevronDownIcon,
|
||||
} from "lucide-vue-next";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 响应式数据
|
||||
const userName = ref<string>("");
|
||||
const isUserMenuOpen = ref<boolean>(false);
|
||||
const isLoggedIn = ref<boolean>(false); // 改为响应式变量
|
||||
|
||||
// 方法
|
||||
const loadUserInfo = async () => {
|
||||
try {
|
||||
const authenticated = await AuthManager.isAuthenticated();
|
||||
if (authenticated) {
|
||||
const client = AuthManager.createAuthenticatedDataClient();
|
||||
const userInfo = await client.getUserInfo();
|
||||
userName.value = userInfo.name;
|
||||
isLoggedIn.value = true;
|
||||
} else {
|
||||
userName.value = "";
|
||||
isLoggedIn.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load user info:", error);
|
||||
// 如果获取用户信息失败,清除token
|
||||
AuthManager.clearToken();
|
||||
userName.value = "";
|
||||
isLoggedIn.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
AuthManager.logout();
|
||||
userName.value = "";
|
||||
isLoggedIn.value = false;
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
loadUserInfo();
|
||||
|
||||
// 监听路由变化
|
||||
router.afterEach(() => {
|
||||
loadUserInfo();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="postcss">
|
||||
@import "../assets/main.css";
|
||||
|
||||
.icon {
|
||||
@apply h-5 w-5 opacity-70;
|
||||
}
|
||||
|
||||
.dropdown[open] .dropdown-icon,
|
||||
.dropdown:focus-within .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
|
||||
287
src/components/Oscilloscope/OscilloscopeManager.ts
Normal file
287
src/components/Oscilloscope/OscilloscopeManager.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { autoResetRef, createInjectionState } from "@vueuse/core";
|
||||
import { shallowRef, reactive, ref, computed } from "vue";
|
||||
import { Mutex } from "async-mutex";
|
||||
import {
|
||||
OscilloscopeFullConfig,
|
||||
OscilloscopeDataResponse,
|
||||
} from "@/APIClient";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
import { useAlertStore } from "@/components/Alert";
|
||||
import { useRequiredInjection } from "@/utils/Common";
|
||||
|
||||
export type OscilloscopeDataType = {
|
||||
x: number[];
|
||||
y: number[] | number[][];
|
||||
xUnit: "s" | "ms" | "us" | "ns";
|
||||
yUnit: "V" | "mV" | "uV";
|
||||
adFrequency: number;
|
||||
adVpp: number;
|
||||
adMax: number;
|
||||
adMin: number;
|
||||
};
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG: OscilloscopeFullConfig = new OscilloscopeFullConfig({
|
||||
captureEnabled: false,
|
||||
triggerLevel: 128,
|
||||
triggerRisingEdge: true,
|
||||
horizontalShift: 0,
|
||||
decimationRate: 50,
|
||||
autoRefreshRAM: false,
|
||||
});
|
||||
|
||||
// 采样频率常量(后端返回)
|
||||
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(() => {
|
||||
const oscData = shallowRef<OscilloscopeDataType>();
|
||||
const alert = useRequiredInjection(useAlertStore);
|
||||
|
||||
// 互斥锁
|
||||
const operationMutex = new Mutex();
|
||||
|
||||
// 状态
|
||||
const isApplying = ref(false);
|
||||
const isCapturing = ref(false);
|
||||
|
||||
// 配置
|
||||
const config = reactive<OscilloscopeFullConfig>(new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }));
|
||||
|
||||
// 采样点数(由后端数据决定)
|
||||
const sampleCount = ref(0);
|
||||
|
||||
// 采样周期(ns),由adFrequency计算
|
||||
const samplePeriodNs = computed(() =>
|
||||
oscData.value?.adFrequency ? 1_000_000_000 / oscData.value.adFrequency : 200
|
||||
);
|
||||
|
||||
// 应用配置
|
||||
const applyConfiguration = async () => {
|
||||
if (operationMutex.isLocked()) {
|
||||
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
|
||||
return;
|
||||
}
|
||||
const release = await operationMutex.acquire();
|
||||
isApplying.value = true;
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
const success = await client.initialize({ ...config });
|
||||
if (success) {
|
||||
alert.success("示波器配置已应用", 2000);
|
||||
} else {
|
||||
throw new Error("应用失败");
|
||||
}
|
||||
} catch (error) {
|
||||
alert.error("应用配置失败", 3000);
|
||||
} finally {
|
||||
isApplying.value = false;
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
// 重置配置
|
||||
const resetConfiguration = () => {
|
||||
Object.assign(config, { ...DEFAULT_CONFIG });
|
||||
alert.info("配置已重置", 2000);
|
||||
};
|
||||
|
||||
const clearOscilloscopeData = () => {
|
||||
oscData.value = undefined;
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const getOscilloscopeData = async () => {
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
const resp: OscilloscopeDataResponse = await client.getData();
|
||||
|
||||
// 解析波形数据
|
||||
const binaryString = atob(resp.waveformData);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
sampleCount.value = bytes.length;
|
||||
|
||||
// 构建时间轴
|
||||
const x = Array.from(
|
||||
{ length: bytes.length },
|
||||
(_, i) => (i * samplePeriodNs.value) / 1000 // us
|
||||
);
|
||||
const y = Array.from(bytes);
|
||||
|
||||
oscData.value = {
|
||||
x,
|
||||
y,
|
||||
xUnit: "us",
|
||||
yUnit: "V",
|
||||
adFrequency: resp.adFrequency,
|
||||
adVpp: resp.adVpp,
|
||||
adMax: resp.adMax,
|
||||
adMin: resp.adMin,
|
||||
};
|
||||
} catch (error) {
|
||||
alert.error("获取示波器数据失败", 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// 定时器引用
|
||||
let refreshIntervalId: number | undefined;
|
||||
// 刷新间隔(毫秒),可根据需要调整
|
||||
const refreshIntervalMs = ref(1000);
|
||||
|
||||
// 定时刷新函数
|
||||
const startAutoRefresh = () => {
|
||||
if (refreshIntervalId !== undefined) return;
|
||||
refreshIntervalId = window.setInterval(async () => {
|
||||
await refreshRAM();
|
||||
await getOscilloscopeData();
|
||||
}, refreshIntervalMs.value);
|
||||
};
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshIntervalId !== undefined) {
|
||||
clearInterval(refreshIntervalId);
|
||||
refreshIntervalId = undefined;
|
||||
isCapturing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 启动捕获
|
||||
const startCapture = async () => {
|
||||
if (operationMutex.isLocked()) {
|
||||
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
|
||||
return;
|
||||
}
|
||||
isCapturing.value = true;
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
const started = await client.startCapture();
|
||||
if (!started) throw new Error("无法启动捕获");
|
||||
alert.info("开始捕获...", 2000);
|
||||
|
||||
// 启动定时刷新
|
||||
startAutoRefresh();
|
||||
} catch (error) {
|
||||
alert.error("捕获失败", 3000);
|
||||
isCapturing.value = false;
|
||||
stopAutoRefresh();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
// 停止捕获
|
||||
const stopCapture = async () => {
|
||||
if (!isCapturing.value) {
|
||||
alert.warn("当前没有正在进行的捕获操作", 2000);
|
||||
return;
|
||||
}
|
||||
isCapturing.value = false;
|
||||
stopAutoRefresh();
|
||||
const release = await operationMutex.acquire();
|
||||
try {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
const stopped = await client.stopCapture();
|
||||
if (!stopped) throw new Error("无法停止捕获");
|
||||
alert.info("捕获已停止", 2000);
|
||||
} catch (error) {
|
||||
alert.error("停止捕获失败", 3000);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
// 更新触发参数
|
||||
const updateTrigger = async (level: number, risingEdge: boolean) => {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
try {
|
||||
const ok = await client.updateTrigger(level, risingEdge);
|
||||
if (ok) {
|
||||
config.triggerLevel = level;
|
||||
config.triggerRisingEdge = risingEdge;
|
||||
alert.success("触发参数已更新", 2000);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
alert.error("更新触发参数失败", 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新采样参数
|
||||
const updateSampling = async (horizontalShift: number, decimationRate: number) => {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
try {
|
||||
const ok = await client.updateSampling(horizontalShift, decimationRate);
|
||||
if (ok) {
|
||||
config.horizontalShift = horizontalShift;
|
||||
config.decimationRate = decimationRate;
|
||||
alert.success("采样参数已更新", 2000);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
alert.error("更新采样参数失败", 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 手动刷新RAM
|
||||
const refreshRAM = async () => {
|
||||
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
|
||||
try {
|
||||
const ok = await client.refreshRAM();
|
||||
if (ok) {
|
||||
// alert.success("RAM已刷新", 2000);
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
} catch {
|
||||
alert.error("刷新RAM失败", 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 生成测试数据
|
||||
const generateTestData = () => {
|
||||
const freq = 5_000_000;
|
||||
const duration = 0.001; // 1ms
|
||||
const points = Math.floor(freq * duration);
|
||||
const x = Array.from({ length: points }, (_, i) => (i * 1_000_000_000 / freq) / 1000);
|
||||
const y = Array.from({ length: points }, (_, i) =>
|
||||
Math.floor(Math.sin(i * 0.01) * 127 + 128)
|
||||
);
|
||||
oscData.value = {
|
||||
x,
|
||||
y,
|
||||
xUnit: "us",
|
||||
yUnit: "V",
|
||||
adFrequency: freq,
|
||||
adVpp: 2.0,
|
||||
adMax: 255,
|
||||
adMin: 0,
|
||||
};
|
||||
alert.success("测试数据生成成功", 2000);
|
||||
};
|
||||
|
||||
return {
|
||||
oscData,
|
||||
config,
|
||||
isApplying,
|
||||
isCapturing,
|
||||
sampleCount,
|
||||
samplePeriodNs,
|
||||
refreshIntervalMs,
|
||||
|
||||
applyConfiguration,
|
||||
resetConfiguration,
|
||||
clearOscilloscopeData,
|
||||
getOscilloscopeData,
|
||||
startCapture,
|
||||
stopCapture,
|
||||
updateTrigger,
|
||||
updateSampling,
|
||||
refreshRAM,
|
||||
generateTestData,
|
||||
};
|
||||
});
|
||||
|
||||
export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG };
|
||||
214
src/components/Oscilloscope/OscilloscopeWaveformDisplay.vue
Normal file
214
src/components/Oscilloscope/OscilloscopeWaveformDisplay.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="w-full h-100 flex flex-col">
|
||||
<!-- 原有内容 -->
|
||||
<v-chart v-if="hasData" class="w-full h-full" :option="option" autoresize />
|
||||
<div v-else class="w-full h-full flex flex-col gap-4 items-center justify-center text-gray-500">
|
||||
<span> 暂无数据 </span>
|
||||
<!-- 采集控制按钮 -->
|
||||
<div class="flex justify-center items-center mb-2">
|
||||
<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':
|
||||
!oscManager.isCapturing.value,
|
||||
'from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 focus:ring-red-300':
|
||||
oscManager.isCapturing.value,
|
||||
}" @click="
|
||||
oscManager.isCapturing.value
|
||||
? oscManager.stopCapture()
|
||||
: oscManager.startCapture()
|
||||
">
|
||||
<span class="flex items-center gap-2">
|
||||
<template v-if="oscManager.isCapturing.value">
|
||||
<Square class="w-5 h-5" />
|
||||
停止采集
|
||||
</template>
|
||||
<template v-else>
|
||||
<Play class="w-5 h-5" />
|
||||
开始采集
|
||||
</template>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { forEach } from "lodash";
|
||||
import VChart from "vue-echarts";
|
||||
import { useOscilloscopeState } from "./OscilloscopeManager";
|
||||
|
||||
// Echarts
|
||||
import { use } from "echarts/core";
|
||||
import { LineChart } from "echarts/charts";
|
||||
import {
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
GridComponent,
|
||||
} from "echarts/components";
|
||||
import { CanvasRenderer } from "echarts/renderers";
|
||||
import type { ComposeOption } from "echarts/core";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import type {
|
||||
TooltipComponentOption,
|
||||
LegendComponentOption,
|
||||
ToolboxComponentOption,
|
||||
DataZoomComponentOption,
|
||||
GridComponentOption,
|
||||
} from "echarts/components";
|
||||
import { useRequiredInjection } from "@/utils/Common";
|
||||
import { Play, Square } from "lucide-vue-next";
|
||||
|
||||
use([
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
ToolboxComponent,
|
||||
DataZoomComponent,
|
||||
GridComponent,
|
||||
LineChart,
|
||||
CanvasRenderer,
|
||||
]);
|
||||
|
||||
type EChartsOption = ComposeOption<
|
||||
| TooltipComponentOption
|
||||
| LegendComponentOption
|
||||
| ToolboxComponentOption
|
||||
| DataZoomComponentOption
|
||||
| GridComponentOption
|
||||
| LineSeriesOption
|
||||
>;
|
||||
|
||||
// 使用 manager 获取 oscilloscope 数据
|
||||
const oscManager = useRequiredInjection(useOscilloscopeState);
|
||||
|
||||
const oscData = computed(() => oscManager.oscData.value);
|
||||
|
||||
const hasData = computed(() => {
|
||||
return (
|
||||
oscData.value &&
|
||||
oscData.value.x &&
|
||||
oscData.value.y &&
|
||||
oscData.value.x.length > 0 &&
|
||||
(Array.isArray(oscData.value.y[0])
|
||||
? oscData.value.y.some((channel: any) => channel.length > 0)
|
||||
: oscData.value.y.length > 0)
|
||||
);
|
||||
});
|
||||
|
||||
const option = computed((): EChartsOption => {
|
||||
if (!oscData.value || !oscData.value.x || !oscData.value.y) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const isCapturing = oscManager.isCapturing.value;
|
||||
|
||||
const series: LineSeriesOption[] = [];
|
||||
|
||||
// 兼容单通道和多通道,确保 yChannels 为 number[]
|
||||
const yChannels: number[][] = Array.isArray(oscData.value.y[0])
|
||||
? (oscData.value.y as number[][])
|
||||
: [oscData.value.y as number[]];
|
||||
|
||||
forEach(yChannels, (yData, index) => {
|
||||
if (!oscData.value || !yData) return;
|
||||
const seriesData = oscData.value.x.map((xValue, i) => [
|
||||
xValue,
|
||||
yData && yData[i] !== undefined ? yData[i] : 0,
|
||||
]);
|
||||
series.push({
|
||||
type: "line",
|
||||
name: `通道 ${index + 1}`,
|
||||
data: seriesData,
|
||||
smooth: false,
|
||||
symbol: "none",
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
// 关闭系列动画
|
||||
animation: !isCapturing,
|
||||
animationDuration: isCapturing ? 0 : 1000,
|
||||
animationEasing: isCapturing ? "linear" : "cubicOut",
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
grid: {
|
||||
left: "10%",
|
||||
right: "10%",
|
||||
top: "15%",
|
||||
bottom: "25%",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
formatter: (params: any) => {
|
||||
if (!oscData.value) return "";
|
||||
let result = `时间: ${params[0].data[0].toFixed(2)} ${oscData.value.xUnit}<br/>`;
|
||||
params.forEach((param: any) => {
|
||||
result += `${param.seriesName}: ${param.data[1].toFixed(3)} ${oscData.value?.yUnit ?? ""}<br/>`;
|
||||
});
|
||||
return result;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
top: "5%",
|
||||
data: series.map((s) => s.name) as string[],
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
restore: {},
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: "inside",
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
xAxis: {
|
||||
type: "value",
|
||||
name: oscData.value ? `时间 (${oscData.value.xUnit})` : "时间",
|
||||
nameLocation: "middle",
|
||||
nameGap: 30,
|
||||
axisLine: {
|
||||
show: true,
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: oscData.value ? `电压 (${oscData.value.yUnit})` : "电压",
|
||||
nameLocation: "middle",
|
||||
nameGap: 40,
|
||||
axisLine: {
|
||||
show: true,
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
// 全局动画开关
|
||||
animation: !isCapturing,
|
||||
animationDuration: isCapturing ? 0 : 1000,
|
||||
animationEasing: isCapturing ? "linear" : "cubicOut",
|
||||
series: series,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
3
src/components/Oscilloscope/index.ts
Normal file
3
src/components/Oscilloscope/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import OscilloscopeWaveformDisplay from "./OscilloscopeWaveformDisplay.vue";
|
||||
|
||||
export { OscilloscopeWaveformDisplay };
|
||||
@@ -98,7 +98,6 @@ const props = defineProps<{
|
||||
const propertySectionExpanded = ref(false); // 基本属性区域默认展开
|
||||
const pinsSectionExpanded = ref(false); // 引脚配置区域默认折叠
|
||||
const componentCapsExpanded = ref(true); // 组件功能区域默认展开
|
||||
const wireSectionExpanded = ref(false); // 连线管理区域默认折叠
|
||||
|
||||
const componentCaps = useTemplateRef("ComponentCapabilities");
|
||||
|
||||
|
||||
@@ -31,8 +31,20 @@
|
||||
|
||||
<!-- 标题覆盖层 -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-base-300 to-transparent">
|
||||
<h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3>
|
||||
<p class="text-sm opacity-80 truncate">{{ tutorial.description }}</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-lg font-bold text-base-content">{{ tutorial.title }}</h3>
|
||||
<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>
|
||||
@@ -54,6 +66,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { AuthManager } from '@/utils/AuthManager';
|
||||
import type { ExamSummary } from '@/APIClient';
|
||||
|
||||
// 接口定义
|
||||
interface Tutorial {
|
||||
@@ -61,7 +75,7 @@ interface Tutorial {
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnail?: string;
|
||||
docPath: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
// Props
|
||||
@@ -81,87 +95,92 @@ let autoRotationTimer: number | null = null;
|
||||
// 处理卡片点击
|
||||
const handleCardClick = (index: number, tutorialId: string) => {
|
||||
if (index === currentIndex.value) {
|
||||
goToTutorial(tutorialId);
|
||||
goToExam(tutorialId);
|
||||
} else {
|
||||
setActiveCard(index);
|
||||
}
|
||||
};
|
||||
|
||||
// 从 public/doc 目录加载例程信息
|
||||
// 从数据库加载实验数据
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 尝试从API获取教程目录
|
||||
let tutorialIds: string[] = [];
|
||||
try {
|
||||
const response = await fetch('/api/tutorial');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
tutorialIds = data.tutorials || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('无法从API获取教程目录,使用默认值:', error);
|
||||
console.log('正在从数据库加载实验数据...');
|
||||
|
||||
// 创建认证客户端
|
||||
const client = AuthManager.createAuthenticatedExamClient();
|
||||
|
||||
// 获取实验列表
|
||||
const examList: ExamSummary[] = await client.getExamList();
|
||||
|
||||
// 筛选可见的实验并转换为Tutorial格式
|
||||
const visibleExams = examList
|
||||
.filter(exam => exam.isVisibleToUsers)
|
||||
.slice(0, 6); // 限制轮播显示最多6个实验
|
||||
|
||||
if (visibleExams.length === 0) {
|
||||
console.warn('没有找到可见的实验');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果API调用失败或返回空列表,使用默认值
|
||||
if (tutorialIds.length === 0) {
|
||||
console.log('使用默认教程列表');
|
||||
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`; // 默认使用第一张图片作为缩略图
|
||||
// 转换数据格式并获取封面图片
|
||||
const tutorialPromises = visibleExams.map(async (exam) => {
|
||||
let thumbnail: string | undefined;
|
||||
|
||||
try {
|
||||
// 尝试读取文档内容获取标题
|
||||
const response = await fetch(`/doc/${id}/doc.md`);
|
||||
if (response.ok) {
|
||||
const text = await response.text();
|
||||
// 从Markdown提取标题
|
||||
const titleMatch = text.match(/^#\s+(.+)$/m);
|
||||
if (titleMatch && titleMatch[1]) {
|
||||
title = titleMatch[1].trim();
|
||||
}
|
||||
|
||||
// 提取第一段作为描述
|
||||
const descMatch = text.match(/\n\n([^#\n][^\n]+)/);
|
||||
if (descMatch && descMatch[1]) {
|
||||
description = descMatch[1].substring(0, 100).trim();
|
||||
if (description.length === 100) description += '...';
|
||||
}
|
||||
// 获取实验的封面资源
|
||||
const resourceList = await client.getExamResourceList(exam.id, 'cover');
|
||||
if (resourceList && resourceList.length > 0) {
|
||||
// 使用第一个封面资源
|
||||
const coverResource = resourceList[0];
|
||||
const fileResponse = await client.getExamResourceById(coverResource.id);
|
||||
// 创建Blob URL作为缩略图
|
||||
thumbnail = URL.createObjectURL(fileResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`无法读取例程${id}的文档内容:`, error);
|
||||
console.warn(`无法获取实验${exam.id}的封面图片:`, error);
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
id: exam.id,
|
||||
title: exam.name,
|
||||
description: '点击查看实验详情',
|
||||
thumbnail,
|
||||
docPath: `/doc/${id}/doc.md`
|
||||
tags: exam.tags || []
|
||||
};
|
||||
});
|
||||
|
||||
tutorials.value = await Promise.all(tutorialPromises);
|
||||
|
||||
console.log('成功加载实验数据:', tutorials.value.length, '个实验');
|
||||
|
||||
// 启动自动旋转
|
||||
startAutoRotation();
|
||||
} catch (error) {
|
||||
console.error('加载例程失败:', error);
|
||||
console.error('加载实验数据失败:', error);
|
||||
|
||||
// 如果加载失败,显示默认的占位内容
|
||||
tutorials.value = [{
|
||||
id: 'placeholder',
|
||||
title: '实验数据加载中...',
|
||||
description: '请稍后或刷新页面重试',
|
||||
thumbnail: undefined,
|
||||
tags: []
|
||||
}];
|
||||
}
|
||||
});
|
||||
|
||||
// 在组件销毁时清除计时器
|
||||
// 在组件销毁时清除计时器和Blob URLs
|
||||
onUnmounted(() => {
|
||||
if (autoRotationTimer) {
|
||||
clearInterval(autoRotationTimer);
|
||||
}
|
||||
|
||||
// 清理创建的Blob URLs
|
||||
tutorials.value.forEach(tutorial => {
|
||||
if (tutorial.thumbnail && tutorial.thumbnail.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(tutorial.thumbnail);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 鼠标滚轮处理
|
||||
@@ -210,12 +229,12 @@ const resumeAutoRotation = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 前往例程
|
||||
const goToTutorial = (tutorialId: string) => {
|
||||
// 跳转到工程页面,并通过 query 参数传递文档路径
|
||||
// 前往实验
|
||||
const goToExam = (examId: string) => {
|
||||
// 跳转到实验列表页面并传递examId参数,页面将自动打开对应的实验详情模态框
|
||||
router.push({
|
||||
path: '/project',
|
||||
query: { tutorial: tutorialId }
|
||||
path: '/exam',
|
||||
query: { examId: examId }
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,64 @@
|
||||
<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 -->
|
||||
<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 bg-base-200 rounded">
|
||||
<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 -->
|
||||
<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" />
|
||||
<label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
|
||||
</fieldset>
|
||||
|
||||
<!-- Upload Button -->
|
||||
<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">
|
||||
<span class="loading loading-spinner"></span>
|
||||
下载中...
|
||||
上传中...
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ buttonText }}
|
||||
@@ -27,6 +70,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 { isNull, isUndefined } from "lodash";
|
||||
|
||||
@@ -34,10 +78,12 @@ interface Props {
|
||||
uploadEvent?: (file: File) => Promise<boolean>;
|
||||
downloadEvent?: () => Promise<boolean>;
|
||||
maxMemory?: number;
|
||||
examId?: string; // 新增examId属性
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
maxMemory: 4,
|
||||
examId: '',
|
||||
});
|
||||
|
||||
const emits = defineEmits<{
|
||||
@@ -47,6 +93,10 @@ const emits = defineEmits<{
|
||||
const dialog = useDialogStore();
|
||||
|
||||
const isUploading = ref(false);
|
||||
const isDownloading = ref(false);
|
||||
const isProgramming = ref(false);
|
||||
const availableBitstreams = ref<{id: number, name: string}[]>([]);
|
||||
|
||||
const buttonText = computed(() => {
|
||||
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
|
||||
});
|
||||
@@ -56,14 +106,113 @@ const bitstream = defineModel("bitstreamFile", {
|
||||
type: File,
|
||||
default: undefined,
|
||||
});
|
||||
onMounted(() => {
|
||||
|
||||
// 初始化时加载示例比特流
|
||||
onMounted(async () => {
|
||||
if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
|
||||
let fileList = new DataTransfer();
|
||||
fileList.items.add(bitstream.value);
|
||||
fileInput.value.files = fileList.files;
|
||||
}
|
||||
|
||||
await loadAvailableBitstreams();
|
||||
});
|
||||
|
||||
// 加载可用的比特流文件列表
|
||||
async function loadAvailableBitstreams() {
|
||||
console.log('加载可用比特流文件,examId:', props.examId);
|
||||
if (!props.examId) {
|
||||
availableBitstreams.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const examClient = AuthManager.createAuthenticatedExamClient();
|
||||
// 使用新的API获取比特流资源列表
|
||||
const resources = await examClient.getExamResourceList(props.examId, 'bitstream');
|
||||
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 examClient = AuthManager.createAuthenticatedExamClient();
|
||||
|
||||
// 使用动态API获取资源文件
|
||||
const response = await examClient.getExamResourceById(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 || !props.uploadEvent) return;
|
||||
|
||||
isProgramming.value = true;
|
||||
try {
|
||||
const examClient = AuthManager.createAuthenticatedExamClient();
|
||||
|
||||
// 使用动态API获取比特流文件数据
|
||||
const response = await examClient.getExamResourceById(bitstream.id);
|
||||
|
||||
if (!response || !response.data) {
|
||||
throw new Error('获取比特流文件失败');
|
||||
}
|
||||
|
||||
const file = new File([response.data], response.fileName || bitstream.name, { type: response.data.type });
|
||||
|
||||
// 调用上传事件
|
||||
const uploadSuccess = await props.uploadEvent(file);
|
||||
if (uploadSuccess) {
|
||||
// 如果有下载事件(烧录),则执行
|
||||
if (props.downloadEvent) {
|
||||
const downloadSuccess = await props.downloadEvent();
|
||||
if (downloadSuccess) {
|
||||
dialog.info("示例比特流烧录成功");
|
||||
} else {
|
||||
dialog.error("烧录失败");
|
||||
}
|
||||
} else {
|
||||
dialog.info("示例比特流上传成功");
|
||||
}
|
||||
} else {
|
||||
dialog.error("上传失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('烧录示例比特流失败:', error);
|
||||
dialog.error("烧录示例比特流失败");
|
||||
} finally {
|
||||
isProgramming.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileChange(event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0]; // 获取选中的第一个文件
|
||||
|
||||
373
src/components/WaveformDisplay/WaveformDisplay.vue
Normal file
373
src/components/WaveformDisplay/WaveformDisplay.vue
Normal file
@@ -0,0 +1,373 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full"
|
||||
:class="{
|
||||
'h-48': !props.data,
|
||||
'h-150': props.data,
|
||||
}"
|
||||
>
|
||||
<v-chart
|
||||
v-if="props.data"
|
||||
class="w-full h-full"
|
||||
:option="option"
|
||||
autoresize
|
||||
:update-options="updateOptions"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
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">
|
||||
<h3 class="text-xl font-semibold text-slate-600 mb-2">暂无数据</h3>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, shallowRef, useSlots } from "vue";
|
||||
import VChart from "vue-echarts";
|
||||
|
||||
// Echarts
|
||||
import { use } from "echarts/core";
|
||||
import { LineChart } from "echarts/charts";
|
||||
import {
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
AxisPointerComponent,
|
||||
ToolboxComponent,
|
||||
MarkLineComponent,
|
||||
} from "echarts/components";
|
||||
import { CanvasRenderer } from "echarts/renderers";
|
||||
import type { ComposeOption } from "echarts/core";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import type {
|
||||
AxisPointerComponentOption,
|
||||
TooltipComponentOption,
|
||||
GridComponentOption,
|
||||
DataZoomComponentOption,
|
||||
MarkLineComponentOption,
|
||||
} from "echarts/components";
|
||||
import type {
|
||||
ToolboxComponentOption,
|
||||
XAXisOption,
|
||||
YAXisOption,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { isUndefined } from "lodash";
|
||||
import type { LogicDataType } from ".";
|
||||
|
||||
use([
|
||||
TooltipComponent,
|
||||
ToolboxComponent,
|
||||
GridComponent,
|
||||
AxisPointerComponent,
|
||||
DataZoomComponent,
|
||||
LineChart,
|
||||
CanvasRenderer,
|
||||
MarkLineComponent,
|
||||
]);
|
||||
|
||||
type EChartsOption = ComposeOption<
|
||||
| AxisPointerComponentOption
|
||||
| TooltipComponentOption
|
||||
| ToolboxComponentOption
|
||||
| GridComponentOption
|
||||
| DataZoomComponentOption
|
||||
| LineSeriesOption
|
||||
| MarkLineComponentOption
|
||||
>;
|
||||
|
||||
const props = defineProps<{
|
||||
data?: LogicDataType;
|
||||
}>();
|
||||
|
||||
const slots = useSlots();
|
||||
const hasSlot = computed(() => !!slots.default && slots.default().length > 0);
|
||||
|
||||
// 添加更新选项来减少重绘
|
||||
const updateOptions = shallowRef({
|
||||
notMerge: false,
|
||||
lazyUpdate: true,
|
||||
silent: false,
|
||||
});
|
||||
|
||||
const option = computed((): EChartsOption => {
|
||||
if (isUndefined(props.data)) return {};
|
||||
|
||||
// 只获取启用的通道,使用y数据结构
|
||||
const enabledChannels = props.data.y.filter(
|
||||
(channel) => channel.enabled,
|
||||
);
|
||||
const enabledChannelIndices = props.data.y
|
||||
.map((channel, index) => (channel.enabled ? index : -1))
|
||||
.filter((index) => index !== -1);
|
||||
|
||||
// 统计启用通道数量
|
||||
const channelCount = enabledChannels.length;
|
||||
const channelSpacing = 2; // 每个通道之间的间距
|
||||
|
||||
if (channelCount === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 单个网格
|
||||
const grids: GridComponentOption[] = [
|
||||
{
|
||||
left: "5%",
|
||||
right: "5%",
|
||||
top: "5%",
|
||||
bottom: "15%",
|
||||
},
|
||||
];
|
||||
|
||||
// 单个X轴
|
||||
const xAxis: XAXisOption[] = [
|
||||
{
|
||||
type: "category",
|
||||
boundaryGap: true,
|
||||
data: props.data.x.map((x) => x.toFixed(3)),
|
||||
axisLabel: {
|
||||
formatter: (value: string) =>
|
||||
props.data
|
||||
? `${value}${props.data.xUnit}`
|
||||
: `${value}`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 单个Y轴,范围根据启用通道数量调整
|
||||
const yAxis: YAXisOption[] = [
|
||||
{
|
||||
type: "value",
|
||||
min: -0.5,
|
||||
max: channelCount * channelSpacing - 0.5,
|
||||
interval: channelSpacing,
|
||||
axisLabel: {
|
||||
formatter: (value: number) => {
|
||||
const channelIndex = Math.round(value / channelSpacing);
|
||||
return channelIndex < channelCount
|
||||
? enabledChannels[channelIndex].name
|
||||
: "";
|
||||
},
|
||||
},
|
||||
splitLine: { show: false },
|
||||
},
|
||||
];
|
||||
|
||||
// 创建系列数据
|
||||
const series: LineSeriesOption[] = [];
|
||||
enabledChannelIndices.forEach(
|
||||
(originalIndex: number, displayIndex: number) => {
|
||||
const channel = props.data!.y[originalIndex];
|
||||
if (channel.type === "logic") {
|
||||
// logic类型,原样显示
|
||||
series.push({
|
||||
name: channel.name,
|
||||
type: "line",
|
||||
data: channel.value.map(
|
||||
(value: number) => value + displayIndex * channelSpacing + 0.2,
|
||||
),
|
||||
step: "end",
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: channel.color,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.3,
|
||||
origin: displayIndex * channelSpacing,
|
||||
color: channel.color,
|
||||
},
|
||||
symbol: "none",
|
||||
sampling: "lttb",
|
||||
animation: false,
|
||||
});
|
||||
} else if (channel.type === "number") {
|
||||
const values = channel.value;
|
||||
const xArr = props.data!.x;
|
||||
// 构造带过渡的点序列
|
||||
function buildVcdLine(valArr: number[], high: number, low: number) {
|
||||
const points: { x: number; y: number }[] = [];
|
||||
let lastValue = high;
|
||||
points.push({ x: xArr[0], y: lastValue });
|
||||
for (let i = 1; i < valArr.length; i++) {
|
||||
const v =
|
||||
valArr[i] !== valArr[i - 1]
|
||||
? lastValue === high
|
||||
? low
|
||||
: high
|
||||
: lastValue;
|
||||
points.push({ x: xArr[i], y: v });
|
||||
lastValue = v;
|
||||
}
|
||||
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线条
|
||||
series.push({
|
||||
name: channel.name + "_1",
|
||||
type: "line",
|
||||
data: buildVcdLine(
|
||||
values,
|
||||
displayIndex * channelSpacing + 1,
|
||||
displayIndex * channelSpacing,
|
||||
),
|
||||
step: false,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
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",
|
||||
sampling: "lttb",
|
||||
animation: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
animation: false,
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "line",
|
||||
label: {
|
||||
backgroundColor: "#6a7985",
|
||||
},
|
||||
animation: false,
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
if (Array.isArray(params) && params.length > 0) {
|
||||
const timeValue = props.data!.x[params[0].dataIndex];
|
||||
const dataIndex = params[0].dataIndex;
|
||||
let tooltip = `Time: ${timeValue.toFixed(3)}${props.data!.xUnit}<br/>`;
|
||||
enabledChannelIndices.forEach(
|
||||
(originalIndex: number, displayIndex: number) => {
|
||||
const channel = props.data!.y[originalIndex];
|
||||
if (channel.type === "logic") {
|
||||
const channelName = channel.name;
|
||||
const originalValue = channel.value[dataIndex];
|
||||
tooltip += `${channelName}: ${originalValue}<br/>`;
|
||||
} else if (channel.type === "number") {
|
||||
const channelName = channel.name;
|
||||
const originalValue = channel.value[dataIndex];
|
||||
tooltip += `${channelName}: ${originalValue}<br/>`;
|
||||
}
|
||||
},
|
||||
);
|
||||
return tooltip;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
hideDelay: 100,
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
restore: {},
|
||||
},
|
||||
},
|
||||
grid: grids,
|
||||
xAxis: xAxis,
|
||||
yAxis: yAxis,
|
||||
dataZoom: [
|
||||
{
|
||||
show: true,
|
||||
realtime: true,
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
{
|
||||
type: "inside",
|
||||
realtime: true,
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
series: series,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
61
src/components/WaveformDisplay/index.ts
Normal file
61
src/components/WaveformDisplay/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import WaveformDisplay from "./WaveformDisplay.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",
|
||||
};
|
||||
}
|
||||
|
||||
export { WaveformDisplay };
|
||||
@@ -7,7 +7,12 @@
|
||||
<div class="waveform-display">
|
||||
<svg width="100%" height="120" viewBox="0 0 300 120">
|
||||
<rect width="300" height="120" fill="#1a1f25" />
|
||||
<path :d="currentWaveformPath" stroke="lime" stroke-width="2" fill="none" />
|
||||
<path
|
||||
:d="currentWaveformPath"
|
||||
stroke="lime"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
/>
|
||||
|
||||
<!-- 频率和相位显示 -->
|
||||
<text x="20" y="25" fill="#0f0" font-size="14">
|
||||
@@ -16,7 +21,13 @@
|
||||
<text x="200" y="25" fill="#0f0" font-size="14">
|
||||
φ: {{ phase }}°
|
||||
</text>
|
||||
<text x="150" y="110" fill="#0f0" font-size="14" text-anchor="middle">
|
||||
<text
|
||||
x="150"
|
||||
y="110"
|
||||
fill="#0f0"
|
||||
font-size="14"
|
||||
text-anchor="middle"
|
||||
>
|
||||
{{ displayTimebase }}
|
||||
</text>
|
||||
</svg>
|
||||
@@ -35,10 +46,15 @@
|
||||
|
||||
<!-- 波形选择区 -->
|
||||
<div class="waveform-selector">
|
||||
<div v-for="(name, index) in waveformNames" :key="`wave-${index}`" :class="[
|
||||
'waveform-option',
|
||||
{ active: currentWaveformIndex === index },
|
||||
]" @click="selectWaveform(index)">
|
||||
<div
|
||||
v-for="(name, index) in waveformNames"
|
||||
:key="`wave-${index}`"
|
||||
:class="[
|
||||
'waveform-option',
|
||||
{ active: currentWaveformIndex === index },
|
||||
]"
|
||||
@click="selectWaveform(index)"
|
||||
>
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,8 +67,13 @@
|
||||
<button class="control-button" @click="decreaseFrequency">
|
||||
-
|
||||
</button>
|
||||
<input v-model="frequencyInput" @blur="applyFrequencyInput" @keyup.enter="applyFrequencyInput"
|
||||
class="control-input" type="text" />
|
||||
<input
|
||||
v-model="frequencyInput"
|
||||
@blur="applyFrequencyInput"
|
||||
@keyup.enter="applyFrequencyInput"
|
||||
class="control-input"
|
||||
type="text"
|
||||
/>
|
||||
<button class="control-button" @click="increaseFrequency">
|
||||
+
|
||||
</button>
|
||||
@@ -63,8 +84,13 @@
|
||||
<span class="control-label">相位:</span>
|
||||
<div class="control-buttons">
|
||||
<button class="control-button" @click="decreasePhase">-</button>
|
||||
<input v-model="phaseInput" @blur="applyPhaseInput" @keyup.enter="applyPhaseInput" class="control-input"
|
||||
type="text" />
|
||||
<input
|
||||
v-model="phaseInput"
|
||||
@blur="applyPhaseInput"
|
||||
@keyup.enter="applyPhaseInput"
|
||||
class="control-input"
|
||||
type="text"
|
||||
/>
|
||||
<button class="control-button" @click="increasePhase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,9 +101,12 @@
|
||||
<div class="section-heading">自定义波形</div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">函数表达式:</label>
|
||||
<input v-model="customWaveformExpression" class="function-input"
|
||||
<input
|
||||
v-model="customWaveformExpression"
|
||||
class="function-input"
|
||||
placeholder="例如: sin(t) 或 x^(2/3)+0.9*sqrt(3.3-x^2)*sin(a*PI*x) [a=7.8]"
|
||||
@keyup.enter="applyCustomWaveform" />
|
||||
@keyup.enter="applyCustomWaveform"
|
||||
/>
|
||||
<button class="apply-button" @click="applyCustomWaveform">
|
||||
应用
|
||||
</button>
|
||||
@@ -86,17 +115,26 @@
|
||||
<div class="example-functions">
|
||||
<div class="example-label">示例函数:</div>
|
||||
<div class="example-buttons">
|
||||
<button class="example-button" @click="applyExampleFunction('sin(t)')">
|
||||
<button
|
||||
class="example-button"
|
||||
@click="applyExampleFunction('sin(t)')"
|
||||
>
|
||||
正弦波
|
||||
</button>
|
||||
<button class="example-button" @click="applyExampleFunction('sin(t)^3')">
|
||||
<button
|
||||
class="example-button"
|
||||
@click="applyExampleFunction('sin(t)^3')"
|
||||
>
|
||||
立方正弦
|
||||
</button>
|
||||
<button class="example-button" @click="
|
||||
applyExampleFunction(
|
||||
'((x)^(2/3)+0.9*sqrt(3.3-(x)^2)*sin(10*PI*(x)))*0.75',
|
||||
)
|
||||
">
|
||||
<button
|
||||
class="example-button"
|
||||
@click="
|
||||
applyExampleFunction(
|
||||
'((x)^(2/3)+0.9*sqrt(3.3-(x)^2)*sin(10*PI*(x)))*0.75',
|
||||
)
|
||||
"
|
||||
>
|
||||
心形函数
|
||||
</button>
|
||||
</div>
|
||||
@@ -105,8 +143,16 @@
|
||||
<div class="drawing-area">
|
||||
<div class="section-heading">波形绘制</div>
|
||||
<div class="waveform-canvas-container" ref="canvasContainer">
|
||||
<canvas ref="drawingCanvas" class="drawing-canvas" width="280" height="100" @mousedown="startDrawing"
|
||||
@mousemove="draw" @mouseup="stopDrawing" @mouseleave="stopDrawing"></canvas>
|
||||
<canvas
|
||||
ref="drawingCanvas"
|
||||
class="drawing-canvas"
|
||||
width="280"
|
||||
height="100"
|
||||
@mousedown="startDrawing"
|
||||
@mousemove="draw"
|
||||
@mouseup="stopDrawing"
|
||||
@mouseleave="stopDrawing"
|
||||
></canvas>
|
||||
<div class="canvas-actions">
|
||||
<button class="canvas-button" @click="clearCanvas">
|
||||
清除
|
||||
@@ -123,19 +169,30 @@
|
||||
<div class="saved-waveforms">
|
||||
<div class="section-heading">波形存储槽</div>
|
||||
<div class="slot-container">
|
||||
<div v-for="(slot, index) in waveformSlots" :key="`slot-${index}`"
|
||||
:class="['waveform-slot', { empty: !slot.name }]" @click="loadWaveformSlot(index)">
|
||||
<div
|
||||
v-for="(slot, index) in waveformSlots"
|
||||
:key="`slot-${index}`"
|
||||
:class="['waveform-slot', { empty: !slot.name }]"
|
||||
@click="loadWaveformSlot(index)"
|
||||
>
|
||||
<span class="slot-name">{{
|
||||
slot.name || `槽 ${index + 1}`
|
||||
}}</span>
|
||||
<button class="save-button" @click.stop="saveCurrentToSlot(index)">
|
||||
}}</span>
|
||||
<button
|
||||
class="save-button"
|
||||
@click.stop="saveCurrentToSlot(index)"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary text-primary-content w-full" :disabled="isApplying" @click="applyOutputWave">
|
||||
<button
|
||||
class="btn btn-primary text-primary-content w-full"
|
||||
:disabled="isApplying"
|
||||
@click="applyOutputWave"
|
||||
>
|
||||
<div v-if="isApplying">
|
||||
<span class="loading loading-spinner"></span>
|
||||
应用中...
|
||||
@@ -151,10 +208,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import CollapsibleSection from "../CollapsibleSection.vue";
|
||||
import { DDSClient } from "@/APIClient";
|
||||
import { useEquipments } from "@/stores/equipments";
|
||||
import { useDialogStore } from "@/stores/dialog";
|
||||
import { toInteger } from "lodash";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
|
||||
// Component Attributes
|
||||
const props = defineProps<{
|
||||
@@ -164,7 +221,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
// Global varibles
|
||||
const dds = new DDSClient();
|
||||
const dds = AuthManager.createAuthenticatedDDSClient();
|
||||
const eqps = useEquipments();
|
||||
const dialog = useDialogStore();
|
||||
|
||||
@@ -385,6 +442,7 @@ const currentWaveformPath = computed(() => {
|
||||
function selectWaveform(index: number) {
|
||||
currentWaveformIndex.value = index;
|
||||
updateModelValue();
|
||||
applyOutputWave();
|
||||
}
|
||||
|
||||
async function applyOutputWave() {
|
||||
@@ -408,7 +466,7 @@ async function applyOutputWave() {
|
||||
eqps.boardPort,
|
||||
0,
|
||||
currentWaveformIndex.value,
|
||||
toInteger(frequency.value * Math.pow(2, 32 - 20)),
|
||||
toInteger((frequency.value * Math.pow(2, 32 - 20)) / 10),
|
||||
);
|
||||
if (!ret) {
|
||||
dialog.error("应用失败");
|
||||
@@ -424,7 +482,7 @@ async function applyOutputWave() {
|
||||
toInteger((phase.value * 4096) / 360),
|
||||
);
|
||||
if (ret) {
|
||||
dialog.info("应用成功");
|
||||
// dialog.info("应用成功");
|
||||
} else {
|
||||
dialog.error("应用失败");
|
||||
}
|
||||
@@ -455,6 +513,7 @@ function increaseFrequency() {
|
||||
frequency.value = parseFloat(frequency.value.toFixed(1)); // 修复浮点数精度问题
|
||||
frequencyInput.value = formatFrequency(frequency.value);
|
||||
updateModelValue();
|
||||
applyOutputWave();
|
||||
}
|
||||
|
||||
function decreaseFrequency() {
|
||||
@@ -475,6 +534,7 @@ function decreaseFrequency() {
|
||||
frequency.value = parseFloat(frequency.value.toFixed(1)); // 修复浮点数精度问题
|
||||
frequencyInput.value = formatFrequency(frequency.value);
|
||||
updateModelValue();
|
||||
applyOutputWave();
|
||||
}
|
||||
|
||||
function applyFrequencyInput() {
|
||||
@@ -505,6 +565,7 @@ function increasePhase() {
|
||||
}
|
||||
phaseInput.value = phase.value.toString();
|
||||
updateModelValue();
|
||||
applyOutputWave();
|
||||
}
|
||||
|
||||
function decreasePhase() {
|
||||
@@ -514,6 +575,7 @@ function decreasePhase() {
|
||||
}
|
||||
phaseInput.value = phase.value.toString();
|
||||
updateModelValue();
|
||||
applyOutputWave();
|
||||
}
|
||||
|
||||
function applyPhaseInput() {
|
||||
@@ -788,7 +850,7 @@ function saveCurrentToSlot(index: number) {
|
||||
type: waveforms[currentWaveformIndex.value],
|
||||
data:
|
||||
drawPoints.value.length > 0 &&
|
||||
currentWaveformIndex.value === waveforms.indexOf("custom")
|
||||
currentWaveformIndex.value === waveforms.indexOf("custom")
|
||||
? [...drawPoints.value]
|
||||
: null,
|
||||
};
|
||||
|
||||
@@ -1,32 +1,47 @@
|
||||
<template>
|
||||
<div class="motherboard-container" v-bind="$attrs" :style="{
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
position: 'relative',
|
||||
}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" :viewBox="`0 0 800 600`"
|
||||
class="motherboard-svg">
|
||||
<image href="../equipments/svg/motherboard.svg" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" />
|
||||
<div
|
||||
class="motherboard-container"
|
||||
v-bind="$attrs"
|
||||
:style="{
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
position: 'relative',
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:viewBox="`0 0 800 600`"
|
||||
class="motherboard-svg"
|
||||
>
|
||||
<image
|
||||
href="../equipments/svg/motherboard.svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<Teleport to="#ComponentCapabilities" v-if="selectecComponentID === props.componentId">
|
||||
<MotherBoardCaps :jtagAddr="props.boardAddr" :jtagPort="toNumber(props.boardPort)" :jtagFreq="jtagFreq"
|
||||
@change-jtag-freq="changeJtagFreq" />
|
||||
<Teleport
|
||||
to="#ComponentCapabilities"
|
||||
v-if="selectecComponentID === props.componentId"
|
||||
>
|
||||
<MotherBoardCaps :jtagFreq="jtagFreq" :exam-id="examId" @change-jtag-freq="changeJtagFreq" />
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import MotherBoardCaps from "./MotherBoardCaps.vue";
|
||||
import { ref, computed, watchEffect, inject } from "vue";
|
||||
import { ref, computed, inject } from "vue";
|
||||
import { CanvasCurrentSelectedComponentID } from "../InjectKeys";
|
||||
import { toNumber } from "lodash";
|
||||
|
||||
// 主板特有属性
|
||||
export interface MotherBoardProps {
|
||||
size: number;
|
||||
boardAddr?: string;
|
||||
boardPort?: string;
|
||||
componentId?: string;
|
||||
examId?: string; // 新增examId属性
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -61,8 +76,6 @@ defineExpose({
|
||||
export function getDefaultProps(): MotherBoardProps {
|
||||
return {
|
||||
size: 1,
|
||||
boardAddr: "127.0.0.1",
|
||||
boardPort: "1234",
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,19 +8,15 @@
|
||||
<p>
|
||||
IDCode: 0x{{ jtagIDCode.toString(16).padStart(8, "0").toUpperCase() }}
|
||||
</p>
|
||||
<button class="btn btn-circle w-6 h-6" :onclick="getIDCode">
|
||||
<svg class="icon opacity-70 fill-primary" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="4865" width="200" height="200">
|
||||
<path
|
||||
d="M894.481158 505.727133c0 49.589418-9.711176 97.705276-28.867468 143.007041-18.501376 43.74634-44.98454 83.031065-78.712713 116.759237-33.728172 33.728172-73.012897 60.211337-116.759237 78.712713-45.311998 19.156292-93.417623 28.877701-143.007041 28.877701s-97.695043-9.721409-142.996808-28.877701c-43.756573-18.501376-83.031065-44.98454-116.76947-78.712713-33.728172-33.728172-60.211337-73.012897-78.712713-116.759237-19.156292-45.301765-28.867468-93.417623-28.867468-143.007041 0-49.579185 9.711176-97.695043 28.867468-142.996808 18.501376-43.74634 44.98454-83.031065 78.712713-116.759237 33.738405-33.728172 73.012897-60.211337 116.76947-78.712713 45.301765-19.166525 93.40739-28.877701 142.996808-28.877701 52.925397 0 104.008842 11.010775 151.827941 32.745798 46.192042 20.977777 86.909395 50.79692 121.016191 88.608084 4.389984 4.860704 8.646937 9.854439 12.781094 14.97097l0-136.263453c0-11.307533 9.168824-20.466124 20.466124-20.466124 11.307533 0 20.466124 9.15859 20.466124 20.466124l0 183.64253c0 5.433756-2.148943 10.632151-5.986341 14.46955-3.847631 3.837398-9.046027 5.996574-14.479783 5.996574l-183.64253-0.020466c-11.307533 0-20.466124-9.168824-20.466124-20.466124 0-11.307533 9.168824-20.466124 20.466124-20.466124l132.293025 0.020466c-3.960195-4.952802-8.063653-9.782807-12.289907-14.479783-30.320563-33.605376-66.514903-60.098773-107.549481-78.753645-42.467207-19.289322-87.850837-29.072129-134.902456-29.072129-87.195921 0-169.172981 33.9533-230.816946 95.597265-61.654198 61.654198-95.597265 143.621025-95.597265 230.816946s33.943067 169.172981 95.597265 230.816946c61.643965 61.654198 143.621025 95.607498 230.816946 95.607498s169.172981-33.9533 230.816946-95.607498c61.654198-61.643965 95.597265-143.621025 95.597265-230.816946 0-11.2973 9.168824-20.466124 20.466124-20.466124C885.322567 485.261009 894.481158 494.429833 894.481158 505.727133z"
|
||||
p-id="4866"></path>
|
||||
</svg>
|
||||
<button class="btn btn-circle w-6 h-6" :disabled="isGettingIDCode" :onclick="getIDCode">
|
||||
<RefreshCcwIcon class="icon" :class="{ 'animate-spin': isGettingIDCode }" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<UploadCard class="bg-base-200" :upload-event="eqps.jtagUploadBitstream"
|
||||
:download-event="eqps.jtagDownloadBitstream" :bitstream-file="eqps.jtagBitstream"
|
||||
:exam-id="examId"
|
||||
@update:bitstream-file="handleBitstreamChange">
|
||||
</UploadCard>
|
||||
<div class="divider"></div>
|
||||
@@ -46,16 +42,12 @@
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<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">
|
||||
<input type="checkbox" class="checkbox" :checked="eqps.enableMatrixKey"
|
||||
@change="handleMatrixkeyCheckboxChange" />
|
||||
<p class="mx-2">启用矩阵键盘</p>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<input type="checkbox" class="checkbox" :checked="eqps.enablePower" @change="handlePowerCheckboxChange" />
|
||||
<p class="mx-2">启用电源</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -65,12 +57,12 @@ import z from "zod";
|
||||
import UploadCard from "@/components/UploadCard.vue";
|
||||
import { useDialogStore } from "@/stores/dialog";
|
||||
import { useEquipments } from "@/stores/equipments";
|
||||
import { computed, ref, watchEffect, watchPostEffect } from "vue";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { RefreshCcwIcon } from "lucide-vue-next";
|
||||
|
||||
interface CapsProps {
|
||||
jtagAddr?: string;
|
||||
jtagPort?: number;
|
||||
jtagFreq?: string;
|
||||
examId?: string; // 新增examId属性
|
||||
}
|
||||
|
||||
const emits = defineEmits<{
|
||||
@@ -126,33 +118,16 @@ 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() {
|
||||
if (eqps.jtagClientMutex.isLocked()) {
|
||||
dialog.warn("Jtag正在被占用");
|
||||
return;
|
||||
}
|
||||
|
||||
eqps.enableJtagBoundaryScan = !eqps.enableJtagBoundaryScan;
|
||||
}
|
||||
|
||||
const isGettingIDCode = ref(false);
|
||||
async function getIDCode(isQuiet: boolean = false) {
|
||||
isGettingIDCode.value = true;
|
||||
jtagIDCode.value = await eqps.jtagGetIDCode(isQuiet);
|
||||
isGettingIDCode.value = false;
|
||||
}
|
||||
|
||||
watchEffect(async () => {
|
||||
if (eqps.setAddr(props.jtagAddr) && eqps.setPort(props.jtagPort))
|
||||
getIDCode(true);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
|
||||
@@ -177,26 +177,26 @@ defineExpose({
|
||||
getPinPosition: (pinId: string) => {
|
||||
// 如果是自定义的引脚ID
|
||||
if (props.pins && props.pins.length > 0) {
|
||||
console.log('SMT_LED查找Pin ID:', pinId);
|
||||
console.log('SMT_LED组件尺寸:', props.size, '宽高:', width.value, 'x', height.value);
|
||||
// console.log('SMT_LED查找Pin ID:', pinId);
|
||||
// console.log('SMT_LED组件尺寸:', props.size, '宽高:', width.value, 'x', height.value);
|
||||
const customPin = props.pins.find(p => p.pinId === pinId);
|
||||
console.log('找到的引脚配置:', customPin);
|
||||
// console.log('找到的引脚配置:', customPin);
|
||||
|
||||
if (customPin) {
|
||||
// 考虑组件尺寸的缩放
|
||||
const scaledX = customPin.x * props.size;
|
||||
const scaledY = customPin.y * props.size;
|
||||
|
||||
console.log('使用Pin缩放后的坐标:', scaledX, scaledY);
|
||||
// console.log('使用Pin缩放后的坐标:', scaledX, scaledY);
|
||||
return {
|
||||
x: scaledX,
|
||||
y: scaledY
|
||||
};
|
||||
}
|
||||
console.log('未找到匹配的引脚');
|
||||
// console.log('未找到匹配的引脚');
|
||||
return null;
|
||||
}
|
||||
console.log('没有引脚配置');
|
||||
// console.log('没有引脚配置');
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ interface SevenSegmentDisplayProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
AFTERGLOW_BUFFER_SIZE?: number; // 余晖存储槽大小
|
||||
AFTERGLOW_UPDATE_INTERVAL?: number; // 余晖更新间隔,单位毫秒
|
||||
AFTERGLOW_DURATION?: number; // 余晖持续时间(毫秒)
|
||||
pins?: {
|
||||
pinId: string;
|
||||
constraint: string;
|
||||
@@ -93,7 +93,7 @@ const props = withDefaults(defineProps<SevenSegmentDisplayProps>(), {
|
||||
size: 1,
|
||||
color: "red",
|
||||
AFTERGLOW_BUFFER_SIZE: 1, // 默认存储槽大小为100
|
||||
AFTERGLOW_UPDATE_INTERVAL: 1, // 默认更新间隔为2毫秒
|
||||
AFTERGLOW_DURATION: 2000, // 默认余晖持续时间500毫秒
|
||||
cathodeType: "common", // 默认为共阴极
|
||||
pins: () => [
|
||||
{ pinId: "a", constraint: "", x: 10, y: 170 }, // a段
|
||||
@@ -149,55 +149,160 @@ const afterglowBuffers = ref<Record<string, boolean[]>>({
|
||||
dp: [],
|
||||
});
|
||||
|
||||
// 更新间隔计时器
|
||||
let updateIntervalTimer: number | null = null;
|
||||
// 余晖判定阈值(比如持续10帧才算稳定,可根据刷新间隔和人眼余晖调节)
|
||||
const STABLE_THRESHOLD = 3;
|
||||
|
||||
// 判断段是否激活 - 如果当前状态或任一历史状态为true,则视为激活
|
||||
// 实际显示的段状态(只有稳定后才改变)
|
||||
const stableSegmentStates = ref({
|
||||
a: false,
|
||||
b: false,
|
||||
c: false,
|
||||
d: false,
|
||||
e: false,
|
||||
f: false,
|
||||
g: false,
|
||||
dp: false,
|
||||
});
|
||||
|
||||
// 每段的稳定计数器
|
||||
const segmentStableCounters = ref<Record<string, number>>({
|
||||
a: 0,
|
||||
b: 0,
|
||||
c: 0,
|
||||
d: 0,
|
||||
e: 0,
|
||||
f: 0,
|
||||
g: 0,
|
||||
dp: 0,
|
||||
});
|
||||
|
||||
// 段选关闭时的余晖状态保持
|
||||
const afterglowStates = ref({
|
||||
a: false,
|
||||
b: false,
|
||||
c: false,
|
||||
d: false,
|
||||
e: false,
|
||||
f: false,
|
||||
g: false,
|
||||
dp: false,
|
||||
});
|
||||
|
||||
// 余晖计时器
|
||||
const afterglowTimers = ref<Record<string, number | null>>({
|
||||
a: null,
|
||||
b: null,
|
||||
c: null,
|
||||
d: null,
|
||||
e: null,
|
||||
f: null,
|
||||
g: null,
|
||||
dp: null,
|
||||
});
|
||||
|
||||
// 余晖持续时间(毫秒)
|
||||
const AFTERGLOW_DURATION = computed(() => props.AFTERGLOW_DURATION || 500);
|
||||
|
||||
// 当前COM口状态
|
||||
const currentComActive = ref(false); // 初始化为false,等待第一次状态检查
|
||||
|
||||
// 是否处于余晖模式
|
||||
const isInAfterglowMode = ref(false);
|
||||
|
||||
// 判断段是否激活(用稳定状态或余晖状态)
|
||||
function isSegmentActive(
|
||||
segment: "a" | "b" | "c" | "d" | "e" | "f" | "g" | "dp",
|
||||
): boolean {
|
||||
return segmentStates.value[segment] || afterglowBuffers.value[segment].some(state => state);
|
||||
// 如果处于余晖模式,使用余晖状态
|
||||
if (isInAfterglowMode.value) {
|
||||
return afterglowStates.value[segment];
|
||||
}
|
||||
|
||||
// 如果COM口未激活,所有段都不显示
|
||||
if (!currentComActive.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 否则使用稳定状态
|
||||
return stableSegmentStates.value[segment];
|
||||
}
|
||||
|
||||
// 更新引脚状态的函数
|
||||
function updateSegmentStates() {
|
||||
// 先获取COM口状态
|
||||
const comPin = props.pins.find(p => p.pinId === "COM");
|
||||
let comActive = true;
|
||||
const comPin = props.pins.find((p) => p.pinId === "COM");
|
||||
let comActive = false; // 默认未激活
|
||||
|
||||
if (comPin && comPin.constraint) {
|
||||
const comState = getConstraintState(comPin.constraint);
|
||||
if (props.cathodeType === "anode") {
|
||||
// anode模式下,COM为高电平则所有段都熄灭
|
||||
comActive = comState !== "high";
|
||||
// 共阳极模式下,COM为低电平才激活
|
||||
comActive = comState === "low";
|
||||
} else {
|
||||
// 共阴极模式下,COM为低电平才激活
|
||||
comActive = comState === "low";
|
||||
}
|
||||
// 可扩展其他模式
|
||||
} else if (!comPin || !comPin.constraint) {
|
||||
// 如果没有COM引脚或者COM引脚没有约束,则认为始终激活
|
||||
comActive = true;
|
||||
}
|
||||
|
||||
// 如果COM口激活,更新所有段的状态到存储槽
|
||||
if (comActive) {
|
||||
updateAfterglowBuffers();
|
||||
// 检查COM口状态是否发生变化
|
||||
const comStateChanged = currentComActive.value !== comActive;
|
||||
currentComActive.value = comActive;
|
||||
|
||||
// 如果COM从激活变为非激活,进入余晖模式
|
||||
if (comStateChanged && !comActive) {
|
||||
enterAfterglowMode();
|
||||
return; // 在余晖模式下,不处理其他引脚变化
|
||||
}
|
||||
|
||||
// 如果COM从非激活变为激活,退出余晖模式
|
||||
if (comStateChanged && comActive) {
|
||||
exitAfterglowMode();
|
||||
}
|
||||
|
||||
// 关键修复:如果COM口未激活或处于余晖模式,不处理任何引脚状态变化
|
||||
if (!comActive || isInAfterglowMode.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有当COM口激活时,才更新段的状态
|
||||
updateAfterglowBuffers();
|
||||
|
||||
// 先更新 segmentStates
|
||||
for (const pin of props.pins) {
|
||||
if (["a", "b", "c", "d", "e", "f", "g", "dp"].includes(pin.pinId)) {
|
||||
// 如果constraint为空,则默认为未激活状态
|
||||
if (!pin.constraint) {
|
||||
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const pinState = getConstraintState(pin.constraint);
|
||||
let newState: boolean;
|
||||
if (props.cathodeType === "common") {
|
||||
// 共阴极: 高电平激活段
|
||||
newState = pinState === "high";
|
||||
} else {
|
||||
// 共阳极: 低电平激活段
|
||||
newState = pinState === "low";
|
||||
}
|
||||
// 段状态只有在COM激活时才有效
|
||||
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = newState;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前状态
|
||||
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = newState && comActive;
|
||||
// 余晖判定:只有新状态持续 STABLE_THRESHOLD 次才更新 stableSegmentStates
|
||||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||||
const typedSegmentId = segmentId as keyof typeof segmentStates.value;
|
||||
const current = segmentStates.value[typedSegmentId];
|
||||
const stable = stableSegmentStates.value[typedSegmentId];
|
||||
|
||||
if (current === stable) {
|
||||
segmentStableCounters.value[segmentId] = 0; // 状态一致,计数器清零
|
||||
} else {
|
||||
segmentStableCounters.value[segmentId]++;
|
||||
if (segmentStableCounters.value[segmentId] >= STABLE_THRESHOLD) {
|
||||
stableSegmentStates.value[typedSegmentId] = current;
|
||||
segmentStableCounters.value[segmentId] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,26 +317,54 @@ function updateAfterglowBuffers() {
|
||||
afterglowBuffers.value[segmentId].unshift(currentState);
|
||||
|
||||
// 如果存储槽超过了最大容量,移除最旧的状态
|
||||
if (afterglowBuffers.value[segmentId].length > props.AFTERGLOW_BUFFER_SIZE) {
|
||||
if (
|
||||
afterglowBuffers.value[segmentId].length > props.AFTERGLOW_BUFFER_SIZE
|
||||
) {
|
||||
afterglowBuffers.value[segmentId].pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开始余晖更新间隔
|
||||
function startAfterglowUpdates() {
|
||||
if (updateIntervalTimer) return;
|
||||
// 进入余晖模式
|
||||
function enterAfterglowMode() {
|
||||
isInAfterglowMode.value = true;
|
||||
|
||||
updateIntervalTimer = window.setInterval(() => {
|
||||
updateSegmentStates();
|
||||
}, props.AFTERGLOW_UPDATE_INTERVAL);
|
||||
// 保存当前稳定状态作为余晖状态
|
||||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||||
const typedSegmentId = segmentId as keyof typeof stableSegmentStates.value;
|
||||
afterglowStates.value[typedSegmentId] = stableSegmentStates.value[typedSegmentId];
|
||||
|
||||
// 设置定时器,在余晖持续时间后退出余晖模式
|
||||
if (afterglowTimers.value[segmentId]) {
|
||||
clearTimeout(afterglowTimers.value[segmentId]!);
|
||||
}
|
||||
|
||||
afterglowTimers.value[segmentId] = setTimeout(() => {
|
||||
afterglowStates.value[typedSegmentId] = false;
|
||||
|
||||
// 检查是否所有段都已经关闭
|
||||
const allSegmentsOff = Object.values(afterglowStates.value).every(state => !state);
|
||||
if (allSegmentsOff) {
|
||||
exitAfterglowMode();
|
||||
}
|
||||
}, AFTERGLOW_DURATION.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 停止余晖更新间隔
|
||||
function stopAfterglowUpdates() {
|
||||
if (updateIntervalTimer) {
|
||||
window.clearInterval(updateIntervalTimer);
|
||||
updateIntervalTimer = null;
|
||||
// 退出余晖模式
|
||||
function exitAfterglowMode() {
|
||||
isInAfterglowMode.value = false;
|
||||
|
||||
// 清除所有定时器
|
||||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||||
if (afterglowTimers.value[segmentId]) {
|
||||
clearTimeout(afterglowTimers.value[segmentId]!);
|
||||
afterglowTimers.value[segmentId] = null;
|
||||
}
|
||||
|
||||
// 重置余晖状态
|
||||
const typedSegmentId = segmentId as keyof typeof afterglowStates.value;
|
||||
afterglowStates.value[typedSegmentId] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,17 +380,22 @@ function onConstraintChange(constraint: string, level: string) {
|
||||
onMounted(() => {
|
||||
// 初始化余晖存储槽
|
||||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||||
afterglowBuffers.value[segmentId] = Array(props.AFTERGLOW_BUFFER_SIZE).fill(false);
|
||||
afterglowBuffers.value[segmentId] = Array(props.AFTERGLOW_BUFFER_SIZE).fill(
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
updateSegmentStates();
|
||||
onConstraintStateChange(onConstraintChange);
|
||||
startAfterglowUpdates();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理约束状态监听
|
||||
stopAfterglowUpdates();
|
||||
// 清理所有余晖定时器
|
||||
for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) {
|
||||
if (afterglowTimers.value[segmentId]) {
|
||||
clearTimeout(afterglowTimers.value[segmentId]!);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 暴露属性和方法
|
||||
@@ -291,6 +429,7 @@ export function getDefaultProps() {
|
||||
size: 1,
|
||||
color: "red",
|
||||
cathodeType: "common",
|
||||
AFTERGLOW_DURATION: 500, // 默认余晖持续时间500毫秒
|
||||
pins: [
|
||||
{ pinId: "a", constraint: "", x: 10, y: 170 },
|
||||
{ pinId: "b", constraint: "", x: 25 - 1, y: 170 },
|
||||
|
||||
@@ -2,10 +2,9 @@ import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import VueKonva from "vue-konva"
|
||||
|
||||
import App from '@/App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App).use(router).use(createPinia()).use(VueKonva).mount('#app')
|
||||
const app = createApp(App).use(router).use(createPinia()).mount('#app')
|
||||
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import LoginView from '../views/LoginView.vue'
|
||||
import LabView from '../views/LabView.vue'
|
||||
import ProjectView from '../views/ProjectView.vue'
|
||||
import TestView from '../views/TestView.vue'
|
||||
import UserView from '../views/UserView.vue'
|
||||
import AdminView from '../views/AdminView.vue'
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import HomeView from "../views/HomeView.vue";
|
||||
import AuthView from "../views/AuthView.vue";
|
||||
import ProjectView from "../views/Project/Index.vue";
|
||||
import TestView from "../views/TestView.vue";
|
||||
import UserView from "@/views/User/Index.vue";
|
||||
import ExamView from "@/views/ExamView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{path: '/', name: 'home', component: HomeView},
|
||||
{path: '/login', name: 'login', component: LoginView},
|
||||
{path: '/lab/:id',name: 'lab', component: LabView},
|
||||
{path: '/project',name: 'project',component: ProjectView},
|
||||
{path: '/test', name: 'test', component: TestView},
|
||||
{path: '/user', name: 'user', component: UserView},
|
||||
{path: '/admin', name: 'admin', component: AdminView}]
|
||||
})
|
||||
{ path: "/", name: "home", component: HomeView },
|
||||
{ path: "/login", name: "login", component: AuthView },
|
||||
{ path: "/project", name: "project", component: ProjectView },
|
||||
{ path: "/test", name: "test", component: TestView },
|
||||
{ path: "/user", name: "user", component: UserView },
|
||||
{ path: "/exam", name: "exam", component: ExamView },
|
||||
],
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
@@ -1,46 +1,62 @@
|
||||
import { ref, reactive, watchPostEffect } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { isString, toNumber } from 'lodash';
|
||||
import { Common } from '@/utils/Common';
|
||||
import z from "zod"
|
||||
import { isNumber } from 'mathjs';
|
||||
import { ref, reactive, watchPostEffect } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { isString, toNumber } from "lodash";
|
||||
import z from "zod";
|
||||
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 { useDialogStore } from './dialog';
|
||||
import { useDialogStore } from "./dialog";
|
||||
import { toFileParameterOrUndefined } from "@/utils/Common";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
|
||||
export const useEquipments = defineStore('equipments', () => {
|
||||
export const useEquipments = defineStore("equipments", () => {
|
||||
// Global Stores
|
||||
const constrainsts = useConstraintsStore();
|
||||
const dialog = useDialogStore();
|
||||
|
||||
// Basic Info
|
||||
const boardAddr = ref("127.0.0.1");
|
||||
const boardPort = ref(1234);
|
||||
const boardAddr = useLocalStorage("fpga-board-addr", "127.0.0.1");
|
||||
const boardPort = useLocalStorage("fpga-board-port", 1234);
|
||||
|
||||
// Jtag
|
||||
const jtagBitstream = ref<File>();
|
||||
const jtagBoundaryScanFreq = ref(100);
|
||||
const jtagClientMutex = withTimeout(new Mutex(), 1000, new Error("JtagClient Mutex Timeout!"))
|
||||
const jtagClient = new JtagClient();
|
||||
const jtagBoundaryScanErrorCount = ref(0); // 边界扫描连续错误计数
|
||||
const maxJtagBoundaryScanErrors = 5; // 最大允许连续错误次数
|
||||
const jtagClientMutex = withTimeout(
|
||||
new Mutex(),
|
||||
1000,
|
||||
new Error("JtagClient Mutex Timeout!"),
|
||||
);
|
||||
|
||||
// Matrix Key
|
||||
const matrixKeyStates = reactive(new Array<boolean>(16).fill(false))
|
||||
const matrixKeypadClientMutex = withTimeout(new Mutex(), 1000, new Error("Matrixkeyclient Mutex Timeout!"));
|
||||
const matrixKeypadClient = new MatrixKeyClient();
|
||||
const matrixKeyStates = reactive(new Array<boolean>(16).fill(false));
|
||||
const matrixKeypadClientMutex = withTimeout(
|
||||
new Mutex(),
|
||||
1000,
|
||||
new Error("Matrixkeyclient Mutex Timeout!"),
|
||||
);
|
||||
|
||||
// Power
|
||||
const powerClientMutex = withTimeout(new Mutex(), 1000, new Error("Matrixkeyclient Mutex Timeout!"));
|
||||
const powerClient = new PowerClient();
|
||||
const powerClientMutex = withTimeout(
|
||||
new Mutex(),
|
||||
1000,
|
||||
new Error("Matrixkeyclient Mutex Timeout!"),
|
||||
);
|
||||
|
||||
// Enable Setting
|
||||
const enableJtagBoundaryScan = ref(false);
|
||||
const enableMatrixKey = ref(false);
|
||||
const enablePower = ref(false)
|
||||
const enablePower = ref(false);
|
||||
|
||||
// Watch
|
||||
watchPostEffect(async () => {
|
||||
if (true === enableJtagBoundaryScan.value) jtagBoundaryScan();
|
||||
if (true === enableJtagBoundaryScan.value) {
|
||||
// 重新启用时重置错误计数器
|
||||
jtagBoundaryScanErrorCount.value = 0;
|
||||
jtagBoundaryScan();
|
||||
}
|
||||
});
|
||||
|
||||
// Parse and Set
|
||||
@@ -60,8 +76,7 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
boardPort.value = portNumber;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (isNumber(port)) {
|
||||
} else if (isNumber(port)) {
|
||||
if (z.number().nonnegative().max(65535).safeParse(port).success) {
|
||||
boardPort.value = port;
|
||||
return true;
|
||||
@@ -70,7 +85,10 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
function setMatrixKey(keyNum: number | string | undefined, keyValue: boolean): boolean {
|
||||
function setMatrixKey(
|
||||
keyNum: number | string | undefined,
|
||||
keyValue: boolean,
|
||||
): boolean {
|
||||
let _keyNum: number;
|
||||
if (isString(keyNum)) {
|
||||
_keyNum = toNumber(keyNum);
|
||||
@@ -90,16 +108,30 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
async function jtagBoundaryScan() {
|
||||
const release = await jtagClientMutex.acquire();
|
||||
try {
|
||||
// 自动开启电源
|
||||
await powerSetOnOff(true);
|
||||
|
||||
const jtagClient = AuthManager.createAuthenticatedJtagClient();
|
||||
const portStates = await jtagClient.boundaryScanLogicalPorts(
|
||||
boardAddr.value,
|
||||
boardPort.value,
|
||||
);
|
||||
|
||||
constrainsts.batchSetConstraintStates(portStates);
|
||||
|
||||
// 扫描成功,重置错误计数器
|
||||
jtagBoundaryScanErrorCount.value = 0;
|
||||
} catch (error) {
|
||||
dialog.error("边界扫描发生错误");
|
||||
console.error(error);
|
||||
enableJtagBoundaryScan.value = false;
|
||||
jtagBoundaryScanErrorCount.value++;
|
||||
|
||||
console.error(`边界扫描错误 (${jtagBoundaryScanErrorCount.value}/${maxJtagBoundaryScanErrors}):`, error);
|
||||
|
||||
// 如果错误次数超过最大允许次数,才停止扫描并显示错误
|
||||
if (jtagBoundaryScanErrorCount.value >= maxJtagBoundaryScanErrors) {
|
||||
dialog.error("边界扫描发生连续错误,已自动停止");
|
||||
enableJtagBoundaryScan.value = false;
|
||||
jtagBoundaryScanErrorCount.value = 0; // 重置错误计数器
|
||||
}
|
||||
} finally {
|
||||
release();
|
||||
|
||||
@@ -110,9 +142,13 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
|
||||
async function jtagUploadBitstream(bitstream: File): Promise<boolean> {
|
||||
try {
|
||||
// 自动开启电源
|
||||
await powerSetOnOff(true);
|
||||
|
||||
const jtagClient = AuthManager.createAuthenticatedJtagClient();
|
||||
const resp = await jtagClient.uploadBitstream(
|
||||
boardAddr.value,
|
||||
Common.toFileParameterOrNull(bitstream),
|
||||
toFileParameterOrUndefined(bitstream),
|
||||
);
|
||||
return resp;
|
||||
} catch (e) {
|
||||
@@ -125,9 +161,13 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
async function jtagDownloadBitstream(): Promise<boolean> {
|
||||
const release = await jtagClientMutex.acquire();
|
||||
try {
|
||||
// 自动开启电源
|
||||
await powerSetOnOff(true);
|
||||
|
||||
const jtagClient = AuthManager.createAuthenticatedJtagClient();
|
||||
const resp = await jtagClient.downloadBitstream(
|
||||
boardAddr.value,
|
||||
boardPort.value
|
||||
boardPort.value,
|
||||
);
|
||||
return resp;
|
||||
} catch (e) {
|
||||
@@ -142,9 +182,13 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
async function jtagGetIDCode(isQuiet: boolean = false): Promise<number> {
|
||||
const release = await jtagClientMutex.acquire();
|
||||
try {
|
||||
// 自动开启电源
|
||||
await powerSetOnOff(true);
|
||||
|
||||
const jtagClient = AuthManager.createAuthenticatedJtagClient();
|
||||
const resp = await jtagClient.getDeviceIDCode(
|
||||
boardAddr.value,
|
||||
boardPort.value
|
||||
boardPort.value,
|
||||
);
|
||||
return resp;
|
||||
} catch (e) {
|
||||
@@ -158,10 +202,14 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
async function jtagSetSpeed(speed: number): Promise<boolean> {
|
||||
const release = await jtagClientMutex.acquire();
|
||||
try {
|
||||
// 自动开启电源
|
||||
await powerSetOnOff(true);
|
||||
|
||||
const jtagClient = AuthManager.createAuthenticatedJtagClient();
|
||||
const resp = await jtagClient.setSpeed(
|
||||
boardAddr.value,
|
||||
boardPort.value,
|
||||
speed
|
||||
speed,
|
||||
);
|
||||
return resp;
|
||||
} catch (e) {
|
||||
@@ -176,10 +224,11 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
const release = await matrixKeypadClientMutex.acquire();
|
||||
console.log("set Key !!!!!!!!!!!!");
|
||||
try {
|
||||
const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient();
|
||||
const resp = await matrixKeypadClient.setMatrixKeyStatus(
|
||||
boardAddr.value,
|
||||
boardPort.value,
|
||||
keyStates
|
||||
keyStates,
|
||||
);
|
||||
return resp;
|
||||
} catch (e) {
|
||||
@@ -194,6 +243,7 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
const release = await matrixKeypadClientMutex.acquire();
|
||||
try {
|
||||
if (enable) {
|
||||
const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient();
|
||||
const resp = await matrixKeypadClient.enabelMatrixKey(
|
||||
boardAddr.value,
|
||||
boardPort.value,
|
||||
@@ -201,6 +251,7 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
enableMatrixKey.value = resp;
|
||||
return resp;
|
||||
} else {
|
||||
const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient();
|
||||
const resp = await matrixKeypadClient.disableMatrixKey(
|
||||
boardAddr.value,
|
||||
boardPort.value,
|
||||
@@ -220,10 +271,11 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
async function powerSetOnOff(enable: boolean) {
|
||||
const release = await powerClientMutex.acquire();
|
||||
try {
|
||||
const powerClient = AuthManager.createAuthenticatedPowerClient();
|
||||
const resp = await powerClient.setPowerOnOff(
|
||||
boardAddr.value,
|
||||
boardPort.value,
|
||||
enable
|
||||
enable,
|
||||
);
|
||||
return resp;
|
||||
} catch (e) {
|
||||
@@ -246,8 +298,8 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
enableJtagBoundaryScan,
|
||||
jtagBitstream,
|
||||
jtagBoundaryScanFreq,
|
||||
jtagBoundaryScanErrorCount,
|
||||
jtagClientMutex,
|
||||
jtagClient,
|
||||
jtagUploadBitstream,
|
||||
jtagDownloadBitstream,
|
||||
jtagGetIDCode,
|
||||
@@ -257,15 +309,12 @@ export const useEquipments = defineStore('equipments', () => {
|
||||
enableMatrixKey,
|
||||
matrixKeyStates,
|
||||
matrixKeypadClientMutex,
|
||||
matrixKeypadClient,
|
||||
matrixKeypadEnable,
|
||||
matrixKeypadSetKeyStates,
|
||||
|
||||
// Power
|
||||
enablePower,
|
||||
powerClient,
|
||||
powerClientMutex,
|
||||
powerSetOnOff,
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
});
|
||||
|
||||
280
src/utils/AuthManager.ts
Normal file
280
src/utils/AuthManager.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import {
|
||||
DataClient,
|
||||
VideoStreamClient,
|
||||
BsdlParserClient,
|
||||
DDSClient,
|
||||
JtagClient,
|
||||
MatrixKeyClient,
|
||||
PowerClient,
|
||||
RemoteUpdateClient,
|
||||
TutorialClient,
|
||||
UDPClient,
|
||||
LogicAnalyzerClient,
|
||||
NetConfigClient,
|
||||
OscilloscopeApiClient,
|
||||
DebuggerClient,
|
||||
ExamClient,
|
||||
} from "@/APIClient";
|
||||
import axios, { type AxiosInstance } from "axios";
|
||||
|
||||
// 支持的客户端类型联合类型
|
||||
type SupportedClient =
|
||||
| DataClient
|
||||
| VideoStreamClient
|
||||
| BsdlParserClient
|
||||
| DDSClient
|
||||
| JtagClient
|
||||
| MatrixKeyClient
|
||||
| PowerClient
|
||||
| RemoteUpdateClient
|
||||
| TutorialClient
|
||||
| LogicAnalyzerClient
|
||||
| UDPClient
|
||||
| NetConfigClient
|
||||
| OscilloscopeApiClient
|
||||
| DebuggerClient
|
||||
| ExamClient;
|
||||
|
||||
export class AuthManager {
|
||||
// 存储token到localStorage
|
||||
public static setToken(token: string): void {
|
||||
localStorage.setItem("authToken", token);
|
||||
}
|
||||
|
||||
// 从localStorage获取token
|
||||
public static getToken(): string | null {
|
||||
return localStorage.getItem("authToken");
|
||||
}
|
||||
|
||||
// 清除token
|
||||
public static clearToken(): void {
|
||||
localStorage.removeItem("authToken");
|
||||
}
|
||||
|
||||
// 检查是否已认证
|
||||
public static async isAuthenticated(): Promise<boolean> {
|
||||
return await AuthManager.verifyToken();
|
||||
}
|
||||
|
||||
// 通用的为HTTP请求添加Authorization header的方法
|
||||
public static addAuthHeader(client: SupportedClient): void {
|
||||
const token = AuthManager.getToken();
|
||||
if (token) {
|
||||
// 创建一个自定义的 http 对象,包装原有的 fetch 方法
|
||||
const customHttp = {
|
||||
fetch: (url: RequestInfo, init?: RequestInit) => {
|
||||
if (!init) init = {};
|
||||
if (!init.headers) init.headers = {};
|
||||
|
||||
// 添加Authorization header
|
||||
if (typeof init.headers === "object" && init.headers !== null) {
|
||||
(init.headers as any)["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 使用全局 fetch 或 window.fetch
|
||||
return (window as any).fetch(url, init);
|
||||
},
|
||||
};
|
||||
|
||||
// 重新构造客户端,传入自定义的 http 对象
|
||||
const ClientClass = client.constructor as new (
|
||||
baseUrl?: string,
|
||||
http?: any,
|
||||
) => SupportedClient;
|
||||
const newClient = new ClientClass(undefined, customHttp);
|
||||
|
||||
// 将新客户端的属性复制到原客户端(这是一个 workaround)
|
||||
// 更好的做法是返回新的客户端实例
|
||||
Object.setPrototypeOf(client, Object.getPrototypeOf(newClient));
|
||||
Object.assign(client, newClient);
|
||||
}
|
||||
}
|
||||
|
||||
// 私有方法:创建带认证的HTTP客户端
|
||||
private static createAuthenticatedHttp() {
|
||||
const token = AuthManager.getToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
fetch: (url: RequestInfo, init?: RequestInit) => {
|
||||
if (!init) init = {};
|
||||
if (!init.headers) init.headers = {};
|
||||
|
||||
if (typeof init.headers === "object" && init.headers !== null) {
|
||||
(init.headers as any)["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return (window as any).fetch(url, init);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 私有方法:创建带认证的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>(
|
||||
ClientClass: new (baseUrl?: string, instance?: AxiosInstance) => T,
|
||||
): T {
|
||||
const axiosInstance = AuthManager.createAuthenticatedAxiosInstance();
|
||||
return axiosInstance
|
||||
? new ClientClass(undefined, axiosInstance)
|
||||
: new ClientClass();
|
||||
}
|
||||
|
||||
// 便捷方法:创建已配置认证的各种客户端
|
||||
public static createAuthenticatedDataClient(): DataClient {
|
||||
return AuthManager.createAuthenticatedClient(DataClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedVideoStreamClient(): VideoStreamClient {
|
||||
return AuthManager.createAuthenticatedClient(VideoStreamClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedBsdlParserClient(): BsdlParserClient {
|
||||
return AuthManager.createAuthenticatedClient(BsdlParserClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedDDSClient(): DDSClient {
|
||||
return AuthManager.createAuthenticatedClient(DDSClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedJtagClient(): JtagClient {
|
||||
return AuthManager.createAuthenticatedClient(JtagClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedMatrixKeyClient(): MatrixKeyClient {
|
||||
return AuthManager.createAuthenticatedClient(MatrixKeyClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedPowerClient(): PowerClient {
|
||||
return AuthManager.createAuthenticatedClient(PowerClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedRemoteUpdateClient(): RemoteUpdateClient {
|
||||
return AuthManager.createAuthenticatedClient(RemoteUpdateClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedTutorialClient(): TutorialClient {
|
||||
return AuthManager.createAuthenticatedClient(TutorialClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedUDPClient(): UDPClient {
|
||||
return AuthManager.createAuthenticatedClient(UDPClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedLogicAnalyzerClient(): LogicAnalyzerClient {
|
||||
return AuthManager.createAuthenticatedClient(LogicAnalyzerClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedNetConfigClient(): NetConfigClient {
|
||||
return AuthManager.createAuthenticatedClient(NetConfigClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedOscilloscopeApiClient(): OscilloscopeApiClient {
|
||||
return AuthManager.createAuthenticatedClient(OscilloscopeApiClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedDebuggerClient(): DebuggerClient {
|
||||
return AuthManager.createAuthenticatedClient(DebuggerClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedExamClient(): ExamClient {
|
||||
return AuthManager.createAuthenticatedClient(ExamClient);
|
||||
}
|
||||
|
||||
// 登录函数
|
||||
public static async login(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const client = new DataClient();
|
||||
const token = await client.login(username, password);
|
||||
|
||||
if (token) {
|
||||
AuthManager.setToken(token);
|
||||
|
||||
// 验证token
|
||||
const authClient = AuthManager.createAuthenticatedDataClient();
|
||||
await authClient.testAuth();
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
AuthManager.clearToken();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 登出函数
|
||||
public static logout(): void {
|
||||
AuthManager.clearToken();
|
||||
}
|
||||
|
||||
// 验证当前token是否有效
|
||||
public static async verifyToken(): Promise<boolean> {
|
||||
try {
|
||||
const token = AuthManager.getToken();
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const client = AuthManager.createAuthenticatedDataClient();
|
||||
await client.testAuth();
|
||||
return true;
|
||||
} catch (error) {
|
||||
AuthManager.clearToken();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证管理员权限
|
||||
public static async verifyAdminAuth(): Promise<boolean> {
|
||||
try {
|
||||
const token = AuthManager.getToken();
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const client = AuthManager.createAuthenticatedDataClient();
|
||||
await client.testAdminAuth();
|
||||
return true;
|
||||
} catch (error) {
|
||||
// 只有在token完全无效的情况下才清除token
|
||||
// 401错误表示token有效但权限不足,不应清除token
|
||||
if (error && typeof error === "object" && "status" in error) {
|
||||
// 如果是403 (Forbidden) 或401 (Unauthorized),说明token有效但权限不足
|
||||
if (error.status === 401 || error.status === 403) {
|
||||
return false;
|
||||
}
|
||||
// 其他状态码可能表示token无效,清除token
|
||||
AuthManager.clearToken();
|
||||
} else {
|
||||
// 网络错误等,不清除token
|
||||
console.error("管理员权限验证失败:", error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查客户端是否已配置认证
|
||||
public static isClientAuthenticated(client: SupportedClient): boolean {
|
||||
const token = AuthManager.getToken();
|
||||
return !!token;
|
||||
}
|
||||
}
|
||||
279
src/utils/BoardManager.ts
Normal file
279
src/utils/BoardManager.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import { ref } from "vue";
|
||||
import { createInjectionState } from "@vueuse/core";
|
||||
import { RemoteUpdateClient, DataClient, Board } from "@/APIClient";
|
||||
import { isUndefined } from "lodash";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
import { toFileParameterOrNull } from "./Common";
|
||||
|
||||
// 统一的板卡数据接口,扩展原有的Board类型
|
||||
export interface BoardData extends Board {
|
||||
defaultBitstream: string;
|
||||
goldBitstreamFile?: File;
|
||||
appBitstream1File?: File;
|
||||
appBitstream2File?: File;
|
||||
appBitstream3File?: File;
|
||||
}
|
||||
|
||||
const [useProvideBoardManager, useBoardManager] = createInjectionState(() => {
|
||||
// 远程升级相关参数
|
||||
const devPort = 1234;
|
||||
const remoteUpdater = AuthManager.createAuthenticatedRemoteUpdateClient();
|
||||
|
||||
// 统一的板卡数据
|
||||
const boards = ref<BoardData[]>([]);
|
||||
|
||||
// 获取位流编号
|
||||
function getSelectedBitstreamNum(bitstreamName: string): number {
|
||||
if (bitstreamName === "黄金位流") return 0;
|
||||
if (bitstreamName === "应用位流1") return 1;
|
||||
if (bitstreamName === "应用位流2") return 2;
|
||||
if (bitstreamName === "应用位流3") return 3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 获取所有板卡信息(管理员权限)
|
||||
async function getAllBoards(): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
// 验证管理员权限
|
||||
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
||||
if (!hasAdminAuth) {
|
||||
console.error("权限验证失败");
|
||||
return { success: false, error: "权限不足" };
|
||||
}
|
||||
|
||||
const client = AuthManager.createAuthenticatedDataClient();
|
||||
const result = await client.getAllBoards();
|
||||
|
||||
if (result) {
|
||||
// 将Board类型转换为BoardData类型,添加默认值
|
||||
boards.value = result.map((board) => {
|
||||
return {
|
||||
...board,
|
||||
defaultBitstream: "黄金位流", // 设置默认位流
|
||||
goldBitstreamFile: undefined,
|
||||
appBitstream1File: undefined,
|
||||
appBitstream2File: undefined,
|
||||
appBitstream3File: undefined,
|
||||
// Ensure methods from Board are present
|
||||
init: board.init?.bind(board),
|
||||
toJSON: board.toJSON?.bind(board),
|
||||
};
|
||||
});
|
||||
console.log("获取板卡信息成功", result.length);
|
||||
return { success: true };
|
||||
} else {
|
||||
console.error("获取板卡信息失败:返回结果为空");
|
||||
return { success: false, error: "获取板卡信息失败" };
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("获取板卡信息异常:", e);
|
||||
return { success: false, error: e.message || "获取板卡信息异常" };
|
||||
}
|
||||
}
|
||||
|
||||
// 新增板卡(管理员权限)
|
||||
async function addBoard(
|
||||
name: string,
|
||||
): Promise<{ success: boolean; error?: string; boardId?: string }> {
|
||||
try {
|
||||
// 验证管理员权限
|
||||
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
||||
if (!hasAdminAuth) {
|
||||
console.error("权限验证失败");
|
||||
return { success: false, error: "权限不足" };
|
||||
}
|
||||
|
||||
// 验证输入参数
|
||||
if (!name) {
|
||||
console.error("参数验证失败", { name });
|
||||
return { success: false, error: "参数不完整" };
|
||||
}
|
||||
|
||||
const client = AuthManager.createAuthenticatedDataClient();
|
||||
const boardId = await client.addBoard(name);
|
||||
|
||||
if (boardId) {
|
||||
console.log("新增板卡成功", { boardId, name});
|
||||
// 刷新板卡列表
|
||||
await getAllBoards();
|
||||
return { success: true };
|
||||
} else {
|
||||
console.error("新增板卡失败:返回ID为空");
|
||||
return { success: false, error: "新增板卡失败" };
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("新增板卡异常:", e);
|
||||
if (e.status === 401) {
|
||||
return { success: false, error: "权限不足" };
|
||||
} else if (e.status === 400) {
|
||||
return { success: false, error: "输入参数错误" };
|
||||
} else {
|
||||
return { success: false, error: e.message || "新增板卡异常" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除板卡(管理员权限)
|
||||
async function deleteBoard(
|
||||
boardId: string,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
// 验证管理员权限
|
||||
const hasAdminAuth = await AuthManager.verifyAdminAuth();
|
||||
if (!hasAdminAuth) {
|
||||
console.error("权限验证失败");
|
||||
return { success: false, error: "权限不足" };
|
||||
}
|
||||
|
||||
if (!boardId) {
|
||||
console.error("板卡ID为空");
|
||||
return { success: false, error: "板卡ID不能为空" };
|
||||
}
|
||||
|
||||
const client = AuthManager.createAuthenticatedDataClient();
|
||||
const result = await client.deleteBoard(boardId);
|
||||
|
||||
if (result > 0) {
|
||||
console.log("删除板卡成功", { boardId, deletedCount: result });
|
||||
// 刷新板卡列表
|
||||
await getAllBoards();
|
||||
return { success: true };
|
||||
} else {
|
||||
console.error("删除板卡失败:影响行数为0");
|
||||
return { success: false, error: "删除板卡失败" };
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("删除板卡异常:", e);
|
||||
if (e.status === 401) {
|
||||
return { success: false, error: "权限不足" };
|
||||
} else if (e.status === 400) {
|
||||
return { success: false, error: "输入参数错误" };
|
||||
} else {
|
||||
return { success: false, error: e.message || "删除板卡异常" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 上传并固化位流
|
||||
async function uploadAndDownloadBitstreams(
|
||||
board: BoardData,
|
||||
goldBitstream?: File,
|
||||
appBitstream1?: File,
|
||||
appBitstream2?: File,
|
||||
appBitstream3?: File,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
let cnt = 0;
|
||||
if (!isUndefined(goldBitstream)) cnt++;
|
||||
if (!isUndefined(appBitstream1)) cnt++;
|
||||
if (!isUndefined(appBitstream2)) cnt++;
|
||||
if (!isUndefined(appBitstream3)) cnt++;
|
||||
|
||||
if (cnt === 0) {
|
||||
console.error("未选择比特流文件");
|
||||
return { success: false, error: "未选择比特流文件" };
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("开始上传比特流", { boardIp: board.ipAddr, fileCount: cnt });
|
||||
|
||||
const uploadResult = await remoteUpdater.uploadBitstreams(
|
||||
board.ipAddr,
|
||||
toFileParameterOrNull(goldBitstream),
|
||||
toFileParameterOrNull(appBitstream1),
|
||||
toFileParameterOrNull(appBitstream2),
|
||||
toFileParameterOrNull(appBitstream3),
|
||||
);
|
||||
|
||||
if (!uploadResult) {
|
||||
console.error("上传比特流失败");
|
||||
return { success: false, error: "上传比特流失败" };
|
||||
}
|
||||
|
||||
console.log("比特流上传成功,开始固化");
|
||||
|
||||
const downloadResult = await remoteUpdater.downloadMultiBitstreams(
|
||||
board.ipAddr,
|
||||
board.port,
|
||||
getSelectedBitstreamNum(board.defaultBitstream),
|
||||
);
|
||||
|
||||
if (downloadResult != cnt) {
|
||||
console.error("固化比特流失败", {
|
||||
expected: cnt,
|
||||
actual: downloadResult,
|
||||
});
|
||||
return { success: false, error: "固化比特流失败" };
|
||||
} else {
|
||||
console.log("固化比特流成功", { count: downloadResult });
|
||||
return { success: true };
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("比特流操作异常:", e);
|
||||
return { success: false, error: e.message || "比特流操作异常" };
|
||||
}
|
||||
}
|
||||
|
||||
// 热启动位流
|
||||
async function hotresetBitstream(
|
||||
board: BoardData,
|
||||
bitstreamNum: number,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
console.log("开始热启动比特流", { boardIp: board.ipAddr, bitstreamNum });
|
||||
|
||||
const ret = await remoteUpdater.hotResetBitstream(
|
||||
board.ipAddr,
|
||||
board.port,
|
||||
bitstreamNum,
|
||||
);
|
||||
|
||||
if (ret) {
|
||||
console.log("热启动比特流成功");
|
||||
return { success: true };
|
||||
} else {
|
||||
console.error("热启动比特流失败");
|
||||
return { success: false, error: "热启动比特流失败" };
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("热启动比特流异常:", e);
|
||||
return { success: false, error: e.message || "热启动比特流异常" };
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件上传
|
||||
function handleFileChange(
|
||||
event: Event,
|
||||
board: BoardData,
|
||||
fileKey: keyof Pick<
|
||||
BoardData,
|
||||
| "goldBitstreamFile"
|
||||
| "appBitstream1File"
|
||||
| "appBitstream2File"
|
||||
| "appBitstream3File"
|
||||
>,
|
||||
) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
if (file) {
|
||||
(board as any)[fileKey] = file;
|
||||
console.log(`文件选择成功`, {
|
||||
boardIp: board.ipAddr,
|
||||
fileKey,
|
||||
fileName: file.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
boards,
|
||||
uploadAndDownloadBitstreams,
|
||||
hotresetBitstream,
|
||||
handleFileChange,
|
||||
getSelectedBitstreamNum,
|
||||
getAllBoards,
|
||||
addBoard,
|
||||
deleteBoard,
|
||||
};
|
||||
});
|
||||
|
||||
export { useProvideBoardManager, useBoardManager };
|
||||
@@ -1,22 +1,50 @@
|
||||
import { type FileParameter } from "@/APIClient";
|
||||
import { isNull, isUndefined } from "lodash";
|
||||
|
||||
export namespace Common {
|
||||
export function toFileParameter(object: File): FileParameter {
|
||||
if (isNull(object) || isUndefined(object))
|
||||
throw new Error("File is Null or Undefined");
|
||||
export function toFileParameter(object: File): FileParameter {
|
||||
if (isNull(object) || isUndefined(object))
|
||||
throw new Error("File is Null or Undefined");
|
||||
return {
|
||||
data: object,
|
||||
fileName: object.name,
|
||||
};
|
||||
}
|
||||
|
||||
export function toFileParameterOrNull(
|
||||
object?: File | null,
|
||||
): FileParameter | null {
|
||||
if (isNull(object) || isUndefined(object)) return null;
|
||||
else
|
||||
return {
|
||||
data: object,
|
||||
fileName: object.name
|
||||
}
|
||||
}
|
||||
|
||||
export function toFileParameterOrNull(object?: File | null): FileParameter | null {
|
||||
if (isNull(object) || isUndefined(object)) return null;
|
||||
else return {
|
||||
data: object,
|
||||
fileName: object.name
|
||||
}
|
||||
}
|
||||
|
||||
fileName: object.name,
|
||||
};
|
||||
}
|
||||
|
||||
export function toFileParameterOrUndefined(
|
||||
object?: File | undefined,
|
||||
): FileParameter | undefined {
|
||||
if (isNull(object) || isUndefined(object)) return undefined;
|
||||
else
|
||||
return {
|
||||
data: object,
|
||||
fileName: object.name,
|
||||
};
|
||||
}
|
||||
|
||||
// 自定义 Hook:检查依赖注入值是否为空
|
||||
export function useRequiredInjection<T>(useFn: () => T | undefined): T {
|
||||
const value = useFn();
|
||||
if (value === undefined) {
|
||||
throw new Error("Missing required injection");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function useOptionalInjection<T>(
|
||||
useFn: () => T | undefined,
|
||||
defaultValue: T,
|
||||
): T {
|
||||
const value = useFn();
|
||||
return value ?? defaultValue;
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import Konva from "konva";
|
||||
import type { VueElement } from "vue";
|
||||
|
||||
interface VNode extends VueElement {
|
||||
getNode(): Konva.Node
|
||||
}
|
||||
|
||||
interface VLayer extends VueElement {
|
||||
getNode(): Konva.Layer
|
||||
}
|
||||
|
||||
interface VGroup extends VueElement {
|
||||
getNode(): Konva.Group
|
||||
}
|
||||
|
||||
interface VStage extends VueElement {
|
||||
getNode(): Konva.Stage
|
||||
}
|
||||
|
||||
interface VTransformer extends VueElement {
|
||||
getNode(): Konva.Transformer
|
||||
}
|
||||
|
||||
export type {
|
||||
VNode,
|
||||
VLayer,
|
||||
VGroup,
|
||||
VStage,
|
||||
VTransformer,
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
<template>
|
||||
<div class="bg-base-200 min-h-screen p-6">
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<h1 class="text-3xl font-bold mb-6">FPGA 设备管理</h1>
|
||||
<button class="btn btn-ghost text-error hover:underline" @click="
|
||||
() => {
|
||||
isEditMode = !isEditMode;
|
||||
}
|
||||
">
|
||||
编辑
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<h2 class="card-title mb-4">IP 地址列表</h2>
|
||||
<button class="btn btn-ghost" @click="">刷新</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<!-- 表头 -->
|
||||
<thead>
|
||||
<tr class="bg-base-300">
|
||||
<th class="w-50">IP 地址</th>
|
||||
<th class="w-30">版本号</th>
|
||||
<th class="w-50">默认启动位流</th>
|
||||
<th class="w-80">黄金位流</th>
|
||||
<th class="w-80">应用位流1</th>
|
||||
<th class="w-80">应用位流2</th>
|
||||
<th class="w-80">应用位流3</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<!-- 表格内容 -->
|
||||
<tbody>
|
||||
<tr class="hover">
|
||||
<td class="font-medium">
|
||||
<input v-if="isEditMode" type="text" placeholder="Type here" class="input m-0" v-model="devAddr" />
|
||||
<span v-else>{{ devAddr }}</span>
|
||||
</td>
|
||||
<td>v1.2.3</td>
|
||||
<td>
|
||||
<select class="select select-bordered w-full max-w-xs" v-model="selectBitstream">
|
||||
<option selected>黄金位流</option>
|
||||
<option>应用位流1</option>
|
||||
<option>应用位流2</option>
|
||||
<option>应用位流3</option>
|
||||
</select>
|
||||
</td>
|
||||
<!-- 黄金位流上传区 -->
|
||||
<td>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<input type="file" class="file-input file-input-primary" @change="handleFileChange($event, 0)" />
|
||||
</div>
|
||||
</td>
|
||||
<!-- 应用位流1上传区 -->
|
||||
<td>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<input type="file" class="file-input file-input-secondary" @change="handleFileChange($event, 1)" />
|
||||
</div>
|
||||
</td>
|
||||
<!-- 应用位流2上传区 -->
|
||||
<td>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<input type="file" class="file-input file-input-accent" @change="handleFileChange($event, 2)" />
|
||||
</div>
|
||||
</td>
|
||||
<!-- 应用位流3上传区 -->
|
||||
<td>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<input type="file" class="file-input file-input-info" @change="handleFileChange($event, 3)" />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<td class="flex gap-2">
|
||||
<button class="btn grow btn-warning" @click="
|
||||
uploadAndDownloadBitstreams(
|
||||
devAddr,
|
||||
goldBitstreamFile,
|
||||
appBitstream1File,
|
||||
appBitstream2File,
|
||||
appBitstream3File,
|
||||
)
|
||||
">
|
||||
固化
|
||||
</button>
|
||||
<button class="btn grow btn-success" @click="
|
||||
hotresetBitstream(devAddr, getSelectedBitstreamNum())
|
||||
">
|
||||
切换并热启动
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 bg-base-300 p-4 rounded-lg">
|
||||
<p class="text-sm opacity-80">
|
||||
<span class="font-semibold text-error">提示:</span>
|
||||
请谨慎操作FPGA固化和热启动功能,确保上传的位流文件无误,以避免设备损坏。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isNull, isUndefined } from "lodash";
|
||||
import { ref } from "vue";
|
||||
import { RemoteUpdateClient } from "@/APIClient";
|
||||
import { useDialogStore } from "@/stores/dialog";
|
||||
import { Common } from "@/utils/Common";
|
||||
|
||||
const dialog = useDialogStore();
|
||||
|
||||
// 编辑状态
|
||||
const isEditMode = ref(false);
|
||||
|
||||
// 选择热切换的比特流
|
||||
const selectBitstream = ref("黄金位流");
|
||||
|
||||
// 存储上传文件的信息
|
||||
const goldBitstreamFile = ref<File>();
|
||||
const appBitstream1File = ref<File>();
|
||||
const appBitstream2File = ref<File>();
|
||||
const appBitstream3File = ref<File>();
|
||||
|
||||
// 远程升级相关参数
|
||||
const devAddr = ref("192.168.1.100");
|
||||
const devPort = 1234;
|
||||
const remoteUpdater = new RemoteUpdateClient();
|
||||
|
||||
// 处理文件上传
|
||||
function handleFileChange(event: Event, bistreamNum: number) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0]; // 获取选中的第一个文件
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bistreamNum === 0) {
|
||||
goldBitstreamFile.value = file;
|
||||
} else if (bistreamNum === 1) {
|
||||
appBitstream1File.value = file;
|
||||
} else if (bistreamNum === 2) {
|
||||
appBitstream2File.value = file;
|
||||
} else if (bistreamNum === 3) {
|
||||
appBitstream3File.value = file;
|
||||
} else {
|
||||
goldBitstreamFile.value = file;
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedBitstreamNum(): number {
|
||||
if (selectBitstream.value == "黄金位流") {
|
||||
return 0;
|
||||
} else if (selectBitstream.value == "应用位流1") {
|
||||
return 1;
|
||||
} else if (selectBitstream.value == "应用位流2") {
|
||||
return 2;
|
||||
} else if (selectBitstream.value == "应用位流3") {
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function uploadAndDownloadBitstreams(
|
||||
devAddr: string,
|
||||
goldBitstream?: File,
|
||||
appBitstream1?: File,
|
||||
appBitstream2?: File,
|
||||
appBitstream3?: File,
|
||||
) {
|
||||
let cnt = 0;
|
||||
if (!isUndefined(goldBitstream)) cnt++;
|
||||
if (!isUndefined(appBitstream1)) cnt++;
|
||||
if (!isUndefined(appBitstream2)) cnt++;
|
||||
if (!isUndefined(appBitstream3)) cnt++;
|
||||
if (cnt === 0) {
|
||||
dialog.error("未选择比特流");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
{
|
||||
const ret = await remoteUpdater.uploadBitstreams(
|
||||
devAddr,
|
||||
Common.toFileParameterOrNull(goldBitstream),
|
||||
Common.toFileParameterOrNull(appBitstream1),
|
||||
Common.toFileParameterOrNull(appBitstream2),
|
||||
Common.toFileParameterOrNull(appBitstream3),
|
||||
);
|
||||
if (!ret) {
|
||||
dialog.warn("上传比特流出错");
|
||||
return;
|
||||
}
|
||||
}
|
||||
{
|
||||
const ret = await remoteUpdater.downloadMultiBitstreams(
|
||||
devAddr,
|
||||
devPort,
|
||||
getSelectedBitstreamNum(),
|
||||
);
|
||||
if (ret != cnt) {
|
||||
dialog.warn("固化比特流出错");
|
||||
} else {
|
||||
dialog.info("固化比特流成功");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
dialog.error("比特流上传错误");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function hotresetBitstream(devAddr: string, bitstreamNum: number) {
|
||||
try {
|
||||
const ret = await remoteUpdater.hotResetBitstream(
|
||||
devAddr,
|
||||
devPort,
|
||||
bitstreamNum,
|
||||
);
|
||||
if (ret) {
|
||||
dialog.info("切换比特流成功");
|
||||
} else {
|
||||
dialog.error("切换比特流失败");
|
||||
}
|
||||
} catch (e) {
|
||||
dialog.error("切换比特流失败");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshData() {
|
||||
try {
|
||||
const ret = await remoteUpdater.getFirmwareVersion(devAddr.value, devPort);
|
||||
} catch (e) {
|
||||
dialog.error("获取数据失败");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
@import "../assets/main.css";
|
||||
</style>
|
||||
288
src/views/AuthView.vue
Normal file
288
src/views/AuthView.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center min-h-screen bg-base-200">
|
||||
<div class="relative w-full max-w-md">
|
||||
<!-- Login Card -->
|
||||
<div v-if="!showSignUp" class="card card-dash h-80 w-100 shadow-xl bg-base-100">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title place-self-center my-3 text-2xl">用户登录</h1>
|
||||
<div class="flex flex-col w-full h-full">
|
||||
<label class="input w-full my-3">
|
||||
<User class="h-[1em] opacity-50" />
|
||||
<input
|
||||
type="text"
|
||||
class="grow"
|
||||
placeholder="用户名"
|
||||
v-model="username"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</label>
|
||||
<label class="input w-full my-3">
|
||||
<Lock class="h-[1em] opacity-50" />
|
||||
<input
|
||||
type="password"
|
||||
class="grow"
|
||||
placeholder="密码"
|
||||
v-model="password"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex justify-end mx-3">
|
||||
<RouterLink to="/">忘记密码?</RouterLink>
|
||||
</div>
|
||||
<div class="card-actions flex items-end my-3">
|
||||
<button class="btn flex-1" @click="handleRegister">注册</button>
|
||||
<button
|
||||
class="btn btn-primary flex-3"
|
||||
@click="handleLogin"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
{{ isLoading ? "登录中..." : "登录" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sign Up Card -->
|
||||
<div v-if="showSignUp" class="card card-dash h-96 w-100 shadow-xl bg-base-100">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title place-self-center my-3 text-2xl">用户注册</h1>
|
||||
<div class="flex flex-col w-full h-full">
|
||||
<label class="input w-full my-2">
|
||||
<User class="h-[1em] opacity-50" />
|
||||
<input
|
||||
type="text"
|
||||
class="grow"
|
||||
placeholder="用户名"
|
||||
v-model="signUpData.username"
|
||||
@keyup.enter="handleSignUp"
|
||||
/>
|
||||
</label>
|
||||
<label class="input w-full my-2">
|
||||
<Mail class="h-[1em] opacity-50" />
|
||||
<input
|
||||
type="email"
|
||||
class="grow"
|
||||
placeholder="邮箱"
|
||||
v-model="signUpData.email"
|
||||
@keyup.enter="handleSignUp"
|
||||
/>
|
||||
</label>
|
||||
<label class="input w-full my-2">
|
||||
<Lock class="h-[1em] opacity-50" />
|
||||
<input
|
||||
type="password"
|
||||
class="grow"
|
||||
placeholder="密码"
|
||||
v-model="signUpData.password"
|
||||
@keyup.enter="handleSignUp"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="card-actions flex items-end my-3">
|
||||
<button class="btn flex-1" @click="backToLogin">返回登录</button>
|
||||
<button
|
||||
class="btn btn-primary flex-3"
|
||||
@click="handleSignUp"
|
||||
:disabled="isSignUpLoading"
|
||||
>
|
||||
{{ isSignUpLoading ? "注册中..." : "注册" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { AuthManager } from "@/utils/AuthManager";
|
||||
import { useAlertStore } from "@/components/Alert";
|
||||
import { User, Lock, Mail } from "lucide-vue-next";
|
||||
import { DataClient } from "@/APIClient";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 获取Alert store
|
||||
const alertStore = useAlertStore();
|
||||
|
||||
// 创建API客户端实例
|
||||
const dataClient = new DataClient();
|
||||
|
||||
// 响应式数据
|
||||
const username = ref("");
|
||||
const password = ref("");
|
||||
const isLoading = ref(false);
|
||||
|
||||
// 注册相关数据
|
||||
const showSignUp = ref(false);
|
||||
const isSignUpLoading = ref(false);
|
||||
const signUpData = ref({
|
||||
username: "",
|
||||
email: "",
|
||||
password: ""
|
||||
});
|
||||
|
||||
// 登录处理函数
|
||||
const handleLogin = async () => {
|
||||
// 验证输入
|
||||
if (!username.value.trim()) {
|
||||
alertStore?.show("请输入用户名", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password.value.trim()) {
|
||||
alertStore?.show("请输入密码", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// 调用AuthManager的登录函数
|
||||
await AuthManager.login(username.value.trim(), password.value.trim());
|
||||
|
||||
// 登录成功,显示成功消息并跳转
|
||||
alertStore?.show("登录成功", "success", 1000);
|
||||
|
||||
// 短暂延迟后跳转到project页面
|
||||
setTimeout(async () => {
|
||||
await router.push("/project");
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
console.error("Login error:", error);
|
||||
|
||||
// 处理不同类型的错误
|
||||
let errorMessage = "登录失败,请检查网络连接";
|
||||
|
||||
if (error.status === 400) {
|
||||
errorMessage = "用户名或密码错误";
|
||||
} else if (error.status === 401) {
|
||||
errorMessage = "用户名或密码错误";
|
||||
} else if (error.status === 500) {
|
||||
errorMessage = "服务器错误,请稍后重试";
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
alertStore?.show(errorMessage, "error");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 注册处理函数
|
||||
const handleRegister = () => {
|
||||
showSignUp.value = true;
|
||||
// 清空注册表单
|
||||
signUpData.value = {
|
||||
username: "",
|
||||
email: "",
|
||||
password: ""
|
||||
};
|
||||
};
|
||||
|
||||
// 返回登录页面
|
||||
const backToLogin = () => {
|
||||
showSignUp.value = false;
|
||||
};
|
||||
|
||||
// 注册提交处理函数
|
||||
const handleSignUp = async () => {
|
||||
// 验证输入
|
||||
if (!signUpData.value.username.trim()) {
|
||||
alertStore?.show("请输入用户名", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!signUpData.value.email.trim()) {
|
||||
alertStore?.show("请输入邮箱", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的邮箱格式验证
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(signUpData.value.email.trim())) {
|
||||
alertStore?.show("请输入有效的邮箱地址", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!signUpData.value.password.trim()) {
|
||||
alertStore?.show("请输入密码", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 密码长度验证
|
||||
if (signUpData.value.password.length < 6) {
|
||||
alertStore?.show("密码长度至少6位", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
isSignUpLoading.value = true;
|
||||
|
||||
try {
|
||||
// 调用注册API
|
||||
const result = await dataClient.signUpUser(
|
||||
signUpData.value.username.trim(),
|
||||
signUpData.value.email.trim(),
|
||||
signUpData.value.password.trim()
|
||||
);
|
||||
|
||||
if (result) {
|
||||
// 注册成功
|
||||
alertStore?.show("注册成功!请登录", "success", 2000);
|
||||
|
||||
// 延迟后返回登录页面
|
||||
setTimeout(() => {
|
||||
backToLogin();
|
||||
}, 2000);
|
||||
} else {
|
||||
alertStore?.show("注册失败,请重试", "error");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Sign up error:", error);
|
||||
|
||||
let errorMessage = "注册失败,请检查网络连接";
|
||||
|
||||
if (error.status === 400) {
|
||||
// 检查是否有详细的错误信息
|
||||
if (error.result && error.result.detail) {
|
||||
errorMessage = error.result.detail;
|
||||
} else {
|
||||
errorMessage = "注册信息无效,请检查输入";
|
||||
}
|
||||
} else if (error.status === 500) {
|
||||
errorMessage = "服务器错误,请稍后重试";
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
alertStore?.show(errorMessage, "error");
|
||||
} finally {
|
||||
isSignUpLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 页面初始化时检查是否已有有效token
|
||||
const checkExistingToken = async () => {
|
||||
try {
|
||||
const isValid = await AuthManager.verifyToken();
|
||||
if (isValid) {
|
||||
// 如果token仍然有效,直接跳转到project页面
|
||||
await router.push("/project");
|
||||
}
|
||||
} catch (error) {
|
||||
// token无效或验证失败,继续显示登录页面
|
||||
console.log("Token verification failed, showing login page");
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时检查已存在的token
|
||||
onMounted(() => {
|
||||
checkExistingToken();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
1069
src/views/ExamView.vue
Normal file
1069
src/views/ExamView.vue
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user