Compare commits
287 Commits
b6839af5d2
...
pyg
| Author | SHA1 | Date | |
|---|---|---|---|
| aff9da2a60 | |||
|
|
e0ac21d141 | ||
| 8396b7aaea | |||
|
|
a331494fde | ||
|
|
e86cd5464e | ||
|
|
04b136117d | ||
|
|
5c87204ef6 | ||
| 35647d21bb | |||
|
|
51b39cee07 | ||
|
|
0bd1ad8a0e | ||
|
|
f2c7c78b64 | ||
|
|
2f23ffe482 | ||
|
|
9904fecbee | ||
| cb229c2a30 | |||
|
|
e5f2be616c | ||
|
|
2e9e378457 | ||
|
|
9fe0ee959f | ||
| 9adc5295f8 | |||
|
|
8047987935 | ||
|
|
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 | |||
| de28471f87 | |||
| 3a292c0a98 | |||
| 91b00a977c | |||
| c5ce246caf | |||
| 497fa731ca | |||
| 443aea5e3e | |||
| 67bdec8570 | |||
| 1af3fa3a8f | |||
| dd7efe3c84 | |||
|
|
23236b22bd | ||
|
|
ef1a6c8208 | ||
| ff7f7b5a76 | |||
| da7b3f4a4b | |||
| a9ab5926ed | |||
| 2e084bfb58 | |||
| 221d598a6e | |||
| c3bd61ed51 | |||
| 287c416247 | |||
| e84a784517 | |||
| 178ac0de67 | |||
| bed0158a5f | |||
| 7ffb15c722 | |||
|
|
5ba71d220f | ||
| 14d8499f77 | |||
| d18cf82813 | |||
| f1e2dbd9d8 | |||
| 262c5e4003 | |||
| fbd13f8f2f | |||
| 6cf7ef02ac | |||
|
|
4c14ada97b | ||
| 8207c37e12 | |||
| db71681bdf | |||
| 2270022bbe | |||
| dcadb97a7f | |||
| 1538bb9d07 | |||
| f340c86a41 | |||
| b6fb7e05fa | |||
| e0db12e0eb | |||
| 81f91b2b71 | |||
|
|
bbfe06822d | ||
|
|
d73166187a | ||
|
|
2eabb79d0f | ||
|
|
a865cfc950 | ||
| fa7c947351 | |||
| dc64a65702 | |||
| 46621fdb40 | |||
| 970a537391 | |||
| 3883cd8304 | |||
| 2aa2f1dc37 | |||
| 1bdcb672ab | |||
| 7b1d1a5e87 | |||
| 5bb011e685 | |||
| e3826f0ff6 | |||
| f8163be98b | |||
|
|
ea7c2d425a | ||
| e4791b41a8 | |||
| bea1c7e5ae | |||
|
|
b6d8612e8c | ||
| b68d8eaf11 | |||
| 390ce8250d | |||
|
|
09b8f676ba | ||
|
|
cc83133a6c | ||
|
|
6d640e8049 | ||
| 07eb606324 | |||
|
|
8eefed92a8 | ||
|
|
8f2f90ef1d | ||
| 487e7c114a | |||
| 7f37514dfa | |||
| 0b0b4acb17 | |||
|
|
0bd3b42840 | ||
| 068576b60b | |||
| d754a881d7 | |||
| a6ac728cf1 | |||
| ba6ec73b84 | |||
| 5042bf8ce5 | |||
| 2a3ef1ea7d | |||
| 4885447c2b | |||
| 351a230107 | |||
| 7aff4f3f02 | |||
| c7907b4253 | |||
| 3fb59af2dd | |||
| 9b2ee8ad46 | |||
|
|
7dd5e2189f | ||
| c39f688115 | |||
| 1eded97c76 | |||
| 00ce79fa7b | |||
|
|
48ae3b5975 | ||
|
|
21197887cd | ||
| f0fa32aeaa | |||
| 9657aacf83 | |||
| eea03f5bc8 | |||
| eae67d04d4 | |||
| 1102bba40d | |||
| 099d44663d | |||
| 74d40d25e2 | |||
| 8699a568d1 | |||
| 9eb3acb94c | |||
| 257d63e63d | |||
|
|
91838ff632 | ||
| 020674a277 | |||
|
|
6a786c1519 | ||
| 10918a997c | |||
|
|
d4b34bd6d4 | ||
|
|
47cfe17d16 | ||
| 13a71368da | |||
| 51fd1145d8 | |||
| f75b245abc | |||
| 55e363d505 | |||
| c6b819f24f | |||
| e6d177cf15 | |||
| 210f6aa61c | |||
|
|
1c75aa621a | ||
| 3f77a1a426 | |||
|
|
10db7c67bf | ||
|
|
b3a5342d6b | ||
| f35a295af2 |
5
.gitignore
vendored
@@ -9,13 +9,16 @@ lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
.vscode
|
||||
dist
|
||||
**/wwwroot
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
DebuggerCmd.md
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
@@ -25,10 +28,10 @@ coverage
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Generated Files
|
||||
*.sqlite
|
||||
components.d.ts
|
||||
|
||||
45
.justfile
@@ -1,3 +1,6 @@
|
||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||
isSelfContained := "false"
|
||||
|
||||
@_show-dir:
|
||||
echo "Current Working Directory:"
|
||||
pwd
|
||||
@@ -10,25 +13,49 @@ clean:
|
||||
rm -rf "server.test/bin"
|
||||
rm -rf "server.test/obj"
|
||||
rm -rf "dist"
|
||||
rm -rf "wwwroot"
|
||||
|
||||
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:
|
||||
cd server && dotnet run &
|
||||
npm run gen-api
|
||||
|
||||
gen-api-from-server:
|
||||
npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts
|
||||
pkill server
|
||||
|
||||
# 构建服务器,包含win与linux平台
|
||||
[working-directory: "server"]
|
||||
build-server: _show-dir
|
||||
dotnet publish --self-contained false -t:PublishAllRids
|
||||
build-server self-contained=isSelfContained: _show-dir
|
||||
dotnet publish --self-contained {{self-contained}} -t:PublishAllRids
|
||||
npm run build
|
||||
rsync -avz --delete ../wwwroot/ ./bin/Release/net9.0/linux-x64/publish/wwwroot/
|
||||
rsync -avz --delete ../wwwroot/ ./bin/Release/net9.0/win-x64/publish/wwwroot/
|
||||
|
||||
# 运行服务器
|
||||
[working-directory: "server"]
|
||||
run-server: _show-dir
|
||||
dotnet run
|
||||
run: run-server
|
||||
|
||||
run-server: (build-server "true")
|
||||
./server/bin/Release/net9.0/linux-x64/publish/server
|
||||
|
||||
run-web:
|
||||
npm run build
|
||||
npm run preview
|
||||
|
||||
dev: dev-server
|
||||
|
||||
# 测试服务器
|
||||
dev-server: _show-dir
|
||||
dotnet run --watch --project ./server/server.csproj
|
||||
|
||||
# 运行网页客户端
|
||||
run-web:
|
||||
dev-web:
|
||||
npm run dev
|
||||
|
||||
# 运行测试用例测试服务器
|
||||
|
||||
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
@@ -0,0 +1,13 @@
|
||||
# TODO
|
||||
|
||||
1. 后端HTTP视频流
|
||||
|
||||
640*480, RGB565
|
||||
0x0000_0000 + 25800
|
||||
|
||||
|
||||
2. 信号发生器界面导入.dat文件
|
||||
3. 示波器后端交互、前端界面
|
||||
4. 逻辑分析仪后端交互、前端界面
|
||||
5. 前端重构
|
||||
6. 数据库 —— 用户登录、板卡资源分配、板卡IP地址分配
|
||||
10
flake.lock
generated
@@ -2,12 +2,12 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1741246872,
|
||||
"narHash": "sha256-Q6pMP4a9ed636qilcYX8XUguvKl/0/LGXhHcRI91p0U=",
|
||||
"rev": "10069ef4cf863633f57238f179a0297de84bd8d3",
|
||||
"revCount": 763342,
|
||||
"lastModified": 1748929857,
|
||||
"narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=",
|
||||
"rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4",
|
||||
"revCount": 810143,
|
||||
"type": "tarball",
|
||||
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.763342%2Brev-10069ef4cf863633f57238f179a0297de84bd8d3/01956ed4-f66c-7a87-98e4-b7e58f4aa591/source.tar.gz"
|
||||
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.810143%2Brev-c2a03962b8e24e669fb37b7df10e7c79531ff1a4/01973914-8b42-7168-9ee2-4d6ea6946695/source.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
sqlite
|
||||
sqls
|
||||
sql-studio
|
||||
zlib
|
||||
bash
|
||||
# Backend
|
||||
(dotnetCorePackages.combinePackages [
|
||||
dotnetCorePackages.sdk_9_0
|
||||
@@ -37,9 +39,10 @@
|
||||
typescript-language-server
|
||||
];
|
||||
shellHook = ''
|
||||
export PATH=$PATH:$HOME/.bun/bin
|
||||
export PATH=$PATH:/home/sikongjueluo/.dotnet/tools
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${pkgs.zlib}/lib
|
||||
export DOTNET_ROOT=${pkgs.dotnetCorePackages.sdk_9_0}/share/dotnet
|
||||
'';
|
||||
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
1776
package-lock.json
generated
27
package.json
@@ -4,26 +4,39 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"pregen-api": "cd server && dotnet run &",
|
||||
"gen-api": "npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts",
|
||||
"postgen-api": "pkill server"
|
||||
"gen-api": "npx tsx scripts/GenerateWebAPI.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/signalr": "^9.0.6",
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/signalr": "^2.4.3",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"axios": "^1.11.0",
|
||||
"echarts": "^5.6.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"konva": "^9.3.20",
|
||||
"lodash": "^4.17.21",
|
||||
"log-symbols": "^7.0.0",
|
||||
"lucide-vue-next": "^0.525.0",
|
||||
"marked": "^12.0.0",
|
||||
"mathjs": "^14.4.0",
|
||||
"pinia": "^3.0.1",
|
||||
"tinypool": "^1.0.2",
|
||||
"reka-ui": "^2.3.1",
|
||||
"ts-log": "^2.2.7",
|
||||
"ts-results-es": "^5.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-echarts": "^7.0.3",
|
||||
"vue-konva": "^3.2.1",
|
||||
"vue-router": "4",
|
||||
"yocto-queue": "^1.2.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -35,11 +48,15 @@
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"daisyui": "^5.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"nswag": "^14.3.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "~5.7.3",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vue-tsc": "^2.2.2"
|
||||
|
||||
314
public/EquipmentTemplates/MatrixKey.json
Normal file
@@ -0,0 +1,314 @@
|
||||
{
|
||||
"version": 1,
|
||||
"author": "template",
|
||||
"editor": "system",
|
||||
"parts": [
|
||||
{
|
||||
"id": "board",
|
||||
"type": "BaseBoard",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"attrs": {
|
||||
"size": 1.2,
|
||||
"width": 400,
|
||||
"height": 400,
|
||||
"roundCorner": 20
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": false,
|
||||
"isOn": true,
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"id": "key_0_0",
|
||||
"type": "MechanicalButton",
|
||||
"x": 50,
|
||||
"y": 50,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "1",
|
||||
"bindMatrixKey": "0",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"id": "key_0_1",
|
||||
"type": "MechanicalButton",
|
||||
"x": 150,
|
||||
"y": 50,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "2",
|
||||
"bindMatrixKey": "1",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"id": "key_0_2",
|
||||
"type": "MechanicalButton",
|
||||
"x": 250,
|
||||
"y": 50,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "3",
|
||||
"bindMatrixKey": "2",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 2
|
||||
},
|
||||
{
|
||||
"id": "key_0_3",
|
||||
"type": "MechanicalButton",
|
||||
"x": 350,
|
||||
"y": 50,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "A",
|
||||
"bindMatrixKey": "3",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 3
|
||||
},
|
||||
{
|
||||
"id": "key_1_0",
|
||||
"type": "MechanicalButton",
|
||||
"x": 50,
|
||||
"y": 150,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "4",
|
||||
"bindMatrixKey": "4",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 4
|
||||
},
|
||||
{
|
||||
"id": "key_1_1",
|
||||
"type": "MechanicalButton",
|
||||
"x": 150,
|
||||
"y": 150,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "5",
|
||||
"bindMatrixKey": "5",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 5
|
||||
},
|
||||
{
|
||||
"id": "key_1_2",
|
||||
"type": "MechanicalButton",
|
||||
"x": 250,
|
||||
"y": 150,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "6",
|
||||
"bindMatrixKey": "6",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 6
|
||||
},
|
||||
{
|
||||
"id": "key_1_3",
|
||||
"type": "MechanicalButton",
|
||||
"x": 350,
|
||||
"y": 150,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "B",
|
||||
"bindMatrixKey": "7",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 7
|
||||
},
|
||||
{
|
||||
"id": "key_2_0",
|
||||
"type": "MechanicalButton",
|
||||
"x": 50,
|
||||
"y": 250,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "7",
|
||||
"bindMatrixKey": "8",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 8
|
||||
},
|
||||
{
|
||||
"id": "key_2_1",
|
||||
"type": "MechanicalButton",
|
||||
"x": 150,
|
||||
"y": 250,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "8",
|
||||
"bindMatrixKey": "9",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 9
|
||||
},
|
||||
{
|
||||
"id": "key_2_2",
|
||||
"type": "MechanicalButton",
|
||||
"x": 250,
|
||||
"y": 250,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "9",
|
||||
"bindMatrixKey": "10",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 10
|
||||
},
|
||||
{
|
||||
"id": "key_2_3",
|
||||
"type": "MechanicalButton",
|
||||
"x": 350,
|
||||
"y": 250,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "C",
|
||||
"bindMatrixKey": "11",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 11
|
||||
},
|
||||
{
|
||||
"id": "key_3_0",
|
||||
"type": "MechanicalButton",
|
||||
"x": 50,
|
||||
"y": 350,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "*",
|
||||
"bindMatrixKey": "12",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 12
|
||||
},
|
||||
{
|
||||
"id": "key_3_1",
|
||||
"type": "MechanicalButton",
|
||||
"x": 150,
|
||||
"y": 350,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "0",
|
||||
"bindMatrixKey": "13",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 13
|
||||
},
|
||||
{
|
||||
"id": "key_3_2",
|
||||
"type": "MechanicalButton",
|
||||
"x": 250,
|
||||
"y": 350,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "#",
|
||||
"bindMatrixKey": "14",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 14
|
||||
},
|
||||
{
|
||||
"id": "key_3_3",
|
||||
"type": "MechanicalButton",
|
||||
"x": 350,
|
||||
"y": 350,
|
||||
"attrs": {
|
||||
"size": 0.5,
|
||||
"bindKey": "D",
|
||||
"bindMatrixKey": "15",
|
||||
"pins": []
|
||||
},
|
||||
"rotate": 0,
|
||||
"group": "MatrixKeypad",
|
||||
"positionlock": false,
|
||||
"hidepins": true,
|
||||
"isOn": false,
|
||||
"index": 15
|
||||
}
|
||||
],
|
||||
"connections": []
|
||||
}
|
||||
3417
public/EquipmentTemplates/PG2L100H_Pango100pro.json
Normal file
BIN
public/doc/01/cover.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
175
public/doc/01/doc.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# 基础-1-流水灯
|
||||
|
||||
## 1.1 章节导读
|
||||
|
||||
流水灯实验作为基础实验的第一个实验是非常合适的,本章我们利用试验箱中的LED进行点亮LED,并实现流水灯的功能。
|
||||
|
||||
## 1.2 理论学习
|
||||
|
||||
相信大家之前肯定接触过单片机等设备,而学习这些设备的第一个实验例程往往都是点亮一个LED。本次实验在点亮LED的基础上另LED灯依次闪亮,循环不止,实现“流水”的功能。其原理是依次控制连接到LED的IO口的电平高低,让LED的闪亮间隔为0.5s,以实现流水灯的效果。
|
||||
|
||||
## 1.3 实战演练
|
||||
|
||||
### 1.3.1实验目标
|
||||
|
||||
依次点亮实验板中的8个LED灯,两灯点亮间隔为0.5s,每次点亮持续0.5s,实现流水灯效果。
|
||||
|
||||
### 1.3.2硬件资源
|
||||
|
||||
实验板上有0~31共32个LED灯的资源,每4个LED灯为一组,分别是绿,红,蓝,黄四种颜色,本次实验使用8个LED进行验证。
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/1.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:30%"/>
|
||||
<br> <!--换行-->
|
||||
图1.LED扩展板 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
通过原理图可以得知,本试验箱的LED灯为高电平时点亮。
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/2.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:40%"/>
|
||||
<br> <!--换行-->
|
||||
图2.LED扩展板原理图 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
### 1.3.3程序设计
|
||||
|
||||
流水灯的设计与分频器,计数器的逻辑相似,只是多了LED灯的点亮部分。为了实现计数器肯定需要时钟信号sysclk,也需要一个复位信号rstn,同时为了驱动LED,需要8个IO口。所以模块的端口如下表所示:
|
||||
|
||||
| 端口名称 | 端口位宽 | 端口类型 |功能描述
|
||||
|:----------:|:----:|:----:|:--------------------:|
|
||||
| sysclk | 1Bit | Input | 输入时钟,频率27M |
|
||||
| rstn | 1Bit | Input | 复位信号,低电平有效 |
|
||||
| led | 8Bit | Output | LED控制信号 |
|
||||
|
||||
|
||||
为了使灯点亮0.5s,我们应该设计一个计数器或者是分频器,先将板载27M高频时钟降速。在27M时钟下计数0.5s,需要计数器计数13_500_000个数,也就是计数器从0开始计数到13_499_999。所以我们定义一个寄存器cnt,每一次时钟上升沿cnt就加1,当计数到13_499_999时,led的状态改变,同时cnt归零重新开始计数。
|
||||
|
||||
为了实现8个led流水的效果,我们将0定义为led灭,1表示亮,初始状态led = 8’b0000_0001,当经过0.5s后,也就是cnt等于13_499_999的时候,第一个led灭,第二个led亮起,也就是led = 8‘b0000_0010。同理,再过0.5s,led = 8’b0000_0100,再过0.5s,led = 8‘b0000_1000以此类推。
|
||||
|
||||
根据上面的规律我们很容易发现,led的流水是靠1的移位来实现的,也就是最基本的左移(<<)和右移(>>)运算符去实现。在这里我们需要向左移位,并且每次只需要移动1位。模块的参考代码(waterled_top.v)如下所示:
|
||||
|
||||
```verilog
|
||||
module waterled_top(
|
||||
input sysclk, //27MHz system clock
|
||||
input rstn, //active low reset
|
||||
output [7:0] led
|
||||
);
|
||||
parameter CNT_MAX = 32'd13_499_999;
|
||||
reg [7:0] led_reg;
|
||||
reg [31:0] cnt;
|
||||
//cnt 当cnt == CNT_MAX时变为0,计数0.5秒
|
||||
always @(posedge sysclk) begin
|
||||
if (!rstn)
|
||||
cnt <= 0;
|
||||
else if (cnt < CNT_MAX)
|
||||
cnt <= cnt + 1;
|
||||
else
|
||||
cnt <= 0;
|
||||
end
|
||||
//led_reg 当cnt == CNT_MAX时,左移一位。
|
||||
always @(posedge sysclk) begin
|
||||
if (!rstn)
|
||||
led_reg <= 8'b0000_0001;
|
||||
else if (led_reg == 8'b1000_0000 && cnt == CNT_MAX)//led7亮0.5s后重回led0
|
||||
led_reg <= 8'b0000_0001;
|
||||
else if (cnt == CNT_MAX) //0.5s后左移
|
||||
led_reg <= led_reg << 1;
|
||||
else
|
||||
led_reg <= led_reg;
|
||||
end
|
||||
//led
|
||||
assign led = led_reg;
|
||||
endmodule
|
||||
```
|
||||
|
||||
### 1.3.4仿真验证
|
||||
|
||||
为上述模块编写仿真模块,参考代码(waterled_top_tb.v)如下:
|
||||
|
||||
```verilog
|
||||
`timescale 1ns/1ns
|
||||
module waterled_top_tb;
|
||||
|
||||
reg sysclk;
|
||||
reg rstn;
|
||||
wire [7:0] led;
|
||||
|
||||
// 实例化待测试模块
|
||||
waterled_top #(
|
||||
.CNT_MAX(32'd100)//为了加快仿真速度,将模块内部CNT_MAX由13_499_999变为1000
|
||||
)uut (
|
||||
.sysclk(sysclk),
|
||||
.rstn(rstn),
|
||||
.led(led)
|
||||
);
|
||||
// 产生系统时钟:周期约为 27Mhz
|
||||
initial begin
|
||||
sysclk = 0;
|
||||
forever #(500/27) sysclk = ~sysclk;
|
||||
end
|
||||
// 初始化和复位过程
|
||||
initial begin
|
||||
// 初始化
|
||||
rstn = 0;
|
||||
#100; // 保持复位100ns
|
||||
rstn = 1; // 释放复位
|
||||
end
|
||||
endmodule
|
||||
```
|
||||
|
||||
为了加速仿真,我们在仿真文件中另CNT_MAX的值为100。同时为了便于仿真,可以直接点击sim文件夹下hebav文件夹中的do.bat文件即可利用ModuleSim对模块进行仿真,仿真波形如下:
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/3.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:70%"/>
|
||||
<br> <!--换行-->
|
||||
图3.流水灯仿真波形(一) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/4.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:70%"/>
|
||||
<br> <!--换行-->
|
||||
图4.流水灯仿真波形(二) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
从图3我们可以看到,端口信号led的值经过一定时间之后就进行了左移,并且在图4中我们也可以发现,当cnt的值等于CNT_MAX的时候led进行左移,与我们设计的目标相符合,可以进行下一步上板验证了。
|
||||
|
||||
### 1.3.5上板验证
|
||||
|
||||
仿真已经通过,可以进行上板验证,上板前要先进行管脚约束。端口与对应管脚如下表所示:
|
||||
| 端口名称 |信号类型| 对应管脚|功能
|
||||
|:----:|:----:|:----:|:----:|
|
||||
| sysclk | Input | | 时钟 |
|
||||
| rstn | Input | | 复位 |
|
||||
| led[0] | Output | | LED |
|
||||
| led[1] | Output | | LED |
|
||||
| led[2] | Output | | LED |
|
||||
| led[3] | Output | | LED |
|
||||
| led[4] | Output | | LED |
|
||||
| led[5] | Output | | LED |
|
||||
| led[6] | Output | | LED |
|
||||
| led[7] | Output | | LED |
|
||||
|
||||
管脚分配可以直接编写.fdc文件,也可以使用PDS内置的工具进行分配。
|
||||
|
||||
完成管脚分配之后就可以生成sbit文件,将文件提交到网站后点击烧录,即可将sbit下载到实验板中,在摄像头页面即可观察到流水灯的现象。
|
||||
|
||||
## 1.4 章末总结
|
||||
|
||||
本次实验主要学习使用左移(<<)和右移(>>)运算符实现移位,但实际应用中也可以使用位拼接({})进行更加复杂的移位操作,各位同学可以尝试学习。
|
||||
BIN
public/doc/01/images/1.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/doc/01/images/2.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
public/doc/01/images/3.png
Normal file
|
After Width: | Height: | Size: 620 KiB |
BIN
public/doc/01/images/4.png
Normal file
|
After Width: | Height: | Size: 635 KiB |
BIN
public/doc/02/cover.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
258
public/doc/02/doc.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# 基础-2-按键检测与消抖
|
||||
|
||||
## 2.1 章节导读
|
||||
|
||||
在数字电路中,按键是最常用的人机交互输入方式。然而,机械式按键在按下或释放过程中会产生抖动信号,直接读取会引起误触发。本章我们将实现一个可靠的按键检测模块,完成信号的消抖和下降沿检测,以便为更复杂的模块如状态机切换、模式转换等提供稳定的触发信号。
|
||||
|
||||
## 2.2 理论学习
|
||||
|
||||
由于机械结构的限制,按键在触发的一瞬间,其接触点会发生数次抖动,导致输出信号在0和1之间反复跳变。这种现象称为“抖动”。为避免系统错误响应,需要对按键信号进行“消抖”处理。
|
||||
|
||||
常见的软件消抖方法包括定时器延时,而在软件中通常使用计数器。在本实验中,采用对输入信号进行采样判断,当其状态发生变化时开始计数,若持续稳定一定时长后,才认为按键真正改变。
|
||||
|
||||
在此基础上,若需检测按键的“按下事件”,则还需进一步提取其上升沿(或下降沿)作为一个单周期的“有效触发”信号。
|
||||
|
||||
## 2.3 实战演练
|
||||
|
||||
### 2.3.1 实验目标
|
||||
|
||||
实现一个具有消抖功能的按键检测模块,并进一步提取其下降沿触发信号,输出一个单时钟周期宽度的 `btn_flag` 信号,用于后级逻辑判断。同时为了使实验现象更加明显,设置8位的IO输出连接led,当检测到 `btn_flag` 信号后8位信号`led`会自加1。
|
||||
|
||||
### 2.3.2 硬件资源
|
||||
|
||||
本实验使用试验箱上普通按键输入资源,输入信号经过电平转换后进入 FPGA 芯片,输出信号可连接状态指示灯以观察效果。
|
||||
|
||||
根据原理图可知实验板的按键按下是低电平,不按为高电平。
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/1.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:30%"/>
|
||||
<br> <!--换行-->
|
||||
图1.实验板的按键资源 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/2.png"
|
||||
alt="实验板按键原理图"
|
||||
style="zoom:60%"/>
|
||||
<br> <!--换行-->
|
||||
图2.实验板按键原理图 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/xx.png"
|
||||
alt="数字孪生"
|
||||
style="zoom:60%"/>
|
||||
<br> <!--换行-->
|
||||
图3.远程实验界面按键 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
### 2.3.3 程序设计
|
||||
|
||||
为了实现稳定的按键检测逻辑,设计流程如下:
|
||||
|
||||
1. 对输入 `btn` 进行采样,形成 `btn_temp`;
|
||||
2. 若检测到 `btn_temp` 与当前 `btn` 状态不一致,则开始计数;
|
||||
3. 若计数器 `cnt` 达到设定阈值(如255),则认为按键状态稳定,更新 `btn_ggle`;
|
||||
4. 实验板的按键按下是低电平,不按为高电平。所以对 `btn_ggle` 打两拍形成 `btn_flag_d0` 和 `btn_flag_d1`,再判断其下降沿,输出一个时钟周期的`btn_flag`;
|
||||
5. 检测到信号`btn_flag`后,信号`led <= led + 1`。
|
||||
|
||||
该模块的参考代码如下(`btn_ggle.v`):
|
||||
|
||||
```verilog
|
||||
module btn_ggle(
|
||||
input wire clk,
|
||||
input wire rstn,
|
||||
input wire btn,
|
||||
output wire btn_flag,
|
||||
output reg [7:0] led
|
||||
);
|
||||
reg btn_ggle;
|
||||
reg btn_flag_d0,btn_flag_d1;
|
||||
reg [7:0] cnt;
|
||||
reg btn_temp;
|
||||
//检测按键状态
|
||||
always @(posedge clk) btn_temp <= btn;
|
||||
//按键状态改变时开始计数
|
||||
always @(posedge clk) begin
|
||||
if(~rstn) cnt <= 0;
|
||||
else if(btn_temp != btn) cnt <= 1;
|
||||
else if(cnt != 0) cnt <= cnt + 1;
|
||||
else cnt <= 0;
|
||||
end
|
||||
//计数到255时认为按键值稳定
|
||||
always @(posedge clk) begin
|
||||
if(~rstn) btn_ggle <= btn;
|
||||
else if(cnt == 8'hFF) btn_ggle <= btn_temp;
|
||||
else btn_ggle <= btn_ggle;
|
||||
end
|
||||
//对btn_ggle信号延迟打拍
|
||||
always @(posedge clk) begin
|
||||
if(~rstn) begin
|
||||
btn_flag_d0 <= 0;
|
||||
btn_flag_d1 <= 0;
|
||||
end
|
||||
else begin
|
||||
btn_flag_d0 <= btn_ggle;
|
||||
btn_flag_d1 <= btn_flag_d0;
|
||||
end
|
||||
end
|
||||
//btn_flag检测btn_ggle的下降沿
|
||||
assign btn_flag = ~btn_flag_d0 && btn_flag_d1;
|
||||
//检测到按键按下的标志位(btn_flag),led会加1
|
||||
always @(posedge clk) begin
|
||||
if(~rstn) led <= 0;
|
||||
else if(btn_flag) led <= led + 1;
|
||||
else led <= led;
|
||||
end
|
||||
endmodule
|
||||
```
|
||||
|
||||
### 2.3.4 仿真验证
|
||||
|
||||
为验证功能的正确性,设计测试平台(`btn_ggle_tb.v`),代码如下:
|
||||
|
||||
```verilog
|
||||
`timescale 1ns/1ns
|
||||
module btn_ggle_tb;
|
||||
|
||||
reg clk;
|
||||
reg rstn;
|
||||
reg btn;
|
||||
wire btn_flag;
|
||||
wire [7:0] led;
|
||||
btn_ggle btn_ggle_inst (
|
||||
.clk(clk),
|
||||
.rstn(rstn),
|
||||
.btn(btn),
|
||||
.btn_flag(btn_flag),
|
||||
.led(led)
|
||||
);
|
||||
|
||||
// 27MHz 时钟周期约为 37.037ns,取37ns近似
|
||||
always #(500/27) clk = ~clk; // 半周期18.5ns ≈ 27MHz
|
||||
|
||||
initial begin
|
||||
// 初始化
|
||||
clk = 0;
|
||||
rstn = 0;
|
||||
btn = 1; // 按键默认未按下,高电平有效
|
||||
|
||||
// 释放复位
|
||||
#200;
|
||||
rstn = 1;
|
||||
|
||||
// 模拟带抖动的按下过程
|
||||
#1000 btn = 0;
|
||||
#100 btn = 1; // 抖动
|
||||
#100 btn = 0;
|
||||
#100 btn = 1;
|
||||
#100 btn = 0;
|
||||
// 稳定按下
|
||||
#100000 btn = 0;
|
||||
|
||||
// 模拟抖动松开过程
|
||||
#300000 btn = 1;
|
||||
#100 btn = 0;
|
||||
#100 btn = 1;
|
||||
#100 btn = 0;
|
||||
#100 btn = 1;
|
||||
// 稳定松开
|
||||
#100000 btn = 1;
|
||||
|
||||
// 第二次按下
|
||||
#300000 btn = 0;
|
||||
#100000 btn = 0;
|
||||
|
||||
#300000 $finish;
|
||||
end
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
利用ModuleSim进行仿真,部分仿真波形如下图所示:
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/3.png"
|
||||
alt="仿真波形(一)"
|
||||
style="zoom:60%"/>
|
||||
<br> <!--换行-->
|
||||
图4.仿真波形(一) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/4.png"
|
||||
alt="仿真波形(二)"
|
||||
style="zoom:60%"/>
|
||||
<br> <!--换行-->
|
||||
图5.仿真波形(二) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/5.png"
|
||||
alt="仿真波形(三)"
|
||||
style="zoom:60%"/>
|
||||
<br> <!--换行-->
|
||||
图6.仿真波形(三) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
从仿真波形二和三中,我们可以看到,当我们模拟按键按下(1 ----> 0),当按键抖动(`btn`在0和1之间来回跳转)时,`cnt`的值会变回1重新开始计数,直到按键稳定按下(`btn`的值稳定不变,为0),`cnt`稳定增加,当`cnt`的值增加到`8‘hFF`时,认为按键按下,`btn_ggle`存储此时的按键状态,同时`btn_flag`检测到下降沿,拉高一个时钟周期。`led`信号也加一。
|
||||
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/6.png"
|
||||
alt="仿真波形(四)"
|
||||
style="zoom:60%"/>
|
||||
<br> <!--换行-->
|
||||
图7.仿真波形(四) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/7.png"
|
||||
alt="仿真波形(五)"
|
||||
style="zoom:60%"/>
|
||||
<br> <!--换行-->
|
||||
图8.仿真波形(五) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
|
||||
从波形三和四中,我们可以看到,当模拟按键抬起时(0 ----> 1),按键的抖动也会使`cnt`重新计数,直到稳定,`cnt`计数到`8’hFF`时,更新`btn_ggle`,由于按键是抬起,`btn_flag`不变,`led`不变。
|
||||
|
||||
### 2.3.5 上板验证
|
||||
|
||||
完成仿真后,可进行上板验证。端口连接如下表所示:
|
||||
|
||||
| 端口名称 | 类型 | 管脚 |说明 |
|
||||
| -------- | ------ | ------ | ---------- |
|
||||
| clk | Input | | 27MHz 时钟 |
|
||||
| rstn | Input | | 低电平复位 |
|
||||
| btn | Input | | 外部按钮 |
|
||||
| btn_flag | Output | | 上升沿标志 |
|
||||
| led[0] | Output | | 驱动led |
|
||||
| led[1] | Output | | 驱动led |
|
||||
| led[2] | Output | | 驱动led |
|
||||
| led[3] | Output | | 驱动led |
|
||||
| led[4] | Output | | 驱动led |
|
||||
| led[5] | Output | | 驱动led |
|
||||
| led[6] | Output | | 驱动led |
|
||||
| led[7] | Output | | 驱动led |
|
||||
|
||||
将`.sbit`文件上传至平台,并下载到实验板,多次按下按键,观察led灯跳转,如果按下1次按键led只跳转一次,那么说明达成实验目标。
|
||||
|
||||
## 2.4 章末总结
|
||||
|
||||
本实验通过一个典型的按键检测例子,介绍了数字系统中常用的消抖和边沿检测方法,掌握了如何利用计数器和触发器组合进行抖动抑制与事件捕捉。在更复杂的设计中,这类基础模块可作为控制逻辑的可靠触发信号源,具有广泛应用价值。
|
||||
BIN
public/doc/02/images/1.png
Normal file
|
After Width: | Height: | Size: 821 KiB |
BIN
public/doc/02/images/2.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
BIN
public/doc/02/images/3.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/doc/02/images/4.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/doc/02/images/5.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/doc/02/images/6.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/doc/02/images/7.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/doc/03/cover.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
244
public/doc/03/doc.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# 基础-3-数码管实验
|
||||
|
||||
---
|
||||
|
||||
在许多项目设计中,我们通常需要一些显示设备来显示我们需要的信息,可以选择的显示设备有很多,而数码管是使用最多,最简单的显示设备之一。
|
||||
|
||||
## 3.1 章节导读
|
||||
本章将通过数码管驱动实验讲解FPGA数字系统中重要的"选通控制"概念。读者将学习到:
|
||||
1. 数码管工作原理与动态扫描技术
|
||||
2. 多路复用(Multiplexing)设计方法
|
||||
3. 参数化模块设计技巧
|
||||
4. 外设驱动时序规划
|
||||
5. ASCII到段码的转换原理
|
||||
|
||||
实验将使用Verilog HDL实现一个支持8位数码管显示、包含字符动态滚动和选通控制的完整系统。
|
||||
|
||||
|
||||
## 3.2 理论学习
|
||||
### 3.2.1 数码管结构
|
||||
- 7段数码管组成:A-G段+DP小数点
|
||||
- 共阳/共阴类型区分(本实验采用共阳型,低电平有效)
|
||||
|
||||
### 3.2.2 动态扫描原理
|
||||
```
|
||||
显示周期 = 刷新周期 × 数码管数量
|
||||
人眼视觉暂留效应(>60Hz)
|
||||
扫描频率计算公式:f_scan = f_clk / CLK_CYCLE
|
||||
```
|
||||
|
||||
### 3.2.3 关键技术
|
||||
- 时分复用:分时选通数码管
|
||||
- 段码生成:ASCII字符到七段码转换
|
||||
- 消隐处理:消除切换时的视觉残留
|
||||
|
||||
### 3.2.4 设计指标
|
||||
| 参数 | 值 | 说明 |
|
||||
|-------|-----|-------------------|
|
||||
| 位数 | 8 | 数码管数量 |
|
||||
| 频率 | 200Hz | 单管刷新频率 |
|
||||
| 分辨率 | 8bit | 段码控制(含小数点)|
|
||||
|
||||
|
||||
## 3.2 实战演练
|
||||
### 3.3.1 系统架构
|
||||
```verilog
|
||||
系统框图:
|
||||
[Top模块] → [显示驱动模块] → [选通控制模块]
|
||||
↖ ASCII数据生成 ↙ 时钟分频
|
||||
```
|
||||
|
||||
### 3.3.2 模块设计
|
||||
#### led_display_selector
|
||||
```verilog
|
||||
module led_display_selector #(
|
||||
parameter NUM = 4,
|
||||
parameter VALID_SIGNAL = 1'b0,
|
||||
parameter CLK_CYCLE = 1000
|
||||
)(
|
||||
input wire clk,
|
||||
input wire rstn,
|
||||
input wire [NUM*8-1:0] led_in,
|
||||
output reg [7:0] led_display_seg,//[DP,G,F,E,D,C,B,A]
|
||||
output reg [NUM-1:0] led_display_sel
|
||||
);
|
||||
|
||||
reg [31:0] clk_cnt;
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if (!rstn) clk_cnt <= 0;
|
||||
else if(clk_cnt == CLK_CYCLE) clk_cnt <= 0;
|
||||
else clk_cnt <= clk_cnt + 1;
|
||||
end
|
||||
|
||||
wire seg_change = (clk_cnt == CLK_CYCLE) ? 1'b1 : 1'b0;
|
||||
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(!rstn) led_display_sel <= {{(NUM-1){~VALID_SIGNAL}}, VALID_SIGNAL};
|
||||
else if (seg_change) led_display_sel <= {led_display_sel[NUM-2:0], led_display_sel[NUM-1]};
|
||||
else led_display_sel <= led_display_sel;
|
||||
end
|
||||
|
||||
integer i;
|
||||
always @(*) begin
|
||||
for(i=0;i<NUM;i=i+1) begin
|
||||
if(led_display_sel[NUM-1-i] == VALID_SIGNAL)
|
||||
led_display_seg = led_in[i*8 +: 8] ^ ({8{~VALID_SIGNAL}});
|
||||
end
|
||||
end
|
||||
|
||||
endmodule //led_display_ctrl
|
||||
```
|
||||
|
||||
#### led_display_driver
|
||||
```verilog
|
||||
module led_display_driver(// 8个数码管显示,阳极管(在selector中已经做了阴阳处理)
|
||||
input wire clk,
|
||||
input wire rstn,
|
||||
input wire [8*8-1:0] assic_seg, //ASSIC coding
|
||||
input wire [7:0] seg_point, //显示小数点
|
||||
|
||||
output wire [7:0] led_display_seg,
|
||||
output wire [7:0] led_display_sel
|
||||
);
|
||||
|
||||
reg [8*8-1:0] led_in;
|
||||
|
||||
integer i;
|
||||
always @(*) begin
|
||||
led_in = 0;
|
||||
for(i=0;i<8;i=i+1) begin //led_in[i*8 +: 8] <---> assic_seg[i*8 +: 8]
|
||||
case (assic_seg[i*8 +: 8])
|
||||
"0": led_in[i*8 +: 8] = (8'h3f) | {seg_point[i],7'b0};
|
||||
"1": led_in[i*8 +: 8] = (8'h06) | {seg_point[i],7'b0};
|
||||
"2": led_in[i*8 +: 8] = (8'h5b) | {seg_point[i],7'b0};
|
||||
"3": led_in[i*8 +: 8] = (8'h4f) | {seg_point[i],7'b0};
|
||||
"4": led_in[i*8 +: 8] = (8'h66) | {seg_point[i],7'b0};
|
||||
"5": led_in[i*8 +: 8] = (8'h6d) | {seg_point[i],7'b0};
|
||||
"6": led_in[i*8 +: 8] = (8'h7d) | {seg_point[i],7'b0};
|
||||
"7": led_in[i*8 +: 8] = (8'h07) | {seg_point[i],7'b0};
|
||||
"8": led_in[i*8 +: 8] = (8'h7f) | {seg_point[i],7'b0};
|
||||
"9": led_in[i*8 +: 8] = (8'h6f) | {seg_point[i],7'b0};
|
||||
"A","a": led_in[i*8 +: 8] = (8'h77) | {seg_point[i],7'b0};
|
||||
"B","b": led_in[i*8 +: 8] = (8'h7c) | {seg_point[i],7'b0};
|
||||
"C","c": led_in[i*8 +: 8] = (8'h39) | {seg_point[i],7'b0};
|
||||
"D","d": led_in[i*8 +: 8] = (8'h5e) | {seg_point[i],7'b0};
|
||||
"E","e": led_in[i*8 +: 8] = (8'h79) | {seg_point[i],7'b0};
|
||||
"F","f": led_in[i*8 +: 8] = (8'h71) | {seg_point[i],7'b0};
|
||||
"G","g": led_in[i*8 +: 8] = (8'h3d) | {seg_point[i],7'b0};
|
||||
"H","h": led_in[i*8 +: 8] = (8'h76) | {seg_point[i],7'b0};
|
||||
"I","i": led_in[i*8 +: 8] = (8'h0f) | {seg_point[i],7'b0};
|
||||
"J","j": led_in[i*8 +: 8] = (8'h0e) | {seg_point[i],7'b0};
|
||||
"K","k": led_in[i*8 +: 8] = (8'h75) | {seg_point[i],7'b0};
|
||||
"L","l": led_in[i*8 +: 8] = (8'h38) | {seg_point[i],7'b0};
|
||||
"M","m": led_in[i*8 +: 8] = (8'h37) | {seg_point[i],7'b0};
|
||||
"N","n": led_in[i*8 +: 8] = (8'h54) | {seg_point[i],7'b0};
|
||||
"O","o": led_in[i*8 +: 8] = (8'h5c) | {seg_point[i],7'b0};
|
||||
"P","p": led_in[i*8 +: 8] = (8'h73) | {seg_point[i],7'b0};
|
||||
"Q","q": led_in[i*8 +: 8] = (8'h67) | {seg_point[i],7'b0};
|
||||
"R","r": led_in[i*8 +: 8] = (8'h31) | {seg_point[i],7'b0};
|
||||
"S","s": led_in[i*8 +: 8] = (8'h49) | {seg_point[i],7'b0};
|
||||
"T","t": led_in[i*8 +: 8] = (8'h78) | {seg_point[i],7'b0};
|
||||
"U","u": led_in[i*8 +: 8] = (8'h3e) | {seg_point[i],7'b0};
|
||||
"V","v": led_in[i*8 +: 8] = (8'h1c) | {seg_point[i],7'b0};
|
||||
"W","w": led_in[i*8 +: 8] = (8'h7e) | {seg_point[i],7'b0};
|
||||
"X","x": led_in[i*8 +: 8] = (8'h64) | {seg_point[i],7'b0};
|
||||
"Y","y": led_in[i*8 +: 8] = (8'h6e) | {seg_point[i],7'b0};
|
||||
"Z","z": led_in[i*8 +: 8] = (8'h59) | {seg_point[i],7'b0};
|
||||
" ": led_in[i*8 +: 8] = (8'h00) | {seg_point[i],7'b0};
|
||||
"-": led_in[i*8 +: 8] = (8'h40) | {seg_point[i],7'b0};
|
||||
"_": led_in[i*8 +: 8] = (8'h08) | {seg_point[i],7'b0};
|
||||
"=": led_in[i*8 +: 8] = (8'h48) | {seg_point[i],7'b0};
|
||||
"+": led_in[i*8 +: 8] = (8'h5c) | {seg_point[i],7'b0};
|
||||
"(": led_in[i*8 +: 8] = (8'h39) | {seg_point[i],7'b0};
|
||||
")": led_in[i*8 +: 8] = (8'h0F) | {seg_point[i],7'b0};
|
||||
default: led_in[i*8 +: 8] = (8'h00) | {seg_point[i],7'b0};
|
||||
endcase
|
||||
end
|
||||
end
|
||||
|
||||
led_display_selector #(
|
||||
.NUM ( 8 ),
|
||||
.VALID_SIGNAL ( 1'b0 ), //阳极管,低电平亮
|
||||
.CLK_CYCLE ( 5000 ))
|
||||
u_led_display_selector(
|
||||
.clk ( clk ),
|
||||
.rstn ( rstn ),
|
||||
.led_in ( led_in ),
|
||||
.led_display_seg ( led_display_seg ),
|
||||
.led_display_sel ( led_display_sel )
|
||||
);
|
||||
|
||||
endmodule //moduleName
|
||||
```
|
||||
|
||||
#### led_display_top
|
||||
```verilog
|
||||
|
||||
module led_diaplay_top(
|
||||
//system io
|
||||
input wire external_clk ,
|
||||
input wire external_rstn,
|
||||
//led display io
|
||||
output wire [7:0] led_display_seg,
|
||||
output wire [7:0] led_display_sel
|
||||
);
|
||||
|
||||
reg [43*8-1:0] assic_seg;
|
||||
reg [7:0] seg_point;
|
||||
|
||||
reg [31:0] clk_cnt;
|
||||
always @(posedge external_clk or negedge external_rstn) begin
|
||||
if(!external_rstn) clk_cnt <= 0;
|
||||
else clk_cnt <= clk_cnt + 1;
|
||||
end
|
||||
|
||||
always @(posedge external_clk or negedge external_rstn) begin
|
||||
if(!external_rstn) begin
|
||||
assic_seg <= "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ -_=+()";
|
||||
seg_point <= 8'b00000001;
|
||||
end else if({clk_cnt[24]==1'b1} && (clk_cnt[23:0]==25'b0))begin
|
||||
assic_seg <= {assic_seg[8*43-8-1:0], assic_seg[8*43-1 -: 8]};
|
||||
seg_point <= {seg_point[6:0], seg_point[7]};
|
||||
end else begin
|
||||
assic_seg <= assic_seg;
|
||||
seg_point <= seg_point;
|
||||
end
|
||||
end
|
||||
|
||||
led_display_driver u_led_display_driver(
|
||||
.clk ( external_clk ),
|
||||
.rstn ( external_rstn ),
|
||||
.assic_seg ( assic_seg[8*43-1 -: 8*8] ),
|
||||
.seg_point ( seg_point ),
|
||||
.led_display_seg ( led_display_seg ),
|
||||
.led_display_sel ( led_display_sel )
|
||||
);
|
||||
|
||||
endmodule //led_diaplay_top
|
||||
```
|
||||
|
||||
### 3.3.3 上板验证步骤
|
||||
1. 设置参数:CLK_CYCLE=5000(对应200Hz扫描频率)
|
||||
2. 绑定管脚:连接数码管段选/位选信号
|
||||
3. 观察现象:字符"01234567"应稳定显示
|
||||
4. 修改assic_seg初始值验证滚动功能
|
||||
|
||||
---
|
||||
|
||||
## 3.4 章末总结
|
||||
**关键收获:**
|
||||
1. 掌握动态扫描消除器件闪烁的原理
|
||||
2. 理解参数化设计(NUM/VALID_SIGNAL)的优势
|
||||
3. 学习时序控制中计数器的重要作用
|
||||
4. 实践ASCII到硬件编码的转换方法
|
||||
|
||||
**设计亮点:**
|
||||
- 支持阴阳极自动适配(通过VALID_SIGNAL参数)
|
||||
- 字符环形缓冲区实现无缝滚动
|
||||
- 参数化设计增强模块复用性
|
||||
|
||||
---
|
||||
|
||||
## 3.5 拓展训练
|
||||
|
||||
结合流水灯实验和数码管实验:数码管显示数字,标识出当前流水到了哪一个灯
|
||||
BIN
public/doc/04/cover.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
212
public/doc/04/doc.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# 基础-4-矩阵键盘实验
|
||||
|
||||
## 4.1 章节导读
|
||||
本章将介绍**矩阵键盘检测电路的设计与实现方法**,通过Verilog HDL语言完成4×4矩阵键盘的扫描识别模块,掌握**多键输入设备的行列扫描原理、消抖机制以及按键编码处理方式**。
|
||||
|
||||
矩阵键盘作为常见的人机交互接口之一,广泛应用于嵌入式系统、数字电路和微控制器项目中。与独立按键不同,矩阵键盘在节省IO资源的同时,对扫描逻辑和时序处理提出了更高的要求。实验中我们将采用**逐行扫描法**,结合状态机与延时消抖手段,确保按键信息的准确采集。
|
||||
|
||||
|
||||
## 4.2 理论学习
|
||||
### 4.2.1 矩阵键盘结构
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/1.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:80%"/>
|
||||
<br> <!--换行-->
|
||||
图1.矩阵键盘原理图 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
实验板8个引脚分别连接矩阵键盘的KEY1~KEY8,该矩阵键盘的原理如下:将KEY1~KEY4脚设置为输出引脚,KEY5~KEY8设置为输入引脚。以KEY1和KEY5为例,当没有按键按下时,KEY1和VCC之间是断路,此时R1为上拉电阻,电路几乎没有电流流过,KEY5检测到的电压恰好是VCC,为1。**所以按键不按下,KEY5~KEY8检测到1。**
|
||||
|
||||
如果按键按下。此时KEY5~KEY8检测到的值与KEY1~KEY4的输出电压有关。以KEY1和KEY5为例,如果KEY1输出为0,按键1按下,VCC和KEY1之间形成通路,KEY5检测到0。但如果KEY1输出为1,此时即使按键按下,VCC和KEY1之间也几乎没有电流,此时KEY5检测到高阻态,也就是1。所以,**如果行输出电平为0,并且按键按下,KEY5~8会检测到0;如果行输出电平为1,按键按下,KEY5~8检测到1。**
|
||||
|
||||
现在我们看懂了原理图就可以开始设计verilog,根据原理图我们知道,只有行电平(KEY1~4的输出电平)为0时,按键按下,KEY5~8才会检测到0。那么我们可以用行扫描的逻辑设计:
|
||||
|
||||
1. FPGA按顺序将1到4行中的一行输出为低电平,其余3行为高电平(或高阻态)。
|
||||
2. FPGA逐个读取每列引脚(KEY5~8)的电平,若某列为低电平,则说明该行和该列交汇处的按键被按下。
|
||||
3. 可以在没有按键按下时,把所有行的输出电平都拉低,直到有按键按下时,重复1~2的步骤扫描。
|
||||
|
||||
|
||||
## 4.2 实战演练
|
||||
### 4.3.1 系统架构
|
||||
``` verilog
|
||||
系统框图:
|
||||
[Top模块] = {矩阵键盘扫描模块 → 按键上升沿检测模块}
|
||||
```
|
||||
|
||||
### 4.3.2 模块设计
|
||||
|
||||
根据上述原理,设计行扫描矩阵键盘检测模块如下:
|
||||
|
||||
#### matrix_key
|
||||
```verilog
|
||||
module matrix_key #(
|
||||
parameter ROW_NUM = 4,
|
||||
parameter COL_NUM = 4,
|
||||
parameter DEBOUNCE_TIME = 2000,
|
||||
parameter DELAY_TIME = 200
|
||||
) (
|
||||
input wire clk,
|
||||
input wire rstn,
|
||||
output reg [ROW_NUM-1:0] row,
|
||||
input wire [COL_NUM-1:0] col,
|
||||
output reg [ROW_NUM*COL_NUM-1:0] key_out
|
||||
);
|
||||
|
||||
localparam ROW_ACTIVE = 1'b0; // 行有效电平
|
||||
localparam ROW_INACTIVE = 1'b1; // 行无效电平
|
||||
localparam COL_PRESSED = 1'b0; // 列按下电平
|
||||
localparam COL_RELEASED = 1'b1; // 列释放电平
|
||||
|
||||
reg [ROW_NUM-1:0][COL_NUM-1:0] key; // 按键状态寄存器
|
||||
|
||||
reg [2:0] cu_st, nt_st;
|
||||
localparam [2:0] ST_IDLE = 3'b001;
|
||||
localparam [2:0] ST_SCAN = 3'b010;
|
||||
localparam [2:0] ST_DEBOUNCE = 3'b100;
|
||||
|
||||
wire btn_pressed = ((|(~(col ^ {COL_NUM{COL_PRESSED}}))) && (cu_st == ST_IDLE)) || (key_out != 0); // 只要有一个按键按下,btn_pressed为1
|
||||
reg [31:0] delay_cnt; // 延时计数器
|
||||
reg [31:0] debounce_cnt; // 消抖计数器
|
||||
reg [ROW_NUM-1:0] row_cnt; // 行计数器
|
||||
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(!rstn) delay_cnt <= 0;
|
||||
else if(cu_st == ST_SCAN) begin
|
||||
if(delay_cnt == DELAY_TIME) delay_cnt <= 0;
|
||||
else delay_cnt <= delay_cnt + 1;
|
||||
end else delay_cnt <= 0;
|
||||
end
|
||||
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(!rstn) row_cnt <= 0;
|
||||
else if(cu_st == ST_SCAN) begin
|
||||
if(delay_cnt == DELAY_TIME) row_cnt <= row_cnt + 1;
|
||||
else row_cnt <= row_cnt;
|
||||
end else row_cnt <= 0;
|
||||
end
|
||||
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(!rstn) debounce_cnt <= 0;
|
||||
else if(cu_st == ST_DEBOUNCE) begin
|
||||
if(debounce_cnt == DEBOUNCE_TIME) debounce_cnt <= 0;
|
||||
else debounce_cnt <= debounce_cnt + 1;
|
||||
end else debounce_cnt <= 0;
|
||||
end
|
||||
|
||||
/*
|
||||
处理逻辑
|
||||
ROW作为输出,COL作为输入
|
||||
1. ST_IDLE状态,所有ROW都拉至有效电平
|
||||
2. 若没有按键按下,所有COL都为释放电平
|
||||
3. 若有按键按下,按下的按键所在的COL会变为按下电平
|
||||
4. 进入ST_SCAN状态,启动扫描,ROW全部置为无效电平,并逐次改变为有效电平。(此时,COL会都变成列释放电平)
|
||||
5. 如果某一个ROW行有效电平时,COL变成了列按下电平,则说明该ROW和COL交点的按键被按下
|
||||
6. 每一行都扫描一遍。
|
||||
7. 进入ST_DEBOUNCE状态,所有ROW都拉至行有效电平,在此期间不进行扫描。
|
||||
8. DEBOUNCE时间到后,进入IDLE状态。
|
||||
*/
|
||||
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(!rstn) cu_st <= ST_IDLE;
|
||||
else cu_st <= nt_st;
|
||||
end
|
||||
|
||||
always @(*) begin
|
||||
if(!rstn) nt_st <= ST_IDLE;
|
||||
else case(cu_st)
|
||||
ST_IDLE: begin
|
||||
if(btn_pressed) nt_st <= ST_SCAN;
|
||||
else nt_st <= ST_IDLE;
|
||||
end
|
||||
ST_SCAN: begin
|
||||
if((delay_cnt == DELAY_TIME) && (row_cnt == ROW_NUM-1)) nt_st <= ST_DEBOUNCE;
|
||||
else nt_st <= ST_SCAN;
|
||||
end
|
||||
ST_DEBOUNCE: begin
|
||||
if(debounce_cnt == DEBOUNCE_TIME) nt_st <= ST_IDLE;
|
||||
else nt_st <= ST_DEBOUNCE;
|
||||
end
|
||||
default: nt_st <= ST_IDLE;
|
||||
endcase
|
||||
end
|
||||
|
||||
integer i, j;
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(!rstn) key <= 0;
|
||||
else for(i=0; i<ROW_NUM; i=i+1)
|
||||
for(j=0; j<COL_NUM; j=j+1)
|
||||
if((cu_st == ST_SCAN) && (delay_cnt == DELAY_TIME) && (row_cnt == i)) key[i][j] <= (col[j] == COL_PRESSED)?(1'b1):(1'b0);
|
||||
else key[i][j] <= key[i][j]; // 其他情况不变
|
||||
end
|
||||
|
||||
always @(*) begin
|
||||
for(i=0;i<ROW_NUM;i=i+1) begin
|
||||
for(j=0;j<COL_NUM;j=j+1) begin
|
||||
key_out[i*COL_NUM+j] <= key[i][j];
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(!rstn) row <= {ROW_NUM{ROW_ACTIVE}};
|
||||
else if(cu_st == ST_IDLE && nt_st == ST_SCAN) row <= {{(ROW_NUM-1){ROW_INACTIVE}}, ROW_ACTIVE};
|
||||
else if(cu_st == ST_SCAN) begin
|
||||
if(delay_cnt == DELAY_TIME) row <= {row[ROW_NUM-1:0],ROW_INACTIVE};
|
||||
else row <= row;
|
||||
end else row <= {ROW_NUM{ROW_ACTIVE}};
|
||||
end
|
||||
endmodule //matrix_key
|
||||
```
|
||||
|
||||
为了能够观察到现象,使用板载8个led和实验箱8个led进行显示,按下矩阵键盘的按键,对应led就会亮,顶层文件如下所示:
|
||||
|
||||
#### matrix_key_top
|
||||
|
||||
```verilog
|
||||
module matrix_key_top(
|
||||
//system io
|
||||
input wire external_clk ,
|
||||
input wire external_rstn,
|
||||
|
||||
input wire [ 3:0] col,
|
||||
output wire [ 3:0] row,
|
||||
output wire [15:0] led
|
||||
);
|
||||
|
||||
wire [15:0] key_out;
|
||||
|
||||
assign led = key_out;
|
||||
matrix_key #(
|
||||
.ROW_NUM ( 4 ),
|
||||
.COL_NUM ( 4 ),
|
||||
.DEBOUNCE_TIME ( 10000 ),
|
||||
.DELAY_TIME ( 2000 ))
|
||||
u_matrix_key(
|
||||
.clk ( external_clk ),
|
||||
.rstn ( external_rstn ),
|
||||
.row ( row ),
|
||||
.col ( col ),
|
||||
.key_out ( key_out )
|
||||
);
|
||||
endmodule
|
||||
```
|
||||
|
||||
### 4.3.3 上板验证步骤
|
||||
1. 设置参数:CLK_CYCLE=5000(对应200Hz扫描频率)
|
||||
2. 绑定管脚:连接led和矩阵键盘管脚
|
||||
|
||||
---
|
||||
|
||||
## 4.4 章末总结
|
||||
**关键收获:**
|
||||
1. 掌握矩阵键盘行扫描原理,能看懂原理图
|
||||
3. 学习时序控制中计数器的重要作用
|
||||
|
||||
---
|
||||
|
||||
## 4.5 拓展训练
|
||||
|
||||
可以将数码管与矩阵键盘相结合
|
||||
BIN
public/doc/04/images/1.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
public/doc/05/cover.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
194
public/doc/05/doc.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# 基础-5-PWM呼吸灯
|
||||
|
||||
## 5.1 章节导读
|
||||
|
||||
本章将实现 PWM(脉宽调制)呼吸灯效果,即控制 LED 灯的亮度在一个周期内从暗到亮再从亮到暗,形成如人呼吸般的灯光变化。通过该实验可以掌握 PWM 占空比调节以及 FPGA 控制 LED 的基本方法。
|
||||
|
||||
## 5.2 理论学习
|
||||
|
||||
呼吸灯在我们的生活中很常见,在电脑上多作为消息提醒指示灯而被广泛使用,其效果是小灯在一段时间内从完全熄灭的状态逐渐变到最亮,再在同样的时间段内逐渐达到完全熄灭的状态,并循环往复。这种效果就像“呼吸”一样。而实现”呼吸“的方法就是PWM技术。
|
||||
|
||||
PWM(Pulse Width Modulation)是一种常用的控制技术,其核心思想是通过控制一个周期内信号为高电平的时间比例(占空比)来实现输出电压或亮度的变化。也就是说只要我们在小时间段内,led灯的亮度依次增加,然后依次减小,即可实现”呼吸“的效果。
|
||||
|
||||
|
||||
|
||||
## 5.3 实战演练
|
||||
|
||||
### 5.3.1 实验目标
|
||||
|
||||
实现 LED 呼吸灯效果,亮度逐渐变亮再逐渐变暗,周而复始,整体周期约为2秒,视觉上更加自然流畅。
|
||||
|
||||
### 5.3.2 硬件资源
|
||||
|
||||
实验板提供 32 颗 LED 灯,本实验选用其中的 1 颗绿色 LED 进行 PWM 控制
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/1.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:30%"/>
|
||||
<br> <!--换行-->
|
||||
图1.LED扩展板 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
通过原理图可以得知,本试验箱的LED灯为高电平时点亮。
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/2.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:40%"/>
|
||||
<br> <!--换行-->
|
||||
图2.LED扩展板原理图 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
### 5.3.3 程序设计
|
||||
|
||||
本模块的设计事实上是两个计数器,所以肯定需要时钟信号sysclk,也需要一个rstn复位信号,同时需要一个IO口驱动LED。所以模块的端口如下表所示:
|
||||
|
||||
| 端口名称 | 端口位宽 | 端口类型 |功能描述|
|
||||
|:----------:|:----:|:----:|:--------------------:|
|
||||
| sysclk | 1Bit | Input | 输入时钟,频率27M |
|
||||
| rstn | 1Bit | Input | 复位信号,低电平有效 |
|
||||
| led | 1Bit | Output | LED控制信号 |
|
||||
|
||||
为了实现一个视觉上柔和自然的 LED 呼吸效果,我们设定完整的呼吸周期为 2 秒,即 LED 亮度在 1 秒内逐渐增强,接着在另 1 秒内逐渐减弱。整个过程由占空比(duty cycle)的变化来控制 PWM 输出的高电平持续时间。
|
||||
|
||||
在本设计中,使用实验板的 27MHz 系统时钟。为了获得合适的 PWM 控制精度,我们将一个 PWM 周期设定为 1ms,这对应 27000 个时钟周期(27M ÷ 1000)。通过一个名为 `pwm_cnt` 的计数器来实现这一周期性计数,当 `pwm_cnt` 小于占空比 `duty` 的值时,LED 输出高电平,从而控制亮度。
|
||||
|
||||
为了实现“呼吸”变化,我们再设计另一个计数器 `duty`,它每 1ms(即 `pwm_cnt` 计满一次)更新一次。前 1000ms 内占空比逐渐增加,即 `duty` 每次增加,从而输出高电平的时间逐步变长,LED 亮度逐渐增强;后 1000ms 内占空比逐渐减小,每次减小,LED 亮度逐渐变弱。如此循环往复,即可实现 LED 的“柔和呼吸”效果。
|
||||
|
||||
那么,占空比 duty 的变化步长如何选择?考虑到:一个 1ms是 27000 个时钟;如果我们希望1ms内led亮的时间为1us的倍数,那么我们可以将27000分成1000份,一份是27。如果duty的每次增减是27,那么也就对应了led每次亮灭的时间增减了1us。也就是说当duty为27时,led亮的时间为1us,1ms过后,duty变为54,led亮的时间为2us,以此类推,当duty为27000时,led亮满1ms。这样就实现了led亮的时间逐渐增加的效果。
|
||||
|
||||
模块的参考代码如下所示(`pwm.v`):
|
||||
|
||||
```verilog
|
||||
module pwm(
|
||||
input wire sysclk, // 27MHz 系统时钟
|
||||
input wire rstn, // 低有效复位
|
||||
output wire led // PWM 控制LED输出
|
||||
);
|
||||
|
||||
parameter PWM_PERIOD = 16'd27000;//1ms
|
||||
// 单一PWM周期,1ms
|
||||
// duty上升的次数是1000次,下降的次数也是1000次,说明pwm的半周期是 1ms * 1000 = 1s
|
||||
// pwm的一次全周期是 1s * 2 = 2s
|
||||
reg [15:0] pwm_cnt;
|
||||
reg [15:0] duty;
|
||||
reg inc_dec_flag;//0表示duty+ ,1表示duty-
|
||||
//计数器1,不断累加
|
||||
always @(posedge sysclk or negedge rstn) begin
|
||||
if (!rstn)
|
||||
pwm_cnt <= 0;
|
||||
else if (pwm_cnt < PWM_PERIOD - 1)
|
||||
pwm_cnt <= pwm_cnt + 1;
|
||||
else
|
||||
pwm_cnt <= 0;
|
||||
end
|
||||
//计数器2,控制占空比,单一周期结束进行一次累加或者减
|
||||
always @(posedge sysclk or negedge rstn) begin
|
||||
if (!rstn)
|
||||
duty <= 0;
|
||||
else if (pwm_cnt == PWM_PERIOD - 1)begin
|
||||
if(inc_dec_flag == 0)
|
||||
duty <= duty + 27;
|
||||
else
|
||||
duty <= duty - 27;
|
||||
end
|
||||
else duty <= duty;
|
||||
end
|
||||
//加减的标志位,半周期结束后反转。
|
||||
always @(posedge sysclk or negedge rstn) begin
|
||||
if(~rstn)
|
||||
inc_dec_flag <= 0;
|
||||
else if(duty == PWM_PERIOD)
|
||||
inc_dec_flag <= 1;
|
||||
else if(duty == 0)
|
||||
inc_dec_flag <= 0;
|
||||
else
|
||||
inc_dec_flag <= inc_dec_flag;
|
||||
end
|
||||
|
||||
assign led = (pwm_cnt < duty) ? 1'b1 : 1'b0;
|
||||
endmodule
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 5.3.4 仿真验证
|
||||
|
||||
为了验证模块功能,我们可以编写仿真模块,并将 `PWM_PERIOD` 等比例缩小为270,以便快速验证。以下为仿真文件(`pwm_tb.v`):
|
||||
|
||||
```verilog
|
||||
`timescale 1ns/1ns
|
||||
module pwm_tb;
|
||||
|
||||
reg sysclk;
|
||||
reg rstn;
|
||||
wire led;
|
||||
|
||||
// 实例化待测试模块
|
||||
pwm #(
|
||||
.PWM_PERIOD(270)//为了减少仿真时间,将单一pwm周期从27000等比例缩小为270
|
||||
) pwm_inst (
|
||||
.sysclk(sysclk),
|
||||
.rstn(rstn),
|
||||
.led(led)
|
||||
);
|
||||
// 产生系统时钟:周期约为 27Mhz
|
||||
initial begin
|
||||
sysclk = 0;
|
||||
forever #(500/27) sysclk = ~sysclk;
|
||||
end
|
||||
|
||||
// 初始化和复位过程
|
||||
initial begin
|
||||
// 初始化
|
||||
rstn = 0;
|
||||
#100; // 保持复位100ns
|
||||
rstn = 1; // 释放复位
|
||||
end
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
同时为了便于仿真,可以直接点击sim文件夹下hebav文件夹中的do.bat文件即可利用ModuleSim对模块进行仿真,仿真波形如下:
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/3.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:60%"/>
|
||||
<br> <!--换行-->
|
||||
图3.呼吸灯仿真波形(一) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/4.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:60%"/>
|
||||
<br> <!--换行-->
|
||||
图4.呼吸灯仿真波形(二) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
通过观察波形我们发现led输出为1的时间在逐步增加之后逐步减小,duty的值从0增加到270后减小,符合设计预期,可以进行下一步上板验证。
|
||||
|
||||
### 5.3.5 上板验证
|
||||
|
||||
仿真验证通过后,即可进行上板测试。在实际使用时需要进行管脚约束。以下为参考端口与分配示例:
|
||||
|
||||
| 端口名称 | 信号类型 | 对应管脚 | 功能 |
|
||||
| -------- | -------- | -------- | ------------------ |
|
||||
| clk | Input | | 27MHz时钟 |
|
||||
| rstn | Input | | 复位 |
|
||||
| led | Output | | 输出PWM信号连接LED |
|
||||
|
||||
完成管脚绑定后生成 `.sbit` 文件,上传到实验平台后进行烧录,即可在摄像头画面中看到 LED 呼吸闪烁效果。
|
||||
|
||||
## 5.4 章末总结
|
||||
|
||||
本章我们学习了 PWM 控制的基本原理及其在 LED 呼吸灯上的应用,同时通过不断改变PWM占空比方式使呼吸过程更加平滑自然。该方法不仅适用于视觉灯效控制,还广泛应用于马达调速、音量控制等模拟量调节领域。你可以进一步尝试调整占空比范围、节奏速度,甚至扩展到多个 LED 同步/异步呼吸控制,实现更加炫酷的视觉效果。
|
||||
BIN
public/doc/05/images/1.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/doc/05/images/2.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
public/doc/05/images/3.png
Normal file
|
After Width: | Height: | Size: 858 KiB |
BIN
public/doc/05/images/4.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/doc/06/cover.png
Normal file
|
After Width: | Height: | Size: 377 KiB |
650
public/doc/06/doc.md
Normal file
@@ -0,0 +1,650 @@
|
||||
# 基础-6-HDMI显示
|
||||
|
||||
## 6.1 章节导读
|
||||
|
||||
随着多媒体技术的快速发展,高清显示已成为嵌入式系统与FPGA应用中不可或缺的一部分。HDMI(High-Definition Multimedia Interface)作为目前最主流的视频数字传输标准,广泛应用于电视、显示器、笔记本、摄像头等各类终端设备中。相比传统的模拟VGA接口,HDMI具有传输带宽高、支持音视频同步、无压缩信号传输等优点,能更好地满足现代图像处理和显示系统的需求。
|
||||
|
||||
在FPGA开发中,掌握HDMI显示技术不仅是实现图像/视频输出的基础能力,更是后续图像识别、视频监控、图形用户界面(GUI)等复杂系统设计的前提。因此,本实验以HDMI显示为核心内容,带领大家从零开始构建一个完整的视频输出链路。通过配置显示参数、生成时序控制信号、输出RGB图像数据等关键步骤,最终实现在HDMI接口上稳定输出画面。
|
||||
|
||||
在本次实验中我们将学习利用实验板的HDMI接口和MS7210芯片,进行HDMI显示实验的设计。
|
||||
|
||||
## 6.2 理论学习
|
||||
|
||||
### 6.2.1 VGA时序
|
||||
|
||||
VGA显示是在行同步和帧同步(场同步)的信号同步下,按照从上到下,从左到右的顺序,扫描到显示屏上。VGA扫描方式见下图所示:
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/1.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:50%"/>
|
||||
<br> <!--换行-->
|
||||
图1.VGA扫描顺序 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
如上图所示,每一帧图像都是从左上角开始,逐行扫描形成,所以规定最左上角的像素点为第一个像素点,坐标是(0,0),以这个像素为起点,向右x坐标逐渐增大,向下y坐标逐渐增大,重复若干次后扫描到右下角完成一帧图像的扫描,扫描完成后进行图像消隐,随后指针跳回左上角重新进行新一帧的扫描。
|
||||
|
||||
在扫描的过程中会对每一个像素点进行单独赋值,使每个像素点显示对应色彩信息,当扫描速度足够快,加之人眼的视觉暂留特性,我们会看到一幅完整的图片,这就是VGA 显示的原理。
|
||||
|
||||
VGA显示除了要有像素点的信息,还需要有行同步(HSync)和场同步(VSync)两个信号辅助显示。行同步信号规定了一行像素的开始与结束,场同步信号规定了一帧图像的开始与结束。在VESA DMT 1.12版本的标准文档中给出的VGA时序图如下图所示:
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/2.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图2.VGA标准时序 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
行同步时序如下图所示:
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/3.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图3.行同步时序 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
行同步的一个扫周期要经过6个部分,分别是Sync(同步)、 Back Porch(后沿)、 Left Border(左边框)、 “Addressable” Video(有效图像)、 Right Border(右边框)、 Front Porch(前沿),这些过程的长度都是以像素为单位的,也就是以像素时钟为单位,例如Sync的值为96,也就意味着Sync阶段要经历96个像素时钟。HSync信号会在Sync同步阶段拉高(不同的芯片可能有不同标准)以确定新一行的开始与上一行的结束。而完整的一行像素很多,但有效的真正能显示在屏幕上的像素只有 “Addressable” Video(有效图像)部分的像素,其他阶段的像素均无效,无法显示在屏幕中。
|
||||
|
||||
场同步时序如下图所示:
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/4.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图4.场同步时序 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
场同步时序与行同步时序相同,也是分为6个部分,在Sync同步阶段拉高,标志着一帧的结束和新一帧的开始,其中像素只有在“Addressable” Video(有效图像)阶段才有效,其他阶段均无效。而场同步信号的基本单位是行,比如Sync的值为2,也就意味着Sync同步阶段要经历两行。
|
||||
|
||||
那么我们将行同步和场同步信号结合起来,遍可以得到一帧图像的样貌,如下图所示:
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/5.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图5.一帧图像组成示意图 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
可以看到在行场同步信号构成了一个二维坐标系,原点在左上方,中间遍形成了一帧图像,而真正能显示在屏幕中的图像只有 “Addressable” Video(有效图像)部分。
|
||||
|
||||
现在我们知道了行同步和场同步都要经历6个部分,那么这些部分的长度都是如何规定的呢?VGA 行时序对行同步时间、 消隐时间、 行视频有效时间和行前肩时间有特定的规范, 场时序也是如此。 常用VGA 分辨率时序参数如下表所示:
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/6.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图6.常用VGA分辨率时序参数 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
### 6.2.2 MS7210芯片
|
||||
|
||||
MS7210是一款HD发送芯片,支持4K@30Hz的视频3D传输格式。可以支持的最高分辨率高达4K@30Hz,最高采样率达到300MHz。MS7210支持YUV和RGB 之间的色彩空间转换,数字接口支持YUV以及RGB格式输入。MS7210的IIS接口以及S/PDIF 接口支持高清音频的传输,其中S/PDIF接口既可以兼容IEC61937标准下的压缩音频传输,同时还支持高比特音频(HBR)的传输,在高比特音频(HBR)模式下,音频采样率最高为768KHz。MS7210的IIC 地址可以根据SA引脚进行选择。当 SA引脚上拉到电源电压或者悬空时,地址为 OxB2。当 SA 引脚连接到 GND 时,地址为0x56。
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/7.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图7.MS7210芯片 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/8.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:50%"/>
|
||||
<br> <!--换行-->
|
||||
图8.MS7210功能框图 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
MS7210芯片可以通过IIC协议对内部寄存器进行配置,有关芯片寄存器配置需要向芯片厂家进行申请。
|
||||
|
||||
## 6.3 实战演练
|
||||
|
||||
### 6.3.1实验目标
|
||||
|
||||
### 6.3.2硬件资源
|
||||
|
||||
实验板共有一个HDMI-OUT接口,由MS7210驱动,一个HDMI-IN接口,由MS7200驱动。
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/9.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:30%"/>
|
||||
<br> <!--换行-->
|
||||
图9.板载HDMI芯片 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
|
||||
实验箱配备一个小型HDMI显示器,该显示器HDMI接口与HDMI-OUT接口连接,图像可以显示在显示屏中,通过摄像头可以在网站观察现象
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/xxx.png"
|
||||
alt="实验箱显示器"
|
||||
style="zoom:40%"/>
|
||||
<br> <!--换行-->
|
||||
图10.实验箱显示器 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
|
||||
### 6.3.3程序设计
|
||||
|
||||
在设计程序时,我们先对本实验工程有一个整体认知,首先来看一下HDMI彩条显示实验的整体框图。
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/xxxx.png"
|
||||
alt="实验箱显示器"
|
||||
style="zoom:40%"/>
|
||||
<br> <!--换行-->
|
||||
图11.HDMI彩条显示整体框图 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
可见整个实验一共由好多个模块组成,下面是各个模块简介
|
||||
| 模块名称 |功能描述| 备注 |
|
||||
|:----:|:----:|:----:|
|
||||
| hdmi_top | 顶层模块 ||
|
||||
| ms7210_ctrl_iic_top | ms7210芯片配置和iic顶层模块 |参考小眼睛例程|
|
||||
| ms7210_ctl | ms7210芯片配置和时序控制模块 |使用小眼睛例程|
|
||||
| iic_dri | iic驱动模块 |使用小眼睛例程|
|
||||
| vga_ctrl | vga时序信号生成模块 |参考野火例程|
|
||||
| vga_pic | vga像素数据生成模块 |参考野火例程|
|
||||
|
||||
本次实验主要完成vga_ctrl和vga_pic模块的设计。
|
||||
|
||||
对于vga_ctrl模块,我们主要完成hsync,vsync信号,xy坐标,数据有效rgb_valid信号的设计。经过我们前面的学习已经对vga时序有了一定的了解,我们可以想象到这几个信号也只是一种计数器而已。
|
||||
|
||||
本实验要实现640x480的彩条显示,相关参数如下所示:
|
||||
|
||||
```Verilog
|
||||
//parameter define
|
||||
parameter H_SYNC = 10'd96 , //行同步
|
||||
H_BACK = 10'd40 , //行时序后沿
|
||||
H_LEFT = 10'd8 , //行时序左边框
|
||||
H_VALID = 10'd640 , //行有效数据
|
||||
H_RIGHT = 10'd8 , //行时序右边框
|
||||
H_FRONT = 10'd8 , //行时序前沿
|
||||
H_TOTAL = 10'd800 ; //行扫描周期
|
||||
parameter V_SYNC = 10'd2 , //场同步
|
||||
V_BACK = 10'd25 , //场时序后沿
|
||||
V_TOP = 10'd8 , //场时序上边框
|
||||
V_VALID = 10'd480 , //场有效数据
|
||||
V_BOTTOM = 10'd8 , //场时序下边框
|
||||
V_FRONT = 10'd2 , //场时序前沿
|
||||
V_TOTAL = 10'd525 ; //场扫描周期
|
||||
```
|
||||
|
||||
首先设计两个计数器`cnt_h`和`cnt_v`分别对像素和行进行计数,一个像素时钟过后`cnt_h`加一,一行过后`cnt_v`加一,扫描完一帧之后,计数器归零。
|
||||
|
||||
而其他的状态信号则可以根据计数器的计数进行设计。hsync信号只要`cnt_h < H_SYNC`就拉高,vsync信号类似。当计数到有效数据部分数据有效信号rgb_valid就可以拉高,注意,由于时序逻辑有一个时钟周期的反应时间,所以xy的坐标变化比rgb_valid提前一个时钟周期。参考代码如下所示:
|
||||
|
||||
```verilog
|
||||
`timescale 1ns/1ns
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Author : EmbedFire
|
||||
// 实验平台: 野火FPGA系列开发板
|
||||
// 公司 : http://www.embedfire.com
|
||||
// 论坛 : http://www.firebbs.cn
|
||||
// 淘宝 : https://fire-stm32.taobao.com
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
module vga_ctrl
|
||||
(
|
||||
input wire vga_clk , //输入工作时钟,频率25MHz
|
||||
input wire sys_rst_n , //输入复位信号,低电平有效
|
||||
input wire [15:0] pix_data , //输入像素点色彩信息
|
||||
|
||||
output wire [11:0] pix_x , //输出VGA有效显示区域像素点X轴坐标
|
||||
output wire [11:0] pix_y , //输出VGA有效显示区域像素点Y轴坐标
|
||||
output wire hsync , //输出行同步信号
|
||||
output wire vsync , //输出场同步信号
|
||||
output wire rgb_valid ,
|
||||
output wire [15:0] rgb //输出像素点色彩信息
|
||||
);
|
||||
|
||||
//********************************************************************//
|
||||
//****************** Parameter and Internal Signal *******************//
|
||||
//********************************************************************//
|
||||
//parameter define
|
||||
parameter H_SYNC = 10'd96 , //行同步
|
||||
H_BACK = 10'd40 , //行时序后沿
|
||||
H_LEFT = 10'd8 , //行时序左边框
|
||||
H_VALID = 10'd640 , //行有效数据
|
||||
H_RIGHT = 10'd8 , //行时序右边框
|
||||
H_FRONT = 10'd8 , //行时序前沿
|
||||
H_TOTAL = 10'd800 ; //行扫描周期
|
||||
parameter V_SYNC = 10'd2 , //场同步
|
||||
V_BACK = 10'd25 , //场时序后沿
|
||||
V_TOP = 10'd8 , //场时序上边框
|
||||
V_VALID = 10'd480 , //场有效数据
|
||||
V_BOTTOM = 10'd8 , //场时序下边框
|
||||
V_FRONT = 10'd2 , //场时序前沿
|
||||
V_TOTAL = 10'd525 ; //场扫描周期
|
||||
|
||||
//wire define
|
||||
wire pix_data_req ; //像素点色彩信息请求信号
|
||||
|
||||
//reg define
|
||||
reg [11:0] cnt_h ; //行同步信号计数器
|
||||
reg [11:0] cnt_v ; //场同步信号计数器
|
||||
|
||||
//********************************************************************//
|
||||
//***************************** Main Code ****************************//
|
||||
//********************************************************************//
|
||||
|
||||
//cnt_h:行同步信号计数器
|
||||
always@(posedge vga_clk or negedge sys_rst_n)
|
||||
if(sys_rst_n == 1'b0)
|
||||
cnt_h <= 12'd0 ;
|
||||
else if(cnt_h == H_TOTAL - 1'd1)
|
||||
cnt_h <= 12'd0 ;
|
||||
else
|
||||
cnt_h <= cnt_h + 1'd1 ;
|
||||
|
||||
//hsync:行同步信号
|
||||
assign hsync = (cnt_h <= H_SYNC - 1'd1) ? 1'b1 : 1'b0 ;
|
||||
|
||||
//cnt_v:场同步信号计数器
|
||||
always@(posedge vga_clk or negedge sys_rst_n)
|
||||
if(sys_rst_n == 1'b0)
|
||||
cnt_v <= 12'd0 ;
|
||||
else if((cnt_v == V_TOTAL - 1'd1) && (cnt_h == H_TOTAL-1'd1))
|
||||
cnt_v <= 12'd0 ;
|
||||
else if(cnt_h == H_TOTAL - 1'd1)
|
||||
cnt_v <= cnt_v + 1'd1 ;
|
||||
else
|
||||
cnt_v <= cnt_v ;
|
||||
|
||||
//vsync:场同步信号
|
||||
assign vsync = (cnt_v <= V_SYNC - 1'd1) ? 1'b1 : 1'b0 ;
|
||||
|
||||
//rgb_valid:VGA有效显示区域
|
||||
assign rgb_valid = (((cnt_h >= H_SYNC + H_BACK + H_LEFT)
|
||||
&& (cnt_h < H_SYNC + H_BACK + H_LEFT + H_VALID))
|
||||
&&((cnt_v >= V_SYNC + V_BACK + V_TOP)
|
||||
&& (cnt_v < V_SYNC + V_BACK + V_TOP + V_VALID)))
|
||||
? 1'b1 : 1'b0;
|
||||
|
||||
//pix_data_req:像素点色彩信息请求信号,超前rgb_valid信号一个时钟周期
|
||||
assign pix_data_req = (((cnt_h >= H_SYNC + H_BACK + H_LEFT - 1'b1)
|
||||
&& (cnt_h < H_SYNC + H_BACK + H_LEFT + H_VALID - 1'b1))
|
||||
&&((cnt_v >= V_SYNC + V_BACK + V_TOP)
|
||||
&& (cnt_v < V_SYNC + V_BACK + V_TOP + V_VALID)))
|
||||
? 1'b1 : 1'b0;
|
||||
|
||||
//pix_x,pix_y:VGA有效显示区域像素点坐标
|
||||
assign pix_x = (pix_data_req == 1'b1)
|
||||
? (cnt_h - (H_SYNC + H_BACK + H_LEFT - 1'b1)) : 12'hfff;
|
||||
assign pix_y = (pix_data_req == 1'b1)
|
||||
? (cnt_v - (V_SYNC + V_BACK + V_TOP)) : 12'hfff;
|
||||
|
||||
//rgb:输出像素点色彩信息
|
||||
assign rgb = (rgb_valid == 1'b1) ? pix_data : 16'b0 ;
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
对于vga_pic模块,我们可以根据x坐标范围(0~639)分成十份,每一份输出不同的颜色。参考代码如下所示:
|
||||
|
||||
```verilog
|
||||
`timescale 1ns/1ns
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Author : EmbedFire
|
||||
// 实验平台: 野火FPGA系列开发板
|
||||
// 公司 : http://www.embedfire.com
|
||||
// 论坛 : http://www.firebbs.cn
|
||||
// 淘宝 : https://fire-stm32.taobao.com
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
module vga_pic
|
||||
(
|
||||
input wire vga_clk , //输入工作时钟,频率25MHz
|
||||
input wire sys_rst_n , //输入复位信号,低电平有效
|
||||
input wire [11:0] pix_x , //输入VGA有效显示区域像素点X轴坐标
|
||||
input wire [11:0] pix_y , //输入VGA有效显示区域像素点Y轴坐标
|
||||
|
||||
output reg [15:0] pix_data //输出像素点色彩信息
|
||||
);
|
||||
|
||||
//********************************************************************//
|
||||
//****************** Parameter and Internal Signal *******************//
|
||||
//********************************************************************//
|
||||
//parameter define
|
||||
parameter H_VALID = 12'd640 , //行有效数据
|
||||
V_VALID = 12'd480 ; //场有效数据
|
||||
|
||||
parameter RED = 16'hF800, //红色
|
||||
ORANGE = 16'hFC00, //橙色
|
||||
YELLOW = 16'hFFE0, //黄色
|
||||
GREEN = 16'h07E0, //绿色
|
||||
CYAN = 16'h07FF, //青色
|
||||
BLUE = 16'h001F, //蓝色
|
||||
PURPPLE = 16'hF81F, //紫色
|
||||
BLACK = 16'h0000, //黑色
|
||||
WHITE = 16'hFFFF, //白色
|
||||
GRAY = 16'hD69A; //灰色
|
||||
|
||||
//********************************************************************//
|
||||
//***************************** Main Code ****************************//
|
||||
//********************************************************************//
|
||||
//pix_data:输出像素点色彩信息,根据当前像素点坐标指定当前像素点颜色数据
|
||||
always@(posedge vga_clk or negedge sys_rst_n)
|
||||
if(sys_rst_n == 1'b0)
|
||||
pix_data <= 16'd0;
|
||||
else if((pix_x >= 0) && (pix_x < (H_VALID/10)*1))
|
||||
pix_data <= RED;
|
||||
else if((pix_x >= (H_VALID/10)*1) && (pix_x < (H_VALID/10)*2))
|
||||
pix_data <= ORANGE;
|
||||
else if((pix_x >= (H_VALID/10)*2) && (pix_x < (H_VALID/10)*3))
|
||||
pix_data <= YELLOW;
|
||||
else if((pix_x >= (H_VALID/10)*3) && (pix_x < (H_VALID/10)*4))
|
||||
pix_data <= GREEN;
|
||||
else if((pix_x >= (H_VALID/10)*4) && (pix_x < (H_VALID/10)*5))
|
||||
pix_data <= CYAN;
|
||||
else if((pix_x >= (H_VALID/10)*5) && (pix_x < (H_VALID/10)*6))
|
||||
pix_data <= BLUE;
|
||||
else if((pix_x >= (H_VALID/10)*6) && (pix_x < (H_VALID/10)*7))
|
||||
pix_data <= PURPPLE;
|
||||
else if((pix_x >= (H_VALID/10)*7) && (pix_x < (H_VALID/10)*8))
|
||||
pix_data <= BLACK;
|
||||
else if((pix_x >= (H_VALID/10)*8) && (pix_x < (H_VALID/10)*9))
|
||||
pix_data <= WHITE;
|
||||
else if((pix_x >= (H_VALID/10)*9) && (pix_x < H_VALID))
|
||||
pix_data <= GRAY;
|
||||
else
|
||||
pix_data <= BLACK;
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
在顶层模块,我们首先要利用PLL ip核生成iic的驱动时钟进行初始化,由于ms7210芯片的需要,我们通过计数设置一个延迟复位信号,由于我们的彩条颜色是按照RGB565格式生成的,所以需要向RGB888进行转换,只需要填0补位即可,同时由于板载时钟是27M与25.175M相差不大,所以直接使用板载时钟作为像素时钟输出。然后我们将输出的行场同步信号,像素时钟,像素数据,像素数据有效信号等与模块相连接即可完成设计。顶层模块参考代码如下:
|
||||
|
||||
```Verilog
|
||||
`timescale 1ns / 1ns
|
||||
module hdmi_top(
|
||||
input wire sys_clk ,// input system clock 50MHz
|
||||
input rstn_in ,
|
||||
output rstn_out ,
|
||||
output hd_scl ,
|
||||
inout hd_sda ,
|
||||
output led_int ,
|
||||
|
||||
//hdmi_out
|
||||
output pixclk_out ,//pixclk
|
||||
output wire vs_out ,
|
||||
output wire hs_out ,
|
||||
output wire de_out ,
|
||||
output wire [7:0] r_out ,
|
||||
output wire [7:0] g_out ,
|
||||
output wire [7:0] b_out
|
||||
|
||||
);
|
||||
wire cfg_clk ;
|
||||
wire locked ;
|
||||
wire rstn ;
|
||||
wire init_over ;
|
||||
reg [15:0] rstn_1ms ;
|
||||
//**********************************************//
|
||||
//*****************MS7210初始化******************//
|
||||
//**********************************************//
|
||||
//**************仿真时不编译此部分***************//
|
||||
`ifndef SIM
|
||||
//初始化成功标志
|
||||
assign led_int = init_over;
|
||||
//生成10M IIC时钟
|
||||
PLL u_pll (
|
||||
.clkout0(cfg_clk), // output
|
||||
.lock(locked), // output
|
||||
.clkin1(sys_clk) // input
|
||||
);
|
||||
//ms7210初始化模块
|
||||
ms7210_ctrl_iic_top ms7210_ctrl_iic_top_inst(
|
||||
.clk ( cfg_clk ), //input clk,
|
||||
.rst_n ( rstn_out ), //input rstn,
|
||||
|
||||
.init_over ( init_over ), //output init_over,
|
||||
.iic_scl ( hd_scl ), //output iic_scl,
|
||||
.iic_sda ( hd_sda ) //inout iic_sda
|
||||
);
|
||||
//延迟复位
|
||||
always @(posedge cfg_clk)
|
||||
begin
|
||||
if(!locked)
|
||||
rstn_1ms <= 16'd0;
|
||||
else
|
||||
begin
|
||||
if(rstn_1ms == 16'h2710)
|
||||
rstn_1ms <= rstn_1ms;
|
||||
else
|
||||
rstn_1ms <= rstn_1ms + 1'b1;
|
||||
end
|
||||
end
|
||||
assign rstn_out = (rstn_1ms == 16'h2710) && rstn_in;
|
||||
//**********************************************//
|
||||
`else
|
||||
assign led_int = 1;
|
||||
assign rstn_out = rstn_in;
|
||||
|
||||
`endif
|
||||
//**********************************************//
|
||||
//**********************************************//
|
||||
//**********************************************//
|
||||
//**********************************************//
|
||||
wire [15:0] rgb565;
|
||||
wire [15:0] pix_data ;
|
||||
wire [11:0] pix_x;
|
||||
wire [11:0] pix_y;
|
||||
//vga行场同步控制模块
|
||||
vga_ctrl vga_ctrl_inst (
|
||||
.vga_clk (sys_clk ),
|
||||
.sys_rst_n (rstn_out ),
|
||||
.pix_data (pix_data ),
|
||||
.pix_x (pix_x ),
|
||||
.pix_y (pix_y ),
|
||||
.hsync (hs_out ),
|
||||
.vsync (vs_out ),
|
||||
.rgb_valid (de_out ),
|
||||
.rgb (rgb565 )
|
||||
);
|
||||
//彩条数据生成模块
|
||||
vga_pic vga_pic_inst (
|
||||
.vga_clk (sys_clk ),
|
||||
.sys_rst_n (rstn_out ),
|
||||
.pix_x (pix_x ),
|
||||
.pix_y (pix_y ),
|
||||
.pix_data_out (pix_data )
|
||||
);
|
||||
//RGB565转RGB888
|
||||
assign pixclk_out = sys_clk ;//直接使用27M时钟,与25.175相差不大
|
||||
assign r_out = {rgb565[15:11],3'b0};
|
||||
assign g_out = {rgb565[10: 5],2'b0};
|
||||
assign b_out = {rgb565[ 4: 0],3'b0};
|
||||
endmodule
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 6.3.4仿真验证
|
||||
|
||||
由于仿真不需要对MS7210芯片进行初始化,所以我们在top文件中加入条件编译指令,并且在仿真文件中定义SIM宏,那么就可以在仿真中不编译ms7210初始化相关代码,只对vga时序进行仿真。我们只需要提供时钟和复位,即可对模块进行仿真。仿真文件如下所示:
|
||||
|
||||
```verilog
|
||||
`timescale 1ns / 1ns
|
||||
`define SIM
|
||||
module hdmi_top_tb;
|
||||
|
||||
// Parameters
|
||||
|
||||
//Ports
|
||||
reg sys_clk;
|
||||
reg rstn_in;
|
||||
wire rstn_out;
|
||||
wire hd_scl;
|
||||
wire hd_sda;
|
||||
wire led_int;
|
||||
wire pixclk_out;
|
||||
wire vs_out;
|
||||
wire hs_out;
|
||||
wire de_out;
|
||||
wire [7:0] r_out;
|
||||
wire [7:0] g_out;
|
||||
wire [7:0] b_out;
|
||||
|
||||
initial begin
|
||||
sys_clk = 0;
|
||||
rstn_in = 0;
|
||||
#100
|
||||
rstn_in = 1;
|
||||
end
|
||||
always #(500/27) sys_clk = ~sys_clk;
|
||||
hdmi_top hdmi_top_inst (
|
||||
.sys_clk(sys_clk),
|
||||
.rstn_in(rstn_in),
|
||||
.rstn_out(rstn_out),
|
||||
.hd_scl(hd_scl),
|
||||
.hd_sda(hd_sda),
|
||||
.led_int(led_int),
|
||||
.pixclk_out(pixclk_out),
|
||||
.vs_out(vs_out),
|
||||
.hs_out(hs_out),
|
||||
.de_out(de_out),
|
||||
.r_out(r_out),
|
||||
.g_out(g_out),
|
||||
.b_out(b_out)
|
||||
);
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
直接点击sim文件夹下hebav文件夹中的do.bat文件即可利用ModuleSim对模块进行仿真,仿真波形如下:
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/10.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图10.仿真波形(一) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
从上图我们可以发现vsync信号拉高了两个行同步信号的长度,与设计相符
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/11.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图11.仿真波形(二) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/12.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图12.仿真波形(三) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
从图11和12中我们可以看到当cnt_h信号计数结束后会恢复0,cnt_v会加一,hsync信号会拉高96个像素时钟(0~95)cnt_h和hsync与设计相符。
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/13.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图13.仿真波形(四) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
如图13所示,当cnt_h计数到H_SYNC + H_BACK + H_LEFT,也就是144时,rgb_valid拉高,xy轴坐标比rgb_valid提前一个时钟周期,以便pix_data准备好数据,符合设计。
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/14.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图14.仿真波形(五) <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
从每一行看,每一行被分成了10个部分,每部分像素数据分别对应不同颜色,符合设计要求。可以进行下一步上板验证。
|
||||
|
||||
### 6.3.5上板验证
|
||||
|
||||
仿真已经通过,可以进行上板验证,上板前要先进行管脚约束。端口与对应管脚如下表所示:
|
||||
| 端口名称 |信号类型| 对应管脚|功能|
|
||||
|:----:|:----:|:----:|:----:|
|
||||
| sysclk | Input | D18 | 27M时钟 |
|
||||
| rstn_in | Input | C22 | 外部输入复位 |
|
||||
| rstn_out | Output | G25 | 输出ms7210复位 |
|
||||
| hd_scl | Output | K22 | iic SCL信号 |
|
||||
| hd_sda | Output | K23 | iic SDA信号 |
|
||||
| led_int | Output | A20 | 配置完成信号 |
|
||||
| pixclk_out | Output | G25 | 像素时钟输出 |
|
||||
| vs_out | Output | R21 | Vsync输出 |
|
||||
| hs_out | Output | R20 | Hsync输出 |
|
||||
| de_out | Output | N19 | RGB_valid输出 |
|
||||
| r_out[0] | Output | N21 | RGB888输出 |
|
||||
| r_out[1] | Output | L23 | RGB888输出 |
|
||||
| r_out[2] | Output | L22 | RGB888输出 |
|
||||
| r_out[3] | Output | L25 | RGB888输出 |
|
||||
| r_out[4] | Output | L24 | RGB888输出 |
|
||||
| r_out[5] | Output | K26 | RGB888输出 |
|
||||
| r_out[6] | Output | K25 | RGB888输出 |
|
||||
| r_out[7] | Output | P16 | RGB888输出 |
|
||||
| g_out[0] | Output | T25 | RGB888输出 |
|
||||
| g_out[1] | Output | P25 | RGB888输出 |
|
||||
| g_out[2] | Output | R25 | RGB888输出 |
|
||||
| g_out[3] | Output | P24 | RGB888输出 |
|
||||
| g_out[4] | Output | P23 | RGB888输出 |
|
||||
| g_out[5] | Output | N24 | RGB888输出 |
|
||||
| g_out[6] | Output | N23 | RGB888输出 |
|
||||
| g_out[7] | Output | N22 | RGB888输出 |
|
||||
| b_out[0] | Output | P19 | RGB888输出 |
|
||||
| b_out[1] | Output | P21 | RGB888输出 |
|
||||
| b_out[2] | Output | P20 | RGB888输出 |
|
||||
| b_out[3] | Output | M22 | RGB888输出 |
|
||||
| b_out[4] | Output | M21 | RGB888输出 |
|
||||
| b_out[5] | Output | N18 | RGB888输出 |
|
||||
| b_out[6] | Output | R22 | RGB888输出 |
|
||||
| b_out[7] | Output | T22 | RGB888输出 |
|
||||
|
||||
|
||||
管脚分配可以直接编写.fdc文件,也可以使用PDS内置的工具进行分配。完成管脚分配之后就可以生成sbit文件,将文件提交到网站后点击烧录,即可将sbit下载到实验板中,在摄像头页面即可观察到显示屏中显示出彩条。
|
||||
|
||||
## 6.4 章末总结
|
||||
|
||||
本次实验主要学习VGA时序的相关知识,并使用HD硬核进行HDMI显示,感兴趣的同学可以尝试使用HDMI显示其他图像。
|
||||
BIN
public/doc/06/images/1.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
public/doc/06/images/10.png
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
public/doc/06/images/11.png
Normal file
|
After Width: | Height: | Size: 620 KiB |
BIN
public/doc/06/images/12.png
Normal file
|
After Width: | Height: | Size: 614 KiB |
BIN
public/doc/06/images/13.png
Normal file
|
After Width: | Height: | Size: 942 KiB |
BIN
public/doc/06/images/14.png
Normal file
|
After Width: | Height: | Size: 543 KiB |
BIN
public/doc/06/images/2.png
Normal file
|
After Width: | Height: | Size: 745 KiB |
BIN
public/doc/06/images/3.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/doc/06/images/4.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/doc/06/images/5.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/doc/06/images/6.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
public/doc/06/images/7.png
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
public/doc/06/images/8.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
public/doc/06/images/9.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/doc/11/cover.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
400
public/doc/11/doc.md
Normal file
@@ -0,0 +1,400 @@
|
||||
# 进阶-1-密码锁实验
|
||||
|
||||
## 1.1 章节导读
|
||||
本章作为进阶的第一个实验,主要学习状态机的写法和使用,同时联系前面所学的数码管和矩阵键盘,完成一个密码锁的设计。
|
||||
|
||||
|
||||
## 1.2 理论学习
|
||||
### 1.2.1 FSM状态机
|
||||
|
||||
在数字逻辑设计中,**有限状态机(FSM, Finite State Machine)**是一种根据输入和当前状态决定下一个状态和输出的模型,广泛用于顺序逻辑电路的控制部分。
|
||||
|
||||
在本实验中,我们将使用 FSM 构建密码锁的控制逻辑,用于管理**按键输入过程、密码比对、开锁显示、错误处理等多个步骤**。
|
||||
|
||||
FSM 通常包含以下几个组成部分:
|
||||
|
||||
- **状态定义(State)**:用来描述系统当前所处的逻辑阶段。例如:待输入、输入中、校验中、成功、失败等。
|
||||
|
||||
- **状态转移条件(Transition)**:根据输入信号(如按键、定时器、复位)从一个状态跳转到另一个状态。
|
||||
|
||||
- **输出控制(Output)**:每个状态下系统应有的行为,比如更新数码管、检测密码、拉高开锁信号等。
|
||||
|
||||
常见的 FSM 类型包括:
|
||||
|
||||
- **Moore 状态机:**输出只与当前状态有关,结构更稳定;
|
||||
- **Mealy 状态机:**输出与当前状态和输入有关,反应更灵敏。
|
||||
|
||||
在本例中我们要设计一个状态机去对密码锁进行控制。首先我们应该先给密码锁分一下他会处于什么状态,每个状态有什么输出(本例中将密码锁设计成下述4个状态):
|
||||
|
||||
1. SETUP状态:该状态下可以设置4位密码,输入4位数字后按#键设置密码有效,*清空设置,数码管输出4位数字输入
|
||||
2. LOCK状态:锁定状态,可以输入密码解锁,按#确定,*键清空输入,数码管输出4位数字输入
|
||||
3. ERROR状态:如果输入密码错误,或者操作错误,进入此状态,数码管输出ERROR
|
||||
4. UNLOCK状态:解锁状态,可以按*重设密码,也可以按#重新锁定,数码管输出UNLOCK
|
||||
|
||||
然后确定状态之间如何进行转移:
|
||||
|
||||
1. SETUP状态:输入4位数字后按#键设置密码有效,有效后进入LOCK状态
|
||||
2. LOCK状态:输入密码,按#确定后如果密码正确进入UNLOCK状态,如果错误进入ERROR状态
|
||||
3. ERROR状态:按下任意按键后进入LOCK状态
|
||||
4. UNLOCK状态:按下#键进入LOCK状态,按*键进入SETUP状态重设密码
|
||||
|
||||
根据上述状态转移逻辑,我们可以画出状态转移图,状态转移图如下图所示:
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/1.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:100%"/>
|
||||
<br> <!--换行-->
|
||||
图1.状态转移图 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
我们已经了解了本次实验所使用的状态机,那么如何使用verilog编写状态机呢?主要有三种方法,分别是:三段式状态机,二段式状态机,一段式状态机。
|
||||
|
||||
三段式状态机写法如下:
|
||||
|
||||
- 状态机第一段,时序逻辑,非阻塞赋值,传递寄存器的状态。
|
||||
- 状态机第二段,组合逻辑,阻塞赋值,根据当前状态和当前输入,确定下一个状态机的状态。
|
||||
- 状态机第三代,时序逻辑,非阻塞赋值,因为是 Mealy 型状态机,根据当前状态和当前输入,确定输出信号。
|
||||
|
||||
二段式状态机将三段式状态机二三段糅合在一起,一段式状态机则将三段式状态机三段融合。推荐使用三段式状态机,只有在状态转移逻辑非常简单,状态很少时会采用一段式状态机。
|
||||
|
||||
### 1.2.2 数码管
|
||||
|
||||
见基础实验3
|
||||
|
||||
### 1.2.3 矩阵键盘
|
||||
|
||||
见基础实验4
|
||||
|
||||
## 1.3 实战演练
|
||||
### 1.3.1 系统架构
|
||||
``` verilog
|
||||
系统框图:
|
||||
```
|
||||
|
||||
### 1.3.2 模块设计
|
||||
首先是密码锁状态机逻辑,本例采用三段式状态机写法。代码如下:
|
||||
|
||||
#### password_lock
|
||||
|
||||
```verilog
|
||||
module password_lock(
|
||||
input wire clk,
|
||||
input wire rstn,
|
||||
input wire [15:0] key_trigger,
|
||||
output reg [8*8-1:0] assic_seg,
|
||||
output wire [7:0] seg_point
|
||||
);
|
||||
|
||||
/*
|
||||
K00 K01 K02 K03 | 1 2 3 A
|
||||
|
|
||||
K04 K05 K06 K07 | 4 5 6 B
|
||||
|
|
||||
K08 K09 K10 K11 | 7 8 9 C
|
||||
|
|
||||
K12 K13 K14 K15 | * 0 # D
|
||||
*/
|
||||
|
||||
/*
|
||||
密码锁状态机设定:
|
||||
1. SETUP状态 :设置密码,按*清空输入,按#确认输入进入LOCK状态,不足4位#键无效
|
||||
2. LOCK状态 :锁定状态,按*清空输入,按#确认输入,不足4位#键无效,密码正确解锁,错误则进入ERROR状态
|
||||
3. ERROR状态 :密码错误状态,按任意键返回LCOK状态
|
||||
4. UNLOCK状态:解锁状态,按*重设密码,按#重新锁定,其余键无效
|
||||
|
||||
1-D键为输入
|
||||
*为清空之前的输入
|
||||
#为确认输入
|
||||
|
||||
*/
|
||||
|
||||
wire flag_setup_password;
|
||||
wire flag_input_pass;
|
||||
wire flag_input_confirm;
|
||||
wire flag_error_return;
|
||||
wire flag_relock;
|
||||
wire flag_reset;
|
||||
|
||||
localparam [2:0] ST_SETUP = 3'b001;
|
||||
localparam [2:0] ST_LOCK = 3'b010;
|
||||
localparam [2:0] ST_ERROR = 3'b100;
|
||||
localparam [2:0] ST_UNLOCK = 3'b101;
|
||||
|
||||
reg [2:0] cu_st, nt_st;
|
||||
reg [4*4-1:0] password, input_password;
|
||||
reg [2:0] input_num;
|
||||
|
||||
assign flag_setup_password = (cu_st == ST_SETUP) && (key_trigger[14]) && (input_num == 3'b100);
|
||||
assign flag_input_confirm = (cu_st == ST_LOCK) && (key_trigger[14]) && (input_num == 3'b100);
|
||||
assign flag_input_pass = (cu_st == ST_LOCK) && (password == input_password) && (input_num == 3'b100);
|
||||
assign flag_error_return = (cu_st == ST_ERROR) && (|key_trigger);
|
||||
assign flag_relock = (cu_st == ST_UNLOCK) && (key_trigger[14]);
|
||||
assign flag_reset = (cu_st == ST_UNLOCK) && (key_trigger[12]);
|
||||
//状态机第一段,传递寄存器状态
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(~rstn) cu_st <= ST_SETUP;
|
||||
else cu_st <= nt_st;
|
||||
end
|
||||
//状态机第二段,确定下一个状态机状态
|
||||
always @(*) begin
|
||||
case(cu_st)
|
||||
ST_SETUP : nt_st <= (flag_setup_password)?(ST_LOCK):(ST_SETUP);
|
||||
ST_LOCK : nt_st <= (flag_input_confirm)?((flag_input_pass)?(ST_UNLOCK):(ST_ERROR)):(ST_LOCK);
|
||||
ST_ERROR : nt_st <= (flag_error_return)?(ST_LOCK):(ST_ERROR);
|
||||
ST_UNLOCK: nt_st <= (flag_relock)?(ST_LOCK):((flag_reset)?(ST_SETUP):(ST_UNLOCK));
|
||||
default : nt_st <= ST_SETUP;
|
||||
endcase
|
||||
end
|
||||
//状态机第三段,根据状态和输入确定输出,这里由于信号较多,分了多个always块,也可以用case语句写在同一个always块中
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(~rstn) password <= 0;
|
||||
else if((cu_st == ST_SETUP) && (input_num != 3'b100)) begin
|
||||
if(key_trigger[00]) password <= {password[0+:3*4], 4'h1};
|
||||
else if(key_trigger[01]) password <= {password[0+:3*4], 4'h2};
|
||||
else if(key_trigger[02]) password <= {password[0+:3*4], 4'h3};
|
||||
else if(key_trigger[03]) password <= {password[0+:3*4], 4'hA};
|
||||
else if(key_trigger[04]) password <= {password[0+:3*4], 4'h4};
|
||||
else if(key_trigger[05]) password <= {password[0+:3*4], 4'h5};
|
||||
else if(key_trigger[06]) password <= {password[0+:3*4], 4'h6};
|
||||
else if(key_trigger[07]) password <= {password[0+:3*4], 4'hB};
|
||||
else if(key_trigger[08]) password <= {password[0+:3*4], 4'h7};
|
||||
else if(key_trigger[09]) password <= {password[0+:3*4], 4'h8};
|
||||
else if(key_trigger[10]) password <= {password[0+:3*4], 4'h9};
|
||||
else if(key_trigger[11]) password <= {password[0+:3*4], 4'hC};
|
||||
else if(key_trigger[12]) password <= 0;
|
||||
else if(key_trigger[13]) password <= {password[0+:3*4], 4'h0};
|
||||
else if(key_trigger[14]) password <= password;
|
||||
else if(key_trigger[15]) password <= {password[0+:3*4], 4'hD};
|
||||
else password <= password;
|
||||
end else password <= password;
|
||||
end
|
||||
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(~rstn) input_password <= 0;
|
||||
else if(cu_st == ST_LOCK) begin
|
||||
if(input_num == 3'b100) input_password <= input_password;
|
||||
else if(key_trigger[00]) input_password <= {input_password[0+:3*4], 4'h1};
|
||||
else if(key_trigger[01]) input_password <= {input_password[0+:3*4], 4'h2};
|
||||
else if(key_trigger[02]) input_password <= {input_password[0+:3*4], 4'h3};
|
||||
else if(key_trigger[03]) input_password <= {input_password[0+:3*4], 4'hA};
|
||||
else if(key_trigger[04]) input_password <= {input_password[0+:3*4], 4'h4};
|
||||
else if(key_trigger[05]) input_password <= {input_password[0+:3*4], 4'h5};
|
||||
else if(key_trigger[06]) input_password <= {input_password[0+:3*4], 4'h6};
|
||||
else if(key_trigger[07]) input_password <= {input_password[0+:3*4], 4'hB};
|
||||
else if(key_trigger[08]) input_password <= {input_password[0+:3*4], 4'h7};
|
||||
else if(key_trigger[09]) input_password <= {input_password[0+:3*4], 4'h8};
|
||||
else if(key_trigger[10]) input_password <= {input_password[0+:3*4], 4'h9};
|
||||
else if(key_trigger[11]) input_password <= {input_password[0+:3*4], 4'hC};
|
||||
else if(key_trigger[12]) input_password <= 0;
|
||||
else if(key_trigger[13]) input_password <= {input_password[0+:3*4], 4'h0};
|
||||
else if(key_trigger[14]) input_password <= input_password;
|
||||
else if(key_trigger[15]) input_password <= {input_password[0+:3*4], 4'hD};
|
||||
else input_password <= input_password;
|
||||
end else input_password <= 0;
|
||||
end
|
||||
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(~rstn) input_num <= 0;
|
||||
else if(cu_st == ST_SETUP || cu_st == ST_LOCK) begin
|
||||
if(flag_setup_password || flag_input_confirm) input_num <= 0;
|
||||
else if(key_trigger[00] || key_trigger[01] || key_trigger[02] || key_trigger[03] ||
|
||||
key_trigger[04] || key_trigger[05] || key_trigger[06] || key_trigger[07] ||
|
||||
key_trigger[08] || key_trigger[09] || key_trigger[10] || key_trigger[11] ||
|
||||
key_trigger[13] || key_trigger[15])
|
||||
input_num <= (input_num < 3'b100)?(input_num + 1):(input_num);
|
||||
else if(key_trigger[12]) input_num <= 0;
|
||||
else input_num <= input_num;
|
||||
end else input_num <= 0;
|
||||
end
|
||||
|
||||
assign seg_point = 8'b0;
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if(~rstn) assic_seg <= "12345678";
|
||||
else case(cu_st)
|
||||
ST_SETUP :begin
|
||||
assic_seg[0+:8] <= "-";
|
||||
assic_seg[8+:8] <= "-";
|
||||
assic_seg[16+:8] <= (input_num > 0)?(hex2assic(password[0+:4])):("_");
|
||||
assic_seg[24+:8] <= (input_num > 1)?(hex2assic(password[4+:4])):("_");
|
||||
assic_seg[32+:8] <= (input_num > 2)?(hex2assic(password[8+:4])):("_");
|
||||
assic_seg[40+:8] <= (input_num > 3)?(hex2assic(password[12+:4])):("_");
|
||||
assic_seg[48+:8] <= "-";
|
||||
assic_seg[56+:8] <= "-";
|
||||
end
|
||||
ST_LOCK :begin
|
||||
assic_seg[0+:8] <= "=";
|
||||
assic_seg[8+:8] <= "=";
|
||||
assic_seg[16+:8] <= (input_num > 0)?(hex2assic(input_password[0+:4])):("-");
|
||||
assic_seg[24+:8] <= (input_num > 1)?(hex2assic(input_password[4+:4])):("-");
|
||||
assic_seg[32+:8] <= (input_num > 2)?(hex2assic(input_password[8+:4])):("-");
|
||||
assic_seg[40+:8] <= (input_num > 3)?(hex2assic(input_password[12+:4])):("-");
|
||||
assic_seg[48+:8] <= "=";
|
||||
assic_seg[56+:8] <= "=";
|
||||
end
|
||||
ST_ERROR : assic_seg <= " ERROR ";
|
||||
ST_UNLOCK: assic_seg <= " unlock ";
|
||||
default : assic_seg <= "12345678";
|
||||
endcase
|
||||
end
|
||||
|
||||
|
||||
function [7:0] hex2assic;
|
||||
input [3:0] hex;
|
||||
case(hex)
|
||||
4'h0: hex2assic = "0"; // 0
|
||||
4'h1: hex2assic = "1"; // 1
|
||||
4'h2: hex2assic = "2"; // 2
|
||||
4'h3: hex2assic = "3"; // 3
|
||||
4'h4: hex2assic = "4"; // 4
|
||||
4'h5: hex2assic = "5"; // 5
|
||||
4'h6: hex2assic = "6"; // 6
|
||||
4'h7: hex2assic = "7"; // 7
|
||||
4'h8: hex2assic = "8"; // 8
|
||||
4'h9: hex2assic = "9"; // 9
|
||||
4'hA: hex2assic = "A"; // A
|
||||
4'hB: hex2assic = "B"; // B
|
||||
4'hC: hex2assic = "C"; // C
|
||||
4'hD: hex2assic = "D"; // D
|
||||
4'hE: hex2assic = "E"; // E
|
||||
4'hF: hex2assic = "F"; // F
|
||||
default: hex2assic = " ";
|
||||
endcase
|
||||
endfunction
|
||||
|
||||
endmodule //password_lock
|
||||
```
|
||||
|
||||
矩阵键盘行扫描模块在前面基础实验已经介绍过,但这次实验还需要为矩阵键盘添加按键上升沿检测模块,代码如下:
|
||||
|
||||
#### matrix_key_trigger
|
||||
|
||||
```verilog
|
||||
module matrix_key_trigger(
|
||||
input wire clk,
|
||||
input wire rstn,
|
||||
input wire [15:0] key,
|
||||
output wire [15:0] key_trigger
|
||||
);
|
||||
|
||||
// 按键上升沿捕获模块
|
||||
|
||||
reg [15:0] key_d; // 上一时钟周期的按键状态
|
||||
reg [15:0] key_d2; // 上两时钟周期的按键状态
|
||||
|
||||
assign key_trigger = (key_d) & (~key_d2);
|
||||
|
||||
always @(posedge clk or negedge rstn) begin
|
||||
if (!rstn) begin
|
||||
key_d <= 0;
|
||||
key_d2 <= 0;
|
||||
end else begin
|
||||
key_d <= key;
|
||||
key_d2 <= key_d;
|
||||
end
|
||||
end
|
||||
|
||||
endmodule //matrix_key_decode
|
||||
```
|
||||
|
||||
至于数码管模块,为了方便,在led_display_driver模块添加了参数定义,并未进行其他修改。
|
||||
|
||||
最后将几个模块例化在顶层,将端口相连接,代码如下所示:
|
||||
|
||||
#### password_lock_top
|
||||
|
||||
```verilog
|
||||
|
||||
module password_lock_top #(
|
||||
parameter VALID_SIGNAL = 1'b0,
|
||||
parameter CLK_CYCLE = 5000
|
||||
)(
|
||||
//system io
|
||||
input wire external_clk ,
|
||||
input wire external_rstn,
|
||||
|
||||
output wire [7:0] led_display_seg,
|
||||
output wire [7:0] led_display_sel,
|
||||
|
||||
input wire [3:0] col,
|
||||
output wire [3:0] row
|
||||
);
|
||||
|
||||
wire [15:0] key_out;
|
||||
wire [15:0] key_trigger;
|
||||
wire [8*8-1:0] assic_seg;
|
||||
wire [7:0] seg_point;
|
||||
|
||||
led_display_driver #(
|
||||
.VALID_SIGNAL (VALID_SIGNAL),
|
||||
.CLK_CYCLE (CLK_CYCLE)
|
||||
)u_led_display_driver(
|
||||
.clk ( external_clk ),
|
||||
.rstn ( external_rstn ),
|
||||
.assic_seg ( assic_seg ),
|
||||
.seg_point ( seg_point ),
|
||||
.led_display_seg ( led_display_seg ),
|
||||
.led_display_sel ( led_display_sel )
|
||||
);
|
||||
|
||||
matrix_key #(
|
||||
.ROW_NUM ( 4 ),
|
||||
.COL_NUM ( 4 ),
|
||||
.DEBOUNCE_TIME ( 10000 ),
|
||||
.DELAY_TIME ( 2000 ))
|
||||
u_matrix_key(
|
||||
.clk ( external_clk ),
|
||||
.rstn ( external_rstn ),
|
||||
.row ( row ),
|
||||
.col ( col ),
|
||||
.key_out ( key_out )
|
||||
);
|
||||
|
||||
matrix_key_trigger u_matrix_key_trigger(
|
||||
.clk ( external_clk ),
|
||||
.rstn ( external_rstn),
|
||||
.key ( key_out ),
|
||||
.key_trigger ( key_trigger )
|
||||
);
|
||||
|
||||
password_lock u_password_lock(
|
||||
.clk ( external_clk ),
|
||||
.rstn ( external_rstn),
|
||||
.key_trigger ( key_trigger ),
|
||||
.assic_seg ( assic_seg ),
|
||||
.seg_point ( seg_point )
|
||||
);
|
||||
|
||||
endmodule //led_diaplay_top
|
||||
|
||||
```
|
||||
|
||||
### 1.3.3 上板验证步骤
|
||||
|
||||
---
|
||||
|
||||
可以直接将矩阵键盘,数码管的管脚约束文件中的约束复制到本次实验的管脚约束文件中。
|
||||
|
||||
将生成的sbit文件烧录好后,即可使用网页界面的虚拟按键进行使用。
|
||||
|
||||
## 1.4 章末总结
|
||||
|
||||
本章通过设计一个简易密码锁系统,综合运用了前面基础实验中学习的**矩阵键盘扫描**、**数码管显示**等知识,并引入了**有限状态机(FSM)**的设计方法,完成了一个具有较强工程实用性的综合实验。
|
||||
|
||||
通过本实验,你应该掌握了以下几点核心能力:
|
||||
|
||||
- 理解并运用 状态机进行系统流程控制;
|
||||
- 将多个功能模块(键盘、数码管、比较器)整合为一个完整系统;
|
||||
- 设计基于状态的控制逻辑,实现密码输入、校验、反馈显示等功能;
|
||||
- 理解数字电路系统中控制与数据路径的分离思想。
|
||||
|
||||
密码锁系统虽然逻辑简单,但已经具备了完整嵌入式控制系统的基本结构,是后续更复杂项目设计的重要基础。
|
||||
|
||||
## 1.5 拓展训练
|
||||
|
||||
为了进一步加深对本实验内容的理解,并锻炼系统设计与工程实现能力,你可以尝试完成以下拓展任务:
|
||||
|
||||
1. **增加防爆破机制**:限定密码错误尝试次数,例如连续三次错误后锁定一段时间,并在数码管上提示“Err”。
|
||||
2. **利用按键实现简易菜单系统**拓展状态机结构,允许通过矩阵键盘导航菜单,如“输入密码”、“查看状态”、“设置新密码”等。
|
||||
BIN
public/doc/11/images/1.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/doc/11/images/UDP.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
736
public/doc/12/doc.md
Normal file
BIN
public/doc/12/images/1.jfif
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/doc/12/images/2.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/doc/12/images/3.jpg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/doc/12/images/4.png
Normal file
|
After Width: | Height: | Size: 653 KiB |
BIN
public/doc/12/images/5.jpg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/doc/12/images/6.png
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/doc/12/images/7.png
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/doc/12/images/8.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
public/doc/13/cover.png
Normal file
|
After Width: | Height: | Size: 848 KiB |
350
public/doc/13/doc.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# 进阶-3-频率计
|
||||
|
||||
## 3.1 章节导读
|
||||
|
||||
本实验将基于实验平台设计并实现一个简易频率计,用于测量输入信号的频率值,并通过数码管进行实时显示。实验核心是掌握ADC模块的使用方法,被测信号频率的获取方法及其在数字系统中的处理流程。
|
||||
|
||||
## 3.2 理论学习
|
||||
|
||||
### 3.2.1 ADC模块
|
||||
|
||||
实验平台有一块8bit高速ADDA模块,其中ADC模块使用AD9280芯片,支持最高32MSPS的速率,模拟电压输入范围为-5~+5V,ADC模块可以根据输入电压的大小将其转换为0~255(2的8次方)的数值。模块有一个clk管脚和8个data管脚,data的输入速率和驱动时钟有关,给clk管脚的驱动时钟越快,采样率越高,data的输入速率越高。
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/1.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:30%"/>
|
||||
<br> <!--换行-->
|
||||
图1.ADDA模块示意图 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
### 3.2.2 数码管模块
|
||||
|
||||
数码管模块在前面基础实验3已经介绍过,这里不再赘述。
|
||||
|
||||
## 3.3 实战演练
|
||||
|
||||
### 3.3.1实验目标
|
||||
|
||||
能够驱动板载ADC模块,对ADC模块的输入数据进行测试,计算输入信号的频率值,并在数码管模块中显示。
|
||||
|
||||
### 3.3.2硬件资源
|
||||
|
||||
实验所需的信号源来自我们的实验平台,实验平台集成一个以FPGA为基础的dds信号发生器,该dds信号发生器可以输出频率可调的方波,正弦波,三角波,锯齿波等,用户可以在web平台使用并且改变输出波形和频率。
|
||||
|
||||
<div> <!--块级封装-->
|
||||
<center> <!--将图片和文字居中-->
|
||||
<img src="./images/2.png"
|
||||
alt="无法显示图片时显示的文字"
|
||||
style="zoom:30%"/>
|
||||
<br> <!--换行-->
|
||||
图2.ADDA实验示意图 <!--标题-->
|
||||
</center>
|
||||
</div>
|
||||
|
||||
用户接收到信号源之后需自行设计逻辑处理数据并显示。
|
||||
|
||||
### 3.3.3程序设计
|
||||
|
||||
#### pulse_gen.v
|
||||
|
||||
首先用户接收到的是8bit波形数据,要直接利用波形数据计算频率不是很方便,计算频率数据,我们只需要计算其脉冲的个数即可,所以我们设计一个模块,通过设计一个脉冲阈值trig_level,高于阈值的就计算为一次脉冲,输出一个周期的高电平方便后续模块计数,模块代码如下:
|
||||
|
||||
```verilog
|
||||
module pulse_gen(
|
||||
input rstn, //系统复位,低电平有效
|
||||
|
||||
input [7:0] trig_level,
|
||||
input ad_clk, //AD9280驱动时钟
|
||||
input [7:0] ad_data, //AD输入数据
|
||||
|
||||
output ad_pulse //输出的脉冲信号
|
||||
);
|
||||
//因为可能会有抖动,设置一个范围值避免反复触发
|
||||
parameter THR_DATA = 3;
|
||||
|
||||
//reg define
|
||||
reg pulse;
|
||||
reg pulse_delay;
|
||||
|
||||
//*****************************************************
|
||||
//** main code
|
||||
//*****************************************************
|
||||
|
||||
assign ad_pulse = pulse & pulse_delay;
|
||||
|
||||
//根据触发电平,将输入的AD采样值转换成高低电平
|
||||
always @ (posedge ad_clk or negedge rstn)begin
|
||||
if(!rstn)
|
||||
pulse <= 1'b0;
|
||||
else begin
|
||||
if((trig_level >= THR_DATA) && (ad_data < trig_level - THR_DATA))
|
||||
pulse <= 1'b0;
|
||||
else if(ad_data > trig_level + THR_DATA)
|
||||
pulse <= 1'b1;
|
||||
end
|
||||
end
|
||||
|
||||
//延时一个时钟周期,用于消除抖动
|
||||
always @ (posedge ad_clk or negedge rstn)begin
|
||||
if(!rstn)
|
||||
pulse_delay <= 1'b0;
|
||||
else
|
||||
pulse_delay <= pulse;
|
||||
end
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
#### cymometer.v
|
||||
|
||||
下面根据pulse_gen信号生成的脉冲数据进行计数,计算其频率。我们这里采用门控时钟法,用 `clk_fs`(参考时钟)作为时间基准,测量 `clk_fx`(被测信号)的频率。
|
||||
|
||||
门控时钟法的原理很简单,也就是在一个**固定时间窗内**(即门控时间 `GATE_TIME`),**计数被测时钟 clk_fx 的上升沿次数**,再结合参考时钟 `clk_fs` 的计数值,就可以算出频率:
|
||||
$$
|
||||
\text{频率} = \frac{\text{被测脉冲数量}}{\text{门控时间(秒)}} = \frac{fx\_cnt}{fs\_cnt / \text{CLK\_FS}} = \frac{\text{CLK\_FS} \times fx\_cnt}{fs\_cnt}
|
||||
$$
|
||||
|
||||
| 步骤 | 描述 |
|
||||
| ---- | ------------------------------------------------------------ |
|
||||
| **1** | 使用 `clk_fx` 作为计数时钟,控制一个门控时间 `gate` 信号 |
|
||||
| **2** | 当 `gate` 为高电平时,`fx_cnt_temp` 开始统计 `clk_fx` 的脉冲个数 |
|
||||
| **3** | 同时将 `gate` 同步到参考时钟 `clk_fs`,并计数 `fs_cnt_temp`,记录 `gate` 高电平持续期间 `clk_fs` 的个数 |
|
||||
| **4** | 一旦 `gate` 下降沿到来(通过打拍检测),将计数值冻结到 `fx_cnt` 和 `fs_cnt` 中 |
|
||||
| **5** | 最后用上述表达式计算频率输出。 |
|
||||
|
||||
代码设计如下:
|
||||
|
||||
```verilog
|
||||
module cymometer
|
||||
#(parameter CLK_FS = 26'd50_000_000) // 基准时钟频率值
|
||||
( //system clock
|
||||
input clk_fs , // 基准时钟信号
|
||||
input rstn , // 复位信号
|
||||
|
||||
//cymometer interface
|
||||
input clk_fx , // 被测时钟信号
|
||||
output reg [19:0] data_fx // 被测时钟频率输出
|
||||
);
|
||||
|
||||
//parameter define
|
||||
localparam MAX = 30; // 定义fs_cnt、fx_cnt的最大位宽
|
||||
localparam GATE_TIME = 16'd2_000; // 门控时间设置
|
||||
|
||||
//reg define
|
||||
reg gate ; // 门控信号
|
||||
reg gate_fs ; // 同步到基准时钟的门控信号
|
||||
reg gate_fs_r ; // 用于同步gate信号的寄存器
|
||||
reg gate_fs_d0 ; // 用于采集基准时钟下gate下降沿
|
||||
reg gate_fs_d1 ; //
|
||||
reg gate_fx_d0 ; // 用于采集被测时钟下gate下降沿
|
||||
reg gate_fx_d1 ; //
|
||||
reg [ 58:0] data_fx_t ; //
|
||||
reg [ 15:0] gate_cnt ; // 门控计数
|
||||
reg [MAX-1:0] fs_cnt ; // 门控时间内基准时钟的计数值
|
||||
reg [MAX-1:0] fs_cnt_temp ; // fs_cnt 临时值
|
||||
reg [MAX-1:0] fx_cnt ; // 门控时间内被测时钟的计数值
|
||||
reg [MAX-1:0] fx_cnt_temp ; // fx_cnt 临时值
|
||||
|
||||
//wire define
|
||||
wire neg_gate_fs; // 基准时钟下门控信号下降沿
|
||||
wire neg_gate_fx; // 被测时钟下门控信号下降沿
|
||||
|
||||
//*****************************************************
|
||||
//** main code
|
||||
//*****************************************************
|
||||
|
||||
//边沿检测,捕获信号下降沿
|
||||
assign neg_gate_fs = gate_fs_d1 & (~gate_fs_d0);
|
||||
assign neg_gate_fx = gate_fx_d1 & (~gate_fx_d0);
|
||||
|
||||
//门控信号计数器,使用被测时钟计数
|
||||
always @(posedge clk_fx or negedge rstn) begin
|
||||
if(!rstn)
|
||||
gate_cnt <= 16'd0;
|
||||
else if(gate_cnt == GATE_TIME + 5'd20)
|
||||
gate_cnt <= 16'd0;
|
||||
else
|
||||
gate_cnt <= gate_cnt + 1'b1;
|
||||
end
|
||||
|
||||
//门控信号,拉高时间为GATE_TIME个实测时钟周期
|
||||
always @(posedge clk_fx or negedge rstn) begin
|
||||
if(!rstn)
|
||||
gate <= 1'b0;
|
||||
else if(gate_cnt < 4'd10)
|
||||
gate <= 1'b0;
|
||||
else if(gate_cnt < GATE_TIME + 4'd10)
|
||||
gate <= 1'b1;
|
||||
else if(gate_cnt <= GATE_TIME + 5'd20)
|
||||
gate <= 1'b0;
|
||||
else
|
||||
gate <= 1'b0;
|
||||
end
|
||||
|
||||
//将门控信号同步到基准时钟下
|
||||
always @(posedge clk_fs or negedge rstn) begin
|
||||
if(!rstn) begin
|
||||
gate_fs_r <= 1'b0;
|
||||
gate_fs <= 1'b0;
|
||||
end
|
||||
else begin
|
||||
gate_fs_r <= gate;
|
||||
gate_fs <= gate_fs_r;
|
||||
end
|
||||
end
|
||||
|
||||
//打拍采门控信号的下降沿(被测时钟下)
|
||||
always @(posedge clk_fx or negedge rstn) begin
|
||||
if(!rstn) begin
|
||||
gate_fx_d0 <= 1'b0;
|
||||
gate_fx_d1 <= 1'b0;
|
||||
end
|
||||
else begin
|
||||
gate_fx_d0 <= gate;
|
||||
gate_fx_d1 <= gate_fx_d0;
|
||||
end
|
||||
end
|
||||
|
||||
//打拍采门控信号的下降沿(基准时钟下)
|
||||
always @(posedge clk_fs or negedge rstn) begin
|
||||
if(!rstn) begin
|
||||
gate_fs_d0 <= 1'b0;
|
||||
gate_fs_d1 <= 1'b0;
|
||||
end
|
||||
else begin
|
||||
gate_fs_d0 <= gate_fs;
|
||||
gate_fs_d1 <= gate_fs_d0;
|
||||
end
|
||||
end
|
||||
|
||||
//门控时间内对被测时钟计数
|
||||
always @(posedge clk_fx or negedge rstn) begin
|
||||
if(!rstn) begin
|
||||
fx_cnt_temp <= 32'd0;
|
||||
fx_cnt <= 32'd0;
|
||||
end
|
||||
else if(gate)
|
||||
fx_cnt_temp <= fx_cnt_temp + 1'b1;
|
||||
else if(neg_gate_fx) begin
|
||||
fx_cnt_temp <= 32'd0;
|
||||
fx_cnt <= fx_cnt_temp;
|
||||
end
|
||||
end
|
||||
|
||||
//门控时间内对基准时钟计数
|
||||
always @(posedge clk_fs or negedge rstn) begin
|
||||
if(!rstn) begin
|
||||
fs_cnt_temp <= 32'd0;
|
||||
fs_cnt <= 32'd0;
|
||||
end
|
||||
else if(gate_fs)
|
||||
fs_cnt_temp <= fs_cnt_temp + 1'b1;
|
||||
else if(neg_gate_fs) begin
|
||||
fs_cnt_temp <= 32'd0;
|
||||
fs_cnt <= fs_cnt_temp;
|
||||
end
|
||||
end
|
||||
|
||||
//计算被测信号频率
|
||||
always @(posedge clk_fs or negedge rstn) begin
|
||||
if(!rstn) begin
|
||||
data_fx_t <= 1'b0;
|
||||
end
|
||||
else if(gate_fs == 1'b0)
|
||||
data_fx_t <= CLK_FS * fx_cnt ;
|
||||
end
|
||||
|
||||
always @(posedge clk_fs or negedge rstn) begin
|
||||
if(!rstn) begin
|
||||
data_fx <= 20'd0;
|
||||
end
|
||||
else if(gate_fs == 1'b0)
|
||||
data_fx <= data_fx_t / fs_cnt ;
|
||||
end
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
#### frequency_meter.v
|
||||
|
||||
由于之前基础实验设计过数码管显示模块,本次实验不在赘述,但因为数码管模块是输入ascii码进行显示的,而现在输出频率数据是一个20bit的二进制数,所以我们应该先想办法将二进制转成ascii码再连接数码管模块进行显示。BCD转ascii码通过查表的方式即可完成。但二进制转BCD码的算法不是特别简单,之后会在基础实验部分讲解。
|
||||
|
||||
顶层模块代码如下:
|
||||
|
||||
```verilog
|
||||
module frequency_meter(
|
||||
input clk,
|
||||
input rstn, // 复位信号
|
||||
output ad_clk, // AD时钟
|
||||
input [7:0] ad_data, // AD输入数据
|
||||
output [7:0] led_display_seg,
|
||||
output wire [7:0] led_display_sel
|
||||
);
|
||||
wire ad_pulse;
|
||||
wire [19:0] data_fx;
|
||||
wire [25:0] bcd;
|
||||
wire [31:0] data_bcd;
|
||||
wire [63:0] asciidata;
|
||||
assign data_bcd = {6'b00,bcd};
|
||||
//生成ad驱动时钟,由于使用杜邦线连接,ad_clk不要超过10M
|
||||
PLL PLLinst(
|
||||
.clkout0(ad_clk), // output 10M
|
||||
.lock(),
|
||||
.clkin1(clk) // input
|
||||
);
|
||||
|
||||
pulse_gen pulse_gen_inst (
|
||||
.rstn(rstn),
|
||||
.trig_level(8'd128),
|
||||
.ad_clk(ad_clk),
|
||||
.ad_data(ad_data),
|
||||
.ad_pulse(ad_pulse)
|
||||
);
|
||||
|
||||
cymometer # (
|
||||
.CLK_FS(32'd27_000_000)
|
||||
)
|
||||
cymometer_inst (
|
||||
.clk_fs(clk),
|
||||
.rstn(rstn),
|
||||
.clk_fx(ad_pulse),
|
||||
.data_fx(data_fx)
|
||||
);
|
||||
//二进制转bcd码模块
|
||||
bin2bcd # (
|
||||
.W(20)
|
||||
)
|
||||
bin2bcd_inst (
|
||||
.bin(data_fx),
|
||||
.bcd(bcd)
|
||||
);
|
||||
//4位BCD码转ascii模块,例化8次使8个bcd同时输出ascii
|
||||
genvar i;
|
||||
generate
|
||||
for (i = 0; i < 8; i = i + 1) begin : generate_module
|
||||
bcd2ascii bcd2ascii_inst (
|
||||
.bcd(data_bcd[i*4 +:4]),
|
||||
.asciidata(asciidata[i*8 +: 8])
|
||||
);
|
||||
end
|
||||
endgenerate
|
||||
//数码管显示模块
|
||||
led_display_driver led_display_driver_inst (
|
||||
.clk(clk),
|
||||
.rstn(rstn),
|
||||
.assic_seg(asciidata),
|
||||
.seg_point(8'b00000000),
|
||||
.led_display_seg(led_display_seg),
|
||||
.led_display_sel(led_display_sel)
|
||||
);
|
||||
endmodule
|
||||
```
|
||||
|
||||
### 3.3.4仿真验证
|
||||
|
||||
### 3.3.5上板验证
|
||||
|
||||
## 3.4 章末总结
|
||||
BIN
public/doc/13/images/1.jpg
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
public/doc/13/images/1.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
public/doc/13/images/2.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
429
scripts/GenerateWebAPI.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
import { spawn, exec, ChildProcess } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import fetch from "node-fetch";
|
||||
import * as fs from "fs";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Windows 支持函数
|
||||
function getCommand(command: string): string {
|
||||
// dotnet 在 Windows 上不需要 .cmd 后缀
|
||||
if (command === "dotnet") {
|
||||
return "dotnet";
|
||||
}
|
||||
return process.platform === "win32" ? `${command}.cmd` : command;
|
||||
}
|
||||
|
||||
function getSpawnOptions() {
|
||||
return process.platform === "win32"
|
||||
? { stdio: "pipe", shell: true }
|
||||
: { stdio: "pipe" };
|
||||
}
|
||||
|
||||
async function waitForServer(
|
||||
url: string,
|
||||
maxRetries: number = 30,
|
||||
interval: number = 1000,
|
||||
): Promise<boolean> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
console.log("✓ Server is ready");
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
// Server not ready yet
|
||||
}
|
||||
console.log(`Waiting for server... (${i + 1}/${maxRetries})`);
|
||||
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 改进全局变量类型
|
||||
let serverProcess: ChildProcess | null = null;
|
||||
let webProcess: ChildProcess | null = null;
|
||||
|
||||
async function startWeb(): Promise<ChildProcess> {
|
||||
console.log("Starting Vite frontend...");
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn(
|
||||
getCommand("npm"),
|
||||
["run", "dev"],
|
||||
getSpawnOptions() as any,
|
||||
);
|
||||
|
||||
let webStarted = false;
|
||||
|
||||
process.stdout?.on("data", (data) => {
|
||||
const output = data.toString();
|
||||
console.log(`Web: ${output}`);
|
||||
|
||||
// 检查 Vite 是否已启动
|
||||
if (
|
||||
(output.includes("Local:") || output.includes("ready in")) &&
|
||||
!webStarted
|
||||
) {
|
||||
webStarted = true;
|
||||
resolve(process);
|
||||
}
|
||||
});
|
||||
|
||||
process.stderr?.on("data", (data) => {
|
||||
console.error(`Web Error: ${data}`);
|
||||
});
|
||||
|
||||
process.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
process.on("exit", (code, signal) => {
|
||||
console.log(`Web process exited with code ${code} and signal ${signal}`);
|
||||
if (!webStarted) {
|
||||
reject(new Error(`Web process exited unexpectedly with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
// 存储进程引用
|
||||
webProcess = process;
|
||||
|
||||
// 超时处理
|
||||
setTimeout(() => {
|
||||
if (!webStarted) {
|
||||
reject(new Error("Web server failed to start within timeout"));
|
||||
}
|
||||
}, 10000); // 10秒超时
|
||||
});
|
||||
}
|
||||
|
||||
async function startServer(): Promise<ChildProcess> {
|
||||
console.log("Starting .NET server...");
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn(
|
||||
getCommand("dotnet"),
|
||||
["run", "--property:Configuration=Release"],
|
||||
{
|
||||
cwd: "server",
|
||||
...getSpawnOptions(),
|
||||
} as any,
|
||||
);
|
||||
|
||||
let serverStarted = false;
|
||||
|
||||
process.stdout?.on("data", (data) => {
|
||||
const output = data.toString();
|
||||
console.log(`Server: ${output}`);
|
||||
|
||||
// 检查服务器是否已启动
|
||||
if (output.includes("Now listening on:") && !serverStarted) {
|
||||
serverStarted = true;
|
||||
resolve(process);
|
||||
}
|
||||
});
|
||||
|
||||
process.stderr?.on("data", (data) => {
|
||||
console.error(`Server Error: ${data}`);
|
||||
});
|
||||
|
||||
process.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
process.on("exit", (code, signal) => {
|
||||
console.log(
|
||||
`Server process exited with code ${code} and signal ${signal}`,
|
||||
);
|
||||
if (!serverStarted) {
|
||||
reject(
|
||||
new Error(`Server process exited unexpectedly with code ${code}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 存储进程引用
|
||||
serverProcess = process;
|
||||
|
||||
// 超时处理
|
||||
setTimeout(() => {
|
||||
if (!serverStarted) {
|
||||
reject(new Error("Server failed to start within timeout"));
|
||||
}
|
||||
}, 10000); // 10秒超时
|
||||
});
|
||||
}
|
||||
|
||||
async function stopServer(): Promise<void> {
|
||||
console.log("Stopping server...");
|
||||
|
||||
if (!serverProcess) {
|
||||
console.log("No server process to stop");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查进程是否还存在
|
||||
if (serverProcess.killed || serverProcess.exitCode !== null) {
|
||||
console.log("✓ Server process already terminated");
|
||||
serverProcess = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送 SIGTERM 信号
|
||||
const killed = serverProcess.kill("SIGTERM");
|
||||
if (!killed) {
|
||||
console.warn("Failed to send SIGTERM to server process");
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置超时,如果 3 秒内没有退出则强制终止
|
||||
const timeoutPromise = new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
if (
|
||||
serverProcess &&
|
||||
!serverProcess.killed &&
|
||||
serverProcess.exitCode === null
|
||||
) {
|
||||
console.log("Force killing server process...");
|
||||
serverProcess.kill("SIGKILL");
|
||||
}
|
||||
resolve();
|
||||
}, 3000); // 减少超时时间到3秒
|
||||
});
|
||||
|
||||
await Promise.race([timeoutPromise]);
|
||||
} catch (error) {
|
||||
console.warn("Warning: Could not stop server process:", error);
|
||||
} finally {
|
||||
serverProcess = null;
|
||||
|
||||
// 只有在进程可能没有正常退出时才执行清理
|
||||
// 移除自动清理逻辑,因为正常退出时不需要
|
||||
}
|
||||
}
|
||||
|
||||
async function stopWeb(): Promise<void> {
|
||||
console.log("Stopping web server...");
|
||||
|
||||
if (!webProcess) {
|
||||
console.log("No web process to stop");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查进程是否还存在
|
||||
if (webProcess.killed || webProcess.exitCode !== null) {
|
||||
console.log("✓ Web process already terminated");
|
||||
webProcess = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送 SIGTERM 信号
|
||||
const killed = webProcess.kill("SIGTERM");
|
||||
if (!killed) {
|
||||
console.warn("Failed to send SIGTERM to web process");
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置超时,如果 3 秒内没有退出则强制终止
|
||||
const timeoutPromise = new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
if (webProcess && !webProcess.killed && webProcess.exitCode === null) {
|
||||
console.log("Force killing web process...");
|
||||
webProcess.kill("SIGKILL");
|
||||
}
|
||||
resolve();
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
await Promise.race([timeoutPromise]);
|
||||
} catch (error) {
|
||||
console.warn("Warning: Could not stop web process:", error);
|
||||
} finally {
|
||||
webProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function postProcessApiClient(): Promise<void> {
|
||||
console.log("Post-processing API client...");
|
||||
try {
|
||||
const filePath = "src/APIClient.ts";
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`API client file not found: ${filePath}`);
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
let content = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// 替换 ArgumentException 中的 message 属性声明
|
||||
content = content.replace(
|
||||
/(\s+)message!:\s*string;/g,
|
||||
"$1declare message: string;",
|
||||
);
|
||||
content = content.replace(
|
||||
"{ AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelToken }",
|
||||
"{ AxiosError, type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type CancelToken }",
|
||||
);
|
||||
|
||||
// 写回文件
|
||||
fs.writeFileSync(filePath, content, "utf8");
|
||||
|
||||
console.log("✓ API client post-processing completed");
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to post-process API client: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateApiClient(): Promise<void> {
|
||||
console.log("Generating API client...");
|
||||
try {
|
||||
const url = "http://127.0.0.1:5000/GetAPIClientCode";
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch API client code: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
const code = await response.text();
|
||||
|
||||
// 写入 APIClient.ts
|
||||
const filePath = "src/APIClient.ts";
|
||||
fs.writeFileSync(filePath, code, "utf8");
|
||||
console.log("✓ API client code fetched and written successfully");
|
||||
|
||||
// 添加后处理步骤
|
||||
await postProcessApiClient();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to generate API client: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateSignalRClient(): Promise<void> {
|
||||
console.log("Generating SignalR TypeScript client...");
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(
|
||||
"dotnet tsrts --project ./server/server.csproj --output ./src/",
|
||||
);
|
||||
if (stdout) console.log(stdout);
|
||||
if (stderr) console.error(stderr);
|
||||
console.log("✓ SignalR TypeScript client generated successfully");
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to generate SignalR client: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
// Generate SignalR client
|
||||
await generateSignalRClient();
|
||||
console.log("✓ SignalR TypeScript client generated successfully");
|
||||
|
||||
// Start web frontend first
|
||||
await startWeb();
|
||||
console.log("✓ Frontend started");
|
||||
|
||||
// Wait a bit for frontend to fully initialize
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
// Start server
|
||||
await startServer();
|
||||
console.log("✓ Backend started");
|
||||
|
||||
// Wait for server to be ready (给服务器额外时间完全启动)
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// Check if swagger endpoint is available
|
||||
const serverReady = await waitForServer(
|
||||
"http://localhost:5000/swagger/v1/swagger.json",
|
||||
);
|
||||
|
||||
if (!serverReady) {
|
||||
throw new Error("Server failed to start within the expected time");
|
||||
}
|
||||
|
||||
// Generate API client
|
||||
await generateApiClient();
|
||||
|
||||
console.log("✓ API generation completed successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Error:", error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// Always try to stop processes in order: server first, then web
|
||||
await stopServer();
|
||||
await stopWeb();
|
||||
}
|
||||
}
|
||||
|
||||
// 改进的进程终止处理 - 添加防重复执行
|
||||
let isCleaningUp = false;
|
||||
|
||||
const cleanup = async (signal: string) => {
|
||||
if (isCleaningUp) {
|
||||
console.log("Cleanup already in progress, ignoring signal");
|
||||
return;
|
||||
}
|
||||
|
||||
isCleaningUp = true;
|
||||
console.log(`\nReceived ${signal}, cleaning up...`);
|
||||
|
||||
try {
|
||||
await Promise.all([stopServer(), stopWeb()]);
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
|
||||
// 立即退出,不等待
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on("SIGINT", () => cleanup("SIGINT"));
|
||||
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
||||
|
||||
// 处理未捕获的异常
|
||||
process.on("uncaughtException", async (error) => {
|
||||
if (isCleaningUp) return;
|
||||
|
||||
console.error("❌ Uncaught exception:", error);
|
||||
isCleaningUp = true;
|
||||
|
||||
try {
|
||||
await Promise.all([stopServer(), stopWeb()]);
|
||||
} catch (cleanupError) {
|
||||
console.error("Error during cleanup:", cleanupError);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", async (reason, promise) => {
|
||||
if (isCleaningUp) return;
|
||||
|
||||
console.error("❌ Unhandled rejection at:", promise, "reason:", reason);
|
||||
isCleaningUp = true;
|
||||
|
||||
try {
|
||||
await Promise.all([stopServer(), stopWeb()]);
|
||||
} catch (cleanupError) {
|
||||
console.error("Error during cleanup:", cleanupError);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
main().catch(async (error) => {
|
||||
if (isCleaningUp) return;
|
||||
|
||||
console.error("❌ Unhandled error:", error);
|
||||
isCleaningUp = true;
|
||||
|
||||
try {
|
||||
await Promise.all([stopServer(), stopWeb()]);
|
||||
} catch (cleanupError) {
|
||||
console.error("Error during cleanup:", cleanupError);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
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(Number.ToBit(0xFF, 3).Value, true);
|
||||
Assert.Equal(Number.ToBit(0x00, 3).Value, false);
|
||||
Assert.Equal(Number.ToBit(0xAA, 3).Value, true);
|
||||
Assert.Equal(Number.ToBit(0xAA, 2).Value, false);
|
||||
}
|
||||
}
|
||||
310
server.test/NumberTest.cs
Normal file
@@ -0,0 +1,310 @@
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 GetLength
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_GetLength()
|
||||
{
|
||||
Assert.Equal(5, Number.GetLength(12345));
|
||||
Assert.Equal(4, Number.GetLength(-123));
|
||||
Assert.Equal(1, Number.GetLength(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 IntPow
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Test_IntPow()
|
||||
{
|
||||
Assert.Equal(8, Number.IntPow(2, 3));
|
||||
Assert.Equal(1, Number.IntPow(5, 0));
|
||||
Assert.Equal(0, Number.IntPow(0, 5));
|
||||
Assert.Equal(7, Number.IntPow(7, 1));
|
||||
Assert.Equal(81, Number.IntPow(3, 4));
|
||||
}
|
||||
}
|
||||
99
server.test/ProgressTrackerTest.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Moq;
|
||||
using server.Hubs;
|
||||
using server.Services;
|
||||
|
||||
public class ProgressTrackerTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task Test_ProgressReporter_Basic()
|
||||
{
|
||||
int reportedValue = -1;
|
||||
var reporter = new ProgressReporter(async v => { reportedValue = v; await Task.CompletedTask; }, 0, 100, 10);
|
||||
|
||||
// Report
|
||||
reporter.Report(50);
|
||||
Assert.Equal(50, reporter.Progress);
|
||||
Assert.Equal(ProgressStatus.InProgress, reporter.Status);
|
||||
Assert.Equal(50, reportedValue);
|
||||
|
||||
// Increase by step
|
||||
reporter.Increase();
|
||||
Assert.Equal(60, reporter.Progress);
|
||||
|
||||
// Increase by value
|
||||
reporter.Increase(20);
|
||||
Assert.Equal(80, reporter.Progress);
|
||||
|
||||
// Finish
|
||||
reporter.Finish();
|
||||
Assert.Equal(ProgressStatus.Completed, reporter.Status);
|
||||
Assert.Equal(100, reporter.Progress);
|
||||
|
||||
// Cancel
|
||||
reporter = new ProgressReporter(async v => { reportedValue = v; await Task.CompletedTask; }, 0, 100, 10);
|
||||
reporter.Cancel();
|
||||
Assert.Equal(ProgressStatus.Canceled, reporter.Status);
|
||||
Assert.Equal("User Cancelled", reporter.ErrorMessage);
|
||||
|
||||
// Error
|
||||
reporter = new ProgressReporter(async v => { reportedValue = v; await Task.CompletedTask; }, 0, 100, 10);
|
||||
reporter.Error("Test Error");
|
||||
Assert.Equal(ProgressStatus.Failed, reporter.Status);
|
||||
Assert.Equal("Test Error", reporter.ErrorMessage);
|
||||
|
||||
// CreateChild
|
||||
var parent = new ProgressReporter(async v => { await Task.CompletedTask; }, 10, 100, 5);
|
||||
var child = parent.CreateChild(50, 5);
|
||||
Assert.Equal(ProgressStatus.Pending, child.Status);
|
||||
Assert.NotNull(child);
|
||||
|
||||
// Child Increase
|
||||
child.Increase();
|
||||
Assert.Equal(ProgressStatus.InProgress, child.Status);
|
||||
Assert.Equal(20, child.ProgressPercent);
|
||||
Assert.Equal(20, parent.Progress);
|
||||
|
||||
// Child Complete
|
||||
child.Finish();
|
||||
Assert.Equal(ProgressStatus.Completed, child.Status);
|
||||
Assert.Equal(100, child.ProgressPercent);
|
||||
Assert.Equal(60, parent.Progress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Test_ProgressTrackerService_Basic()
|
||||
{
|
||||
// Mock SignalR HubContext
|
||||
var mockHubContext = new Mock<IHubContext<ProgressHub, IProgressReceiver>>();
|
||||
var service = new ProgressTrackerService(mockHubContext.Object);
|
||||
|
||||
// CreateTask
|
||||
var (taskId, reporter) = service.CreateTask();
|
||||
Assert.NotNull(taskId);
|
||||
Assert.NotNull(reporter);
|
||||
|
||||
// GetReporter
|
||||
var optReporter = service.GetReporter(taskId);
|
||||
Assert.True(optReporter.HasValue);
|
||||
Assert.Equal(reporter, optReporter.Value);
|
||||
|
||||
// GetProgressStatus
|
||||
var optStatus = service.GetProgressStatus(taskId);
|
||||
Assert.True(optStatus.HasValue);
|
||||
Assert.Equal(ProgressStatus.Pending, optStatus.Value);
|
||||
|
||||
// BindTask
|
||||
var bindResult = service.BindTask(taskId, "conn1");
|
||||
Assert.True(bindResult);
|
||||
|
||||
// CancelTask
|
||||
var cancelResult = service.CancelTask(taskId);
|
||||
Assert.True(cancelResult);
|
||||
|
||||
// After cancel, status should be Cancelled
|
||||
var optStatus2 = service.GetProgressStatus(taskId);
|
||||
Assert.True(optStatus2.HasValue);
|
||||
Assert.Equal(ProgressStatus.Canceled, optStatus2.Value);
|
||||
}
|
||||
}
|
||||
@@ -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.BytesToNumber(data.Data));
|
||||
}
|
||||
}
|
||||
|
||||
[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.BytesToNumber(data.ToBytes()));
|
||||
}
|
||||
}
|
||||
|
||||
[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(number, Number.BytesToNumber(data.ToBytes()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
1
server/.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
obj
|
||||
bin
|
||||
bitstream
|
||||
bsdl
|
||||
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using NLog.Web;
|
||||
using NSwag;
|
||||
using NSwag.CodeGeneration.TypeScript;
|
||||
using NSwag.Generation.Processors.Security;
|
||||
using server.Services;
|
||||
using TypedSignalR.Client.DevTools;
|
||||
|
||||
// Early init of NLog to allow startup and exception logging, before host is built
|
||||
var logger = NLog.LogManager.Setup()
|
||||
@@ -9,7 +19,6 @@ var logger = NLog.LogManager.Setup()
|
||||
.GetCurrentClassLogger();
|
||||
logger.Debug("Init Main...");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -36,21 +45,69 @@ try
|
||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
});
|
||||
|
||||
// Add CORS policy
|
||||
builder.Services.AddCors(options =>
|
||||
// Add JWT Token Authorization
|
||||
builder.Services
|
||||
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.AddPolicy("Development", policy =>
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
policy
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
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())
|
||||
{
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("Development", policy => policy
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
);
|
||||
});
|
||||
}
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("Users", policy => policy
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
);
|
||||
options.AddPolicy("SignalR", policy => policy
|
||||
.WithOrigins("http://localhost:5173")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials()
|
||||
);
|
||||
});
|
||||
|
||||
// Use SignalR
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
// Add Swagger
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddOpenApiDocument(options =>
|
||||
builder.Services.AddSwaggerDocument(options =>
|
||||
{
|
||||
options.PostProcess = document =>
|
||||
{
|
||||
@@ -72,39 +129,133 @@ try
|
||||
// }
|
||||
};
|
||||
};
|
||||
|
||||
// Authorization
|
||||
options.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme
|
||||
{
|
||||
Description = "请输入token,格式为 Bearer xxxxxxxx(注意中间必须有空格)",
|
||||
Name = "Authorization",
|
||||
In = NSwag.OpenApiSecurityApiKeyLocation.Header,
|
||||
Type = NSwag.OpenApiSecuritySchemeType.ApiKey,
|
||||
});
|
||||
options.OperationProcessors.Add(new OperationSecurityScopeProcessor("Bearer"));
|
||||
});
|
||||
|
||||
|
||||
// 添加 HTTP 视频流服务
|
||||
builder.Services.AddSingleton<HttpVideoStreamService>();
|
||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpVideoStreamService>());
|
||||
builder.Services.AddSingleton<HttpHdmiVideoStreamService>();
|
||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpHdmiVideoStreamService>());
|
||||
|
||||
// 添加进度跟踪服务
|
||||
builder.Services.AddSingleton<ProgressTrackerService>();
|
||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<ProgressTrackerService>());
|
||||
|
||||
// Application Settings
|
||||
var app = builder.Build();
|
||||
// Configure the HTTP request pipeline.
|
||||
// app.UseExceptionHandler(new ExceptionHandlerOptions()
|
||||
// {
|
||||
// AllowStatusCode404Response = true,
|
||||
// ExceptionHandlingPath = "/error"
|
||||
// });
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
// app.UseExceptionHandler("/Home/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
|
||||
// Serve static files
|
||||
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")))
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), "log"));
|
||||
}
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// if (app.Environment.IsDevelopment())
|
||||
// {
|
||||
app.UseOpenApi();
|
||||
// Swagger
|
||||
app.UseOpenApi(settings =>
|
||||
{
|
||||
settings.PostProcess = (document, httpRequest) =>
|
||||
{
|
||||
document.Servers.Clear();
|
||||
document.Servers.Add(new NSwag.OpenApiServer { Url = $"http://{Global.localhost}:5000" });
|
||||
};
|
||||
});
|
||||
app.UseSwaggerUi();
|
||||
// }
|
||||
|
||||
// SignalR
|
||||
app.UseWebSockets();
|
||||
app.UseSignalRHubSpecification();
|
||||
app.UseSignalRHubDevelopmentUI();
|
||||
|
||||
// Router
|
||||
app.MapControllers();
|
||||
app.MapHub<server.Hubs.JtagHub>("hubs/JtagHub");
|
||||
app.MapHub<server.Hubs.ProgressHub>("hubs/ProgressHub");
|
||||
|
||||
// Setup Program
|
||||
MsgBus.Init();
|
||||
|
||||
// Router
|
||||
// API Get
|
||||
app.MapGet("/", () => Results.Redirect("/swagger"));
|
||||
app.MapControllers();
|
||||
// Generate API Client
|
||||
app.MapGet("GetAPIClientCode", async (HttpContext context) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var document = await OpenApiDocument.FromUrlAsync($"http://{Global.localhost}:5000/swagger/v1/swagger.json");
|
||||
|
||||
app.Run("http://localhost:5000");
|
||||
var settings = new TypeScriptClientGeneratorSettings
|
||||
{
|
||||
ClassName = "{controller}Client",
|
||||
UseAbortSignal = false,
|
||||
Template = TypeScriptTemplate.Axios,
|
||||
TypeScriptGeneratorSettings = {
|
||||
},
|
||||
};
|
||||
|
||||
var generator = new TypeScriptClientGenerator(document, settings);
|
||||
var code = generator.GenerateFile();
|
||||
|
||||
return Results.Text(code, "text/plain; charset=utf-8", Encoding.UTF8);
|
||||
}
|
||||
catch (Exception err)
|
||||
{
|
||||
logger.Error(err);
|
||||
return Results.Problem(err.ToString());
|
||||
}
|
||||
}).RequireCors("Development");
|
||||
|
||||
app.Run();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -123,4 +274,3 @@ finally
|
||||
// Close Program
|
||||
MsgBus.Exit();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,18 +5,20 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5188",
|
||||
"applicationUrl": "http://0.0.0.0:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7070;http://localhost:5188",
|
||||
"applicationUrl": "https://0.0.0.0:7278;http://0.0.0.0:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5124
server/boundary_registers.json
Normal file
@@ -1,5 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Import Project="PublishAllRids.targets" />
|
||||
<Import Project="PublishAllRids.xml" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
@@ -8,17 +8,40 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<RuntimeIdentifiers>win-x64;linux-x64;</RuntimeIdentifiers>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SpaRoot>../</SpaRoot>
|
||||
<SpaProxyServerUrl>http://localhost:5173</SpaProxyServerUrl>
|
||||
<SpaProxyLaunchCommand>npm run dev</SpaProxyLaunchCommand>
|
||||
</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" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<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" />
|
||||
<PackageReference Include="Tapper.Analyzer" Version="1.13.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="TypedSignalR.Client.DevTools" Version="1.2.4" />
|
||||
<PackageReference Include="TypedSignalR.Client.TypeScript.Analyzer" Version="1.15.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="TypedSignalR.Client.TypeScript.Attributes" Version="1.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
634
server/src/ArpClient.cs
Normal file
@@ -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; }
|
||||
}
|
||||
193
server/src/BsdlParser.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using DotNext;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BsdlParser;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public class BoundaryScanRegs
|
||||
{
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public class CellEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[JsonProperty("cell_number")]
|
||||
[JsonRequired]
|
||||
public int CellNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[JsonProperty("cell_name")]
|
||||
[JsonRequired]
|
||||
public string CellName { get; set; } = "UnknownCellName";
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[JsonProperty("port_id")]
|
||||
public string? PortID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[JsonProperty("function")]
|
||||
[JsonRequired]
|
||||
public string? Function { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[JsonProperty("safe_bit")]
|
||||
[JsonRequired]
|
||||
public string? SafeBit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[JsonProperty("ccell")]
|
||||
public string? CCell { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[JsonProperty("disabel_value")]
|
||||
public string? DisableValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[JsonProperty("disabel_result")]
|
||||
public string? DisableResult { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[JsonProperty("register_length")]
|
||||
[JsonRequired]
|
||||
public int RegisterLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[JsonProperty("registers")]
|
||||
[JsonRequired]
|
||||
public CellEntry[] Registers { get; set; } = new CellEntry[] { };
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public class Parser
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private const string BOUNDARY_REGS_DESP = "boundary_registers.json";
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public JObject BoundaryRegsDesp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public Parser()
|
||||
{
|
||||
var filePath = Path.Combine(Environment.CurrentDirectory, BOUNDARY_REGS_DESP);
|
||||
if (!Path.Exists(filePath))
|
||||
throw new Exception($"Counld not find boundary_registers.json in {filePath}");
|
||||
|
||||
this.BoundaryRegsDesp = JObject.Parse(File.ReadAllText(filePath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
public Optional<int> GetBoundaryRegsNum()
|
||||
{
|
||||
var ret = this.BoundaryRegsDesp["register_length"];
|
||||
if (ret is null) return new();
|
||||
return Convert.ToInt32(ret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public Optional<List<BoundaryScanRegs.CellEntry>> GetBoundaryPorts()
|
||||
{
|
||||
var registers = this.BoundaryRegsDesp["registers"]?.ToList();
|
||||
if (registers is null) return new();
|
||||
|
||||
var cellList = new List<BoundaryScanRegs.CellEntry>();
|
||||
foreach (var item in registers)
|
||||
{
|
||||
var cell = item.ToObject<BoundaryScanRegs.CellEntry>();
|
||||
if (cell is null) return new();
|
||||
cellList.Add(cell);
|
||||
}
|
||||
|
||||
return cellList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
public Optional<List<BoundaryScanRegs.CellEntry>> GetBoundaryLogicalPorts()
|
||||
{
|
||||
var registers = this.BoundaryRegsDesp["registers"]?.ToList().Where((item) =>
|
||||
{
|
||||
return item["port_id"] is not null;
|
||||
});
|
||||
if (registers is null) return new();
|
||||
|
||||
var cellList = new List<BoundaryScanRegs.CellEntry>();
|
||||
foreach (var item in registers)
|
||||
{
|
||||
var cell = item.ToObject<BoundaryScanRegs.CellEntry>();
|
||||
if (cell is null) return new();
|
||||
cellList.Add(cell);
|
||||
}
|
||||
|
||||
return cellList;
|
||||
}
|
||||
|
||||
// public Result<string> GetLogicalPorts()
|
||||
// {
|
||||
// using (Py.GIL())
|
||||
// {
|
||||
// using (PyModule scope = Py.CreateScope())
|
||||
// {
|
||||
// string code = $@"
|
||||
// bsdl_parser = BsdlParser({this.filePath})
|
||||
// result = json.dumps(bsdl_parser.GetLogicPortDesp(), indent=2)
|
||||
// ";
|
||||
//
|
||||
// var localVariables = new PyDict();
|
||||
// scope.Exec(code, localVariables);
|
||||
// if (!localVariables.HasKey("result"))
|
||||
// return new(new Exception($"PythonNet doesn't has result from dict: {localVariables}"));
|
||||
//
|
||||
// var result = localVariables.GetItem("result");
|
||||
// if (result is null)
|
||||
// return new(new Exception($"PythonNet get null from dict: {localVariables}"));
|
||||
//
|
||||
// var resultString = result.ToString();
|
||||
// if (resultString is null)
|
||||
// return new(new Exception($"Pythonnet convert PyObject to string failed :{result}"));
|
||||
// return resultString;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
using DotNext;
|
||||
|
||||
namespace Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 数字处理工具
|
||||
/// </summary>
|
||||
public class Number
|
||||
{
|
||||
/// <summary>
|
||||
/// 整数转成二进制字节数组
|
||||
/// </summary>
|
||||
/// <param name="num">整数</param>
|
||||
/// <param name="length">整数长度</param>
|
||||
/// <param name="isRightHigh">是否高位在右边</param>
|
||||
/// <returns>二进制字节数组</returns>
|
||||
public static Result<byte[]> NumberToBytes(ulong num, uint length, bool isRightHigh = 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 (isRightHigh)
|
||||
{
|
||||
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>
|
||||
/// 二进制字节数组转成整数
|
||||
/// </summary>
|
||||
/// <param name="bytes">二进制字节数组</param>
|
||||
/// <param name="isRightHigh">是否高位在右边</param>
|
||||
/// <returns>整数</returns>
|
||||
public static Result<ulong> BytesToNumber(byte[] bytes, bool isRightHigh = false)
|
||||
{
|
||||
if (bytes.Length > 8)
|
||||
{
|
||||
return new(new ArgumentException(
|
||||
"Unsigned long number can't over 8 bytes(64 bits).",
|
||||
nameof(bytes)
|
||||
));
|
||||
}
|
||||
|
||||
ulong num = 0;
|
||||
int len = bytes.Length;
|
||||
|
||||
if (isRightHigh)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
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>
|
||||
/// 字符串转二进制字节数组
|
||||
/// </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 2 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>
|
||||
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
@@ -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
@@ -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);
|
||||
384
server/src/Common/Number.cs
Normal file
@@ -0,0 +1,384 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取数字的长度
|
||||
/// </summary>
|
||||
/// <param name="number">数字</param>
|
||||
/// <returns>数字的长度</returns>
|
||||
public static int GetLength(int number)
|
||||
{
|
||||
// 将整数转换为字符串
|
||||
string numberString = number.ToString();
|
||||
|
||||
// 返回字符串的长度
|
||||
return numberString.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算整形的幂
|
||||
/// </summary>
|
||||
/// <param name="x">底数</param>
|
||||
/// <param name="pow">幂</param>
|
||||
/// <returns>计算结果</returns>
|
||||
public static int IntPow(int x, int pow)
|
||||
{
|
||||
int ret = 1;
|
||||
while (pow != 0)
|
||||
{
|
||||
if ((pow & 1) == 1)
|
||||
ret *= x;
|
||||
x *= x;
|
||||
pow >>= 1;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
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
@@ -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,365 +0,0 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using Common;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using WebProtocol;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// UDP API
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class UDPController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private const string LOCALHOST = "127.0.0.1";
|
||||
|
||||
/// <summary>
|
||||
/// 页面
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public string Index()
|
||||
{
|
||||
return "This is UDP Controller";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送字符串
|
||||
/// </summary>
|
||||
/// <param name="address">IPV4 或者 IPV6 地址</param>
|
||||
/// <param name="port">设备端口号</param>
|
||||
/// <param name="text">发送的文本</param>
|
||||
/// <response code="200">发送成功</response>
|
||||
/// <response code="500">发送失败</response>
|
||||
[HttpPost("SendString")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendString(string address = LOCALHOST, int port = 1234, string text = "Hello Server!")
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendStringAsync(endPoint, [text]);
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送二进制数据
|
||||
/// </summary>
|
||||
/// <param name="address" example="127.0.0.1">IPV4 或者 IPV6 地址</param>
|
||||
/// <param name="port" example="1234">设备端口号</param>
|
||||
/// <param name="bytes" example="FFFFAAAA">16进制文本</param>
|
||||
[HttpPost("SendBytes")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendBytes(string address, int port, string bytes)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendBytesAsync(endPoint, Number.StringToBytes(bytes));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 发送地址包
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
/// <param name="port">UDP 端口号</param>
|
||||
/// <param name="opts">地址包选项</param>
|
||||
[HttpPost("SendAddrPackage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendAddrPackage(
|
||||
string address,
|
||||
int port,
|
||||
[FromBody] SendAddrPackOptions opts)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendAddrPackAsync(endPoint, new WebProtocol.SendAddrPackage(opts));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送数据包
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
/// <param name="port">UDP 端口号</param>
|
||||
/// <param name="data">16进制数据</param>
|
||||
[HttpPost("SendDataPackage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SendDataPackage(string address, int port, string data)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendDataPackAsync(endPoint,
|
||||
new WebProtocol.SendDataPackage(Number.StringToBytes(data)));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定IP地址接受的数据列表
|
||||
/// </summary>
|
||||
/// <param name="address">IP地址</param>
|
||||
[HttpGet("GetRecvDataArray")]
|
||||
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> GetRecvDataArray(string address)
|
||||
{
|
||||
var ret = await MsgBus.UDPServer.GetDataArrayAsync(address);
|
||||
|
||||
if (ret.HasValue)
|
||||
{
|
||||
var dataJson = JsonConvert.SerializeObject(ret.Value);
|
||||
logger.Debug($"Get Receive Successfully: {dataJson}");
|
||||
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Debug("Get Receive Failed");
|
||||
return TypedResults.InternalServerError();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Jtag API
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class JtagController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 页面
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public string Index()
|
||||
{
|
||||
return "This is Jtag Controller";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行一个Jtag命令
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
/// <param name="hexDevAddr"> 16进制设备目的地址(Jtag) </param>
|
||||
/// <param name="hexCmd"> 16进制命令 </param>
|
||||
[HttpPost("RunCommand")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> RunCommand(string address, int port, string hexDevAddr, string hexCmd)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.WriteFIFO(Convert.ToUInt32(hexDevAddr, 16), Convert.ToUInt32(hexCmd, 16));
|
||||
|
||||
if (ret.IsSuccessful) { return TypedResults.Ok(ret.Value); }
|
||||
else { return TypedResults.InternalServerError(ret.Error); }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取Jtag ID Code
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
[HttpGet("GetDeviceIDCode")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> GetDeviceIDCode(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadIDCode();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Get device {address} ID code: 0x{ret.Value:X4}");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取状态寄存器
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
[HttpGet("ReadStatusReg")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> ReadStatusReg(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadStatusReg();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
|
||||
var decodeValue = new JtagClient.JtagStatusReg(ret.Value);
|
||||
logger.Info($"Read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
original = ret.Value,
|
||||
binaryValue,
|
||||
decodeValue,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="file">比特流文件</param>
|
||||
[HttpPost("UploadBitstream")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
return TypedResults.BadRequest("未选择文件");
|
||||
|
||||
// 生成安全的文件名(避免路径遍历攻击)
|
||||
var fileName = Path.GetRandomFileName();
|
||||
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"bitstream/{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("Bitstream Upload Successfully");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过Jtag下载比特流文件
|
||||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
[HttpPost("DownloadBitstream")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> DownloadBitstream(string address, int port)
|
||||
{
|
||||
// 检查文件
|
||||
var fileDir = Path.Combine(Environment.CurrentDirectory, $"bitstream/{address}");
|
||||
if (!Directory.Exists(fileDir))
|
||||
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
|
||||
|
||||
try
|
||||
{
|
||||
// 读取文件
|
||||
var filePath = Directory.GetFiles(fileDir)[0];
|
||||
|
||||
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
|
||||
{
|
||||
if (fileStream is null || fileStream.Length <= 0)
|
||||
return TypedResults.BadRequest("Wrong bitstream, Please upload it again");
|
||||
|
||||
// 定义缓冲区大小: 32KB
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
byte[] revBuffer = new byte[32 * 1024];
|
||||
long totalBytesRead = 0;
|
||||
|
||||
// 使用异步流读取文件
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
int bytesRead;
|
||||
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
// 反转 32bits
|
||||
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
|
||||
if (!retBuffer.IsSuccessful)
|
||||
return TypedResults.InternalServerError(retBuffer.Error);
|
||||
revBuffer = retBuffer.Value;
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
revBuffer[i] = BinaryPrimitives.ReverseEndianness(revBuffer[i]);
|
||||
}
|
||||
|
||||
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
|
||||
totalBytesRead += bytesRead;
|
||||
}
|
||||
|
||||
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
|
||||
var fileBytes = memoryStream.ToArray();
|
||||
|
||||
// 下载比特流
|
||||
var jtagCtrl = new JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.DownloadBitstream(fileBytes);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} dowload bitstream successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
return TypedResults.InternalServerError(error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 日志控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class Log : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 日志文件路径
|
||||
/// </summary>
|
||||
private readonly string _logFilePath = Directory.GetFiles(Directory.GetCurrentDirectory())[0];
|
||||
|
||||
}
|
||||
|
||||
30
server/src/Controllers/BsdlParserController.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class BsdlParserController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[EnableCors("Development")]
|
||||
[HttpGet("GetBoundaryLogicalPorts")]
|
||||
public IResult GetBoundaryLogicalPorts()
|
||||
{
|
||||
var parser = new BsdlParser.Parser();
|
||||
var ret = parser.GetBoundaryLogicalPorts();
|
||||
if (ret.IsNull) return TypedResults.InternalServerError("Get Null");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
108
server/src/Controllers/DDSController.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DDSController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="channelNum">[TODO:parameter]</param>
|
||||
/// <param name="waveNum">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetWaveNum")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetWaveNum(string address, int port, int channelNum, int waveNum)
|
||||
{
|
||||
var dds = new Peripherals.DDSClient.DDS(address, port);
|
||||
|
||||
var ret = await dds.SetWaveNum(channelNum, waveNum);
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} set output wave num successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="channelNum">[TODO:parameter]</param>
|
||||
/// <param name="waveNum">[TODO:parameter]</param>
|
||||
/// <param name="step">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetFreq")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetFreq(string address, int port, int channelNum, int waveNum, UInt32 step)
|
||||
{
|
||||
var dds = new Peripherals.DDSClient.DDS(address, port);
|
||||
|
||||
var ret = await dds.SetFreq(channelNum, waveNum, step);
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} set output freqency successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [TODO:description]
|
||||
/// </summary>
|
||||
/// <param name="address">[TODO:parameter]</param>
|
||||
/// <param name="port">[TODO:parameter]</param>
|
||||
/// <param name="channelNum">[TODO:parameter]</param>
|
||||
/// <param name="waveNum">[TODO:parameter]</param>
|
||||
/// <param name="phase">[TODO:parameter]</param>
|
||||
/// <returns>[TODO:return]</returns>
|
||||
[HttpPost("SetPhase")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetPhase(string address, int port, int channelNum, int waveNum, int phase)
|
||||
{
|
||||
var dds = new Peripherals.DDSClient.DDS(address, port);
|
||||
|
||||
var ret = await dds.SetPhase(channelNum, waveNum, phase);
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Device {address} set output phase successfully");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
492
server/src/Controllers/DataController.cs
Normal file
@@ -0,0 +1,492 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 数据控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
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>
|
||||
public class UserInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户的唯一标识符
|
||||
/// </summary>
|
||||
public Guid ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户的名称
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户的电子邮箱
|
||||
/// </summary>
|
||||
public required string EMail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户关联的板卡ID
|
||||
/// </summary>
|
||||
public Guid BoardID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户绑定板子的过期时间
|
||||
/// </summary>
|
||||
public DateTime? BoardExpireTime { get; set; }
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 用户登录,获取 JWT 令牌
|
||||
/// </summary>
|
||||
/// <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.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">用户名(不超过255个字符)</param>
|
||||
/// <param name="email">邮箱地址</param>
|
||||
/// <param name="password">用户密码</param>
|
||||
/// <returns>操作结果,成功返回 true,失败返回错误信息</returns>
|
||||
[HttpPost("SignUpUser")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult SignUpUser(string name, string email, string password)
|
||||
{
|
||||
// 验证输入参数
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return BadRequest("用户名不能为空");
|
||||
|
||||
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
@@ -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, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
291
server/src/Controllers/ExamController.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using DotNext;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 实验控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ExamController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 实验信息类
|
||||
/// </summary>
|
||||
public class ExamInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 实验的唯一标识符
|
||||
/// </summary>
|
||||
public required string ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验名称
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验描述
|
||||
/// </summary>
|
||||
public required string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验最后更新时间
|
||||
/// </summary>
|
||||
public DateTime UpdatedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验标签
|
||||
/// </summary>
|
||||
public string[] Tags { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 实验难度(1-5)
|
||||
/// </summary>
|
||||
public int Difficulty { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 普通用户是否可见
|
||||
/// </summary>
|
||||
public bool IsVisibleToUsers { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实验简要信息类(用于列表显示)
|
||||
/// </summary>
|
||||
public class ExamSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// 实验的唯一标识符
|
||||
/// </summary>
|
||||
public required string ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验名称
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验最后更新时间
|
||||
/// </summary>
|
||||
public DateTime UpdatedTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验标签
|
||||
/// </summary>
|
||||
public string[] Tags { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 实验难度(1-5)
|
||||
/// </summary>
|
||||
public int Difficulty { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 普通用户是否可见
|
||||
/// </summary>
|
||||
public bool IsVisibleToUsers { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建实验请求类
|
||||
/// </summary>
|
||||
public class CreateExamRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 实验ID
|
||||
/// </summary>
|
||||
public required string ID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验名称
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验描述
|
||||
/// </summary>
|
||||
public required string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实验标签
|
||||
/// </summary>
|
||||
public string[] Tags { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 实验难度(1-5)
|
||||
/// </summary>
|
||||
public int Difficulty { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 普通用户是否可见
|
||||
/// </summary>
|
||||
public bool IsVisibleToUsers { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有实验列表
|
||||
/// </summary>
|
||||
/// <returns>实验列表</returns>
|
||||
[Authorize]
|
||||
[HttpGet("list")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(ExamSummary[]), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult GetExamList()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var exams = db.GetAllExams();
|
||||
|
||||
var examSummaries = exams.Select(exam => new ExamSummary
|
||||
{
|
||||
ID = exam.ID,
|
||||
Name = exam.Name,
|
||||
CreatedTime = exam.CreatedTime,
|
||||
UpdatedTime = exam.UpdatedTime,
|
||||
Tags = exam.GetTagsList(),
|
||||
Difficulty = exam.Difficulty,
|
||||
IsVisibleToUsers = exam.IsVisibleToUsers
|
||||
}).ToArray();
|
||||
|
||||
logger.Info($"成功获取实验列表,共 {examSummaries.Length} 个实验");
|
||||
return Ok(examSummaries);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"获取实验列表时出错: {ex.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验列表失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据实验ID获取实验详细信息
|
||||
/// </summary>
|
||||
/// <param name="examId">实验ID</param>
|
||||
/// <returns>实验详细信息</returns>
|
||||
[Authorize]
|
||||
[HttpGet("{examId}")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(ExamInfo), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult GetExam(string examId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(examId))
|
||||
return BadRequest("实验ID不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var result = db.GetExamByID(examId);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"获取实验时出错: {result.Error.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {result.Error.Message}");
|
||||
}
|
||||
|
||||
if (!result.Value.HasValue)
|
||||
{
|
||||
logger.Warn($"实验不存在: {examId}");
|
||||
return NotFound($"实验 {examId} 不存在");
|
||||
}
|
||||
|
||||
var exam = result.Value.Value;
|
||||
var examInfo = new ExamInfo
|
||||
{
|
||||
ID = exam.ID,
|
||||
Name = exam.Name,
|
||||
Description = exam.Description,
|
||||
CreatedTime = exam.CreatedTime,
|
||||
UpdatedTime = exam.UpdatedTime,
|
||||
Tags = exam.GetTagsList(),
|
||||
Difficulty = exam.Difficulty,
|
||||
IsVisibleToUsers = exam.IsVisibleToUsers
|
||||
};
|
||||
|
||||
logger.Info($"成功获取实验信息: {examId}");
|
||||
return Ok(examInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"获取实验 {examId} 时出错: {ex.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建新实验
|
||||
/// </summary>
|
||||
/// <param name="request">创建实验请求</param>
|
||||
/// <returns>创建结果</returns>
|
||||
[Authorize("Admin")]
|
||||
[HttpPost]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(ExamInfo), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult CreateExam([FromBody] CreateExamRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.ID) || string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Description))
|
||||
return BadRequest("实验ID、名称和描述不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
using var db = new Database.AppDataConnection();
|
||||
var result = db.CreateExam(request.ID, request.Name, request.Description, request.Tags, request.Difficulty, request.IsVisibleToUsers);
|
||||
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
if (result.Error.Message.Contains("已存在"))
|
||||
return Conflict(result.Error.Message);
|
||||
|
||||
logger.Error($"创建实验时出错: {result.Error.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {result.Error.Message}");
|
||||
}
|
||||
|
||||
var exam = result.Value;
|
||||
var examInfo = new ExamInfo
|
||||
{
|
||||
ID = exam.ID,
|
||||
Name = exam.Name,
|
||||
Description = exam.Description,
|
||||
CreatedTime = exam.CreatedTime,
|
||||
UpdatedTime = exam.UpdatedTime,
|
||||
Tags = exam.GetTagsList(),
|
||||
Difficulty = exam.Difficulty,
|
||||
IsVisibleToUsers = exam.IsVisibleToUsers
|
||||
};
|
||||
|
||||
logger.Info($"成功创建实验: {request.ID}");
|
||||
return CreatedAtAction(nameof(GetExam), new { examId = request.ID }, examInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"创建实验 {request.ID} 时出错: {ex.Message}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
97
server/src/Controllers/HdmiVideoStreamController.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Security.Claims;
|
||||
using server.Services;
|
||||
using Database;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[EnableCors("Users")]
|
||||
public class HdmiVideoStreamController : ControllerBase
|
||||
{
|
||||
private readonly HttpHdmiVideoStreamService _videoStreamService;
|
||||
private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
public HdmiVideoStreamController(HttpHdmiVideoStreamService videoStreamService)
|
||||
{
|
||||
_videoStreamService = videoStreamService;
|
||||
}
|
||||
|
||||
// 管理员获取所有板子的 endpoints
|
||||
[HttpGet("AllEndpoints")]
|
||||
[Authorize("Admin")]
|
||||
public ActionResult<List<HdmiVideoStreamEndpoint>> GetAllEndpoints()
|
||||
{
|
||||
var endpoints = _videoStreamService.GetAllVideoEndpoints();
|
||||
if (endpoints == null)
|
||||
return NotFound("No boards found.");
|
||||
return Ok(endpoints);
|
||||
}
|
||||
|
||||
// 用户获取自己板子的 endpoint
|
||||
[HttpGet("MyEndpoint")]
|
||||
[Authorize]
|
||||
public ActionResult<HdmiVideoStreamEndpoint> GetMyEndpoint()
|
||||
{
|
||||
var userName = User.FindFirstValue(ClaimTypes.Name);
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
return Unauthorized("User name not found in claims.");
|
||||
|
||||
var db = new AppDataConnection();
|
||||
if (db == null)
|
||||
return NotFound("Database connection failed.");
|
||||
|
||||
var userRet = db.GetUserByName(userName);
|
||||
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
|
||||
return NotFound("User not found.");
|
||||
|
||||
var user = userRet.Value.Value;
|
||||
var boardId = user.BoardID;
|
||||
if (boardId == Guid.Empty)
|
||||
return NotFound("No board bound to this user.");
|
||||
|
||||
var boardRet = db.GetBoardByID(boardId);
|
||||
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
|
||||
return NotFound("Board not found.");
|
||||
|
||||
var endpoint = _videoStreamService.GetVideoEndpoint(boardId.ToString());
|
||||
return Ok(endpoint);
|
||||
}
|
||||
|
||||
// 禁用指定板子的 HDMI 传输
|
||||
[HttpPost("DisableHdmiTransmission")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> DisableHdmiTransmission()
|
||||
{
|
||||
var userName = User.FindFirstValue(ClaimTypes.Name);
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
return Unauthorized("User name not found in claims.");
|
||||
|
||||
var db = new AppDataConnection();
|
||||
if (db == null)
|
||||
return NotFound("Database connection failed.");
|
||||
|
||||
var userRet = db.GetUserByName(userName);
|
||||
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
|
||||
return NotFound("User not found.");
|
||||
|
||||
var user = userRet.Value.Value;
|
||||
var boardId = user.BoardID;
|
||||
if (boardId == Guid.Empty)
|
||||
return NotFound("No board bound to this user.");
|
||||
|
||||
try
|
||||
{
|
||||
await _videoStreamService.DisableHdmiTransmissionAsync(boardId.ToString());
|
||||
return Ok($"HDMI transmission for board {boardId} disabled.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"Failed to disable HDMI transmission for board {boardId}");
|
||||
return StatusCode(500, $"Error disabling HDMI transmission: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
376
server/src/Controllers/JtagController.cs
Normal file
@@ -0,0 +1,376 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Database;
|
||||
using server.Services;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// JTAG 控制器 - 提供 JTAG 相关的 API 操作
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize] // 添加用户认证要求
|
||||
public class JtagController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private readonly ProgressTrackerService _tracker;
|
||||
|
||||
private const string BITSTREAM_PATH = "bitstream/Jtag";
|
||||
|
||||
public JtagController(ProgressTrackerService tracker)
|
||||
{
|
||||
_tracker = tracker;
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
logger.Info($"User {User.Identity?.Name} requesting device ID code from {address}:{port}");
|
||||
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while getting device {address} ID code");
|
||||
return TypedResults.InternalServerError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 JTAG 设备的状态寄存器
|
||||
/// </summary>
|
||||
/// <param name="address">JTAG 设备地址</param>
|
||||
/// <param name="port">JTAG 设备端口</param>
|
||||
/// <returns>状态寄存器的原始值、二进制表示和解码值</returns>
|
||||
[HttpGet("ReadStatusReg")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async ValueTask<IResult> ReadStatusReg(string address, int port)
|
||||
{
|
||||
logger.Info($"User {User.Identity?.Name} requesting status register from {address}:{port}");
|
||||
|
||||
try
|
||||
{
|
||||
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadStatusReg();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
var binaryValue = Common.String.Reverse(Convert.ToString(ret.Value, 2).PadLeft(32, '0'));
|
||||
var decodeValue = new Peripherals.JtagClient.JtagStatusReg(ret.Value);
|
||||
logger.Info($"User {User.Identity?.Name} successfully read device {address} Status Register: \n\t 0b{binaryValue} \n\t {decodeValue}");
|
||||
return TypedResults.Ok(new
|
||||
{
|
||||
original = ret.Value,
|
||||
binaryValue,
|
||||
decodeValue,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"User {User.Identity?.Name} failed to read device {address} status register: {ret.Error}");
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while reading device {address} status register");
|
||||
return TypedResults.InternalServerError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 JTAG 下载比特流文件到 FPGA 设备
|
||||
/// </summary>
|
||||
/// <param name="address">JTAG 设备地址</param>
|
||||
/// <param name="port">JTAG 设备端口</param>
|
||||
/// <param name="bitstreamId">比特流ID</param>
|
||||
/// <returns>进度跟踪TaskID</returns>
|
||||
[HttpPost("DownloadBitstream")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async ValueTask<IResult> DownloadBitstream(string address, int port, int bitstreamId, CancellationToken cancelToken)
|
||||
{
|
||||
logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
|
||||
|
||||
try
|
||||
{
|
||||
// 获取当前用户名
|
||||
var username = User.Identity?.Name;
|
||||
if (string.IsNullOrEmpty(username))
|
||||
{
|
||||
logger.Warn("Anonymous user attempted to download bitstream");
|
||||
return TypedResults.Unauthorized();
|
||||
}
|
||||
|
||||
// 从数据库获取用户信息
|
||||
using var db = new Database.AppDataConnection();
|
||||
var userResult = db.GetUserByName(username);
|
||||
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
|
||||
{
|
||||
logger.Error($"User {username} not found in database");
|
||||
return TypedResults.BadRequest("用户不存在");
|
||||
}
|
||||
|
||||
var user = userResult.Value.Value;
|
||||
|
||||
// 从数据库获取比特流
|
||||
var bitstreamResult = db.GetResourceById(bitstreamId);
|
||||
|
||||
if (!bitstreamResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"User {username} failed to get bitstream from database: {bitstreamResult.Error}");
|
||||
return TypedResults.InternalServerError($"数据库查询失败: {bitstreamResult.Error?.Message}");
|
||||
}
|
||||
|
||||
if (!bitstreamResult.Value.HasValue)
|
||||
{
|
||||
logger.Warn($"User {username} attempted to download non-existent bitstream ID: {bitstreamId}");
|
||||
return TypedResults.BadRequest("比特流不存在");
|
||||
}
|
||||
|
||||
var bitstream = bitstreamResult.Value.Value;
|
||||
|
||||
// 处理比特流数据
|
||||
var fileBytes = bitstream.Data;
|
||||
if (fileBytes == null || fileBytes.Length == 0)
|
||||
{
|
||||
logger.Warn($"User {username} found empty bitstream data for ID: {bitstreamId}");
|
||||
return TypedResults.BadRequest("比特流数据为空,请重新上传");
|
||||
}
|
||||
|
||||
logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes");
|
||||
|
||||
// 定义进度跟踪
|
||||
var (taskId, progress) = _tracker.CreateTask(cancelToken);
|
||||
progress.Report(10);
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// 定义缓冲区大小: 32KB
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
byte[] revBuffer = new byte[32 * 1024];
|
||||
long totalBytesProcessed = 0;
|
||||
|
||||
// 使用内存流处理文件
|
||||
using (var inputStream = new MemoryStream(fileBytes))
|
||||
using (var outputStream = new MemoryStream())
|
||||
{
|
||||
int bytesRead;
|
||||
while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
// 反转 32bits
|
||||
var retBuffer = Common.Number.ReverseBytes(buffer, 4);
|
||||
if (!retBuffer.IsSuccessful)
|
||||
{
|
||||
logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
|
||||
progress.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
|
||||
return;
|
||||
}
|
||||
revBuffer = retBuffer.Value;
|
||||
|
||||
for (int i = 0; i < revBuffer.Length; i++)
|
||||
{
|
||||
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
|
||||
}
|
||||
|
||||
await outputStream.WriteAsync(revBuffer, 0, bytesRead);
|
||||
totalBytesProcessed += bytesRead;
|
||||
}
|
||||
|
||||
// 获取处理后的数据
|
||||
var processedBytes = outputStream.ToArray();
|
||||
logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}");
|
||||
|
||||
progress.Report(20);
|
||||
|
||||
// 下载比特流
|
||||
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
|
||||
var ret = await jtagCtrl.DownloadBitstream(processedBytes);
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}");
|
||||
progress.Finish();
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
|
||||
progress.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return TypedResults.Ok(taskId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"User encountered exception while downloading bitstream to device {address}");
|
||||
return TypedResults.InternalServerError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行边界扫描,获取所有端口状态
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
logger.Info($"User {User.Identity?.Name} initiating boundary scan for all ports on device {address}:{port}");
|
||||
|
||||
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>
|
||||
/// 执行逻辑端口边界扫描
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
logger.Info($"User {User.Identity?.Name} initiating logical ports boundary scan on device {address}:{port}");
|
||||
|
||||
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>
|
||||
/// 设置 JTAG 时钟速度
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
logger.Info($"User {User.Identity?.Name} setting JTAG speed to {speed} Hz for device {address}:{port}");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
425
server/src/Controllers/LogicAnalyzerController.cs
Normal file
@@ -0,0 +1,425 @@
|
||||
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 AnalyzerClockDiv ClockDiv { get; set; } = AnalyzerClockDiv.DIV1;
|
||||
/// <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>
|
||||
/// <param name="clock_div">采样时钟分频系数</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("SetCaptureParams")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> SetCaptureParams(int capture_length, int pre_capture_length, AnalyzerChannelDiv channel_div, AnalyzerClockDiv clock_div)
|
||||
{
|
||||
try
|
||||
{
|
||||
//DDR深度为 32'h01000000 - 32'h0FFFFFFF
|
||||
if (capture_length < 0 || capture_length > 0x10000000 - 0x01000000)
|
||||
return BadRequest("采样深度设置错误");
|
||||
if (pre_capture_length < 0 || pre_capture_length >= capture_length)
|
||||
return BadRequest("预采样深度必须小于捕获深度");
|
||||
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.SetCaptureParams(capture_length, pre_capture_length, channel_div, clock_div);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置深度、预采样深度、有效通道、时钟分频失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置深度、预采样深度、有效通道、时钟分频失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "设置深度、预采样深度、有效通道、时钟分频失败时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量配置捕获参数
|
||||
/// </summary>
|
||||
/// <param name="config">捕获配置</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("ConfigureCapture")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> ConfigureCapture([FromBody] CaptureConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (config == null)
|
||||
return BadRequest("配置参数不能为空");
|
||||
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
// 设置全局触发模式
|
||||
var globalResult = await analyzer.SetGlobalTrigMode(config.GlobalMode);
|
||||
if (!globalResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置全局触发模式失败: {globalResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置全局触发模式失败");
|
||||
}
|
||||
|
||||
// 设置信号触发模式
|
||||
foreach (var signalConfig in config.SignalConfigs)
|
||||
{
|
||||
if (signalConfig.SignalIndex < 0 || signalConfig.SignalIndex > 31)
|
||||
return BadRequest($"信号索引{signalConfig.SignalIndex}超出范围0-31");
|
||||
|
||||
var signalResult = await analyzer.SetSignalTrigMode(
|
||||
signalConfig.SignalIndex, signalConfig.Operator, signalConfig.Value);
|
||||
if (!signalResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置信号{signalConfig.SignalIndex}触发模式失败: {signalResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError,
|
||||
$"设置信号{signalConfig.SignalIndex}触发模式失败");
|
||||
}
|
||||
}
|
||||
// 设置深度、预采样深度、有效通道
|
||||
var paramsResult = await analyzer.SetCaptureParams(
|
||||
config.CaptureLength, config.PreCaptureLength, config.ChannelDiv, config.ClockDiv);
|
||||
if (!paramsResult.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置深度、预采样深度、有效通道失败: {paramsResult.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "设置深度、预采样深度、有效通道失败");
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "配置捕获参数时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制捕获
|
||||
/// </summary>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpPost("ForceCapture")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> ForceCapture()
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.SetCaptureMode(true, true);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"强制捕获失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "强制捕获失败");
|
||||
}
|
||||
|
||||
return Ok(result.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "强制捕获时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取捕获数据
|
||||
/// </summary>
|
||||
/// <returns>捕获的波形数据(Base64编码)</returns>
|
||||
[HttpGet("GetCaptureData")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> GetCaptureData(int capture_length = 2048 * 32)
|
||||
{
|
||||
try
|
||||
{
|
||||
var analyzer = GetAnalyzer();
|
||||
if (analyzer == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await analyzer.ReadCaptureData(capture_length);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"读取捕获数据失败: {result.Error}");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "读取捕获数据失败");
|
||||
}
|
||||
|
||||
// 将二进制数据编码为Base64字符串返回
|
||||
var base64Data = Convert.ToBase64String(result.Value);
|
||||
return Ok(base64Data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "读取捕获数据时发生异常");
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
101
server/src/Controllers/MatrixKeyController.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.Collections;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 矩阵键控制器,用于管理矩阵键的启用、禁用和状态设置
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class MatrixKeyController : ControllerBase
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 启用矩阵键控制。
|
||||
/// </summary>
|
||||
/// <param name="address">设备的IP地址</param>
|
||||
/// <param name="port">设备的端口号</param>
|
||||
/// <returns>返回操作结果的状态码</returns>
|
||||
[HttpPost("EnabelMatrixKey")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> EnabelMatrixKey(string address, int port)
|
||||
{
|
||||
var matrixKeyCtrl = new Peripherals.MatrixKeyClient.MatrixKey(address, port);
|
||||
var ret = await matrixKeyCtrl.EnableControl();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Enable device {address}:{port.ToString()} matrix key finished: {ret.Value}.");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用矩阵键控制。
|
||||
/// </summary>
|
||||
/// <param name="address">设备的IP地址</param>
|
||||
/// <param name="port">设备的端口号</param>
|
||||
/// <returns>返回操作结果的状态码</returns>
|
||||
[HttpPost("DisableMatrixKey")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> DisableMatrixKey(string address, int port)
|
||||
{
|
||||
var matrixKeyCtrl = new Peripherals.MatrixKeyClient.MatrixKey(address, port);
|
||||
var ret = await matrixKeyCtrl.DisableControl();
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Disable device {address}:{port.ToString()} matrix key finished: {ret.Value}.");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置矩阵键的状态。
|
||||
/// </summary>
|
||||
/// <param name="address">设备的IP地址</param>
|
||||
/// <param name="port">设备的端口号</param>
|
||||
/// <param name="keyStates">矩阵键的状态数组,长度应为16</param>
|
||||
/// <returns>返回操作结果的状态码</returns>
|
||||
[HttpPost("SetMatrixKeyStatus")]
|
||||
[EnableCors("Users")]
|
||||
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
|
||||
public async ValueTask<IResult> SetMatrixKeyStatus(string address, int port, [FromBody] bool[] keyStates)
|
||||
{
|
||||
if (keyStates.Length != 16)
|
||||
return TypedResults.BadRequest($"The length of key states should be 16 instead of {keyStates.Length}");
|
||||
|
||||
var matrixKeyCtrl = new Peripherals.MatrixKeyClient.MatrixKey(address, port);
|
||||
var ret = await matrixKeyCtrl.ControlKey(new BitArray(keyStates));
|
||||
|
||||
if (ret.IsSuccessful)
|
||||
{
|
||||
logger.Info($"Set device {address}:{port.ToString()} matrix key finished: {ret.Value}.");
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error(ret.Error);
|
||||
return TypedResults.InternalServerError(ret.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||