Compare commits

...

26 Commits

Author SHA1 Message Date
SikongJueluo 0fb5a16b8e
change throw to return and fix bug: when error always throw 2025-04-23 13:41:48 +08:00
SikongJueluo 87c158ebfe
remove bun and some typescript backend server package 2025-04-22 17:32:43 +08:00
SikongJueluo cdf7131b16
Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab 2025-04-22 17:26:03 +08:00
SikongJueluo 98a5dbe7f3
try to fix server bug: couldn't download bitstream 2025-04-22 17:24:46 +08:00
alivender a6d68f1120 Merge branch 'dpp' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-04-22 14:21:51 +08:00
SikongJueluo 80947adc51 add web api for download bitstream 2025-04-22 14:21:37 +08:00
alivender 301b2a20f4 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-04-22 14:20:51 +08:00
alivender 8b2ecae6e3 remove bun 2025-04-22 14:20:14 +08:00
SikongJueluo 442e40d87a
add web api for download bitstream 2025-04-22 13:34:05 +08:00
SikongJueluo 70f96701f7
jtag add download bitstream 2025-04-21 21:54:09 +08:00
SikongJueluo acd6a68507
use nswag to publish swagger 2025-04-17 16:26:12 +08:00
SikongJueluo d25f9882fc
fix bug: couldn't convert package 2025-04-17 14:25:37 +08:00
SikongJueluo 3b674f413a
change udpData type from list to queue 2025-04-16 21:19:55 +08:00
SikongJueluo 84699708d5
try to fix bug 2025-04-15 21:38:06 +08:00
SikongJueluo e8ec4c2a86
add test solution and try to fix get id code bug 2025-04-15 20:09:34 +08:00
SikongJueluo 5e2da17c28
try to fix receive problems 2025-04-14 21:50:51 +08:00
SikongJueluo ae34cf6436
rewrite udp server handler, change router and protocol 2025-04-14 21:31:50 +08:00
SikongJueluo 5fa4860bfb
try to fix bug, and add schema to swagger 2025-04-13 21:22:55 +08:00
SikongJueluo c9660633f0
server finish nlog 2025-04-10 21:53:21 +08:00
SikongJueluo 143d6c634b
add nlog for server 2025-04-09 19:42:29 +08:00
SikongJueluo 5c0f5b2127
add more view for test 2025-04-07 21:56:41 +08:00
SikongJueluo 93add0c315
add jtag get id code api 2025-04-07 21:56:30 +08:00
SikongJueluo 292c73e757
finish sidebar router push 2025-04-05 20:49:50 +08:00
SikongJueluo 20d4fa12d8
Server: finish jtag controller; Web: finish sibebar animation 2025-04-05 19:41:56 +08:00
SikongJueluo 342a5a2436
finish receive udp data handle function 2025-04-03 17:21:22 +08:00
SikongJueluo 2af0986d67
add some doc and jtag controller 2025-04-02 21:51:20 +08:00
36 changed files with 6513 additions and 938 deletions

22
.justfile Normal file
View File

@ -0,0 +1,22 @@
@_show-dir:
echo "Current Working Directory:"
pwd
echo
clean:
rm -rf "server/bin"
rm -rf "server/obj"
rm -rf "server.test/bin"
rm -rf "server.test/obj"
[working-directory: "server"]
publish: _show-dir
dotnet publish --self-contained false -t:PublishAllRids
[working-directory: "server"]
run-server: _show-dir
dotnet run
[working-directory: "server.test"]
test-server: _show-dir
dotnet test --logger "console;verbosity=detailed"

585
bun.lock
View File

@ -1,585 +0,0 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "fpga-weblab",
"dependencies": {
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"@types/lodash": "^4.17.16",
"lodash": "^4.17.21",
"log-symbols": "^7.0.0",
"pinia": "^3.0.1",
"tinypool": "^1.0.2",
"trpc-bun-adapter": "^1.2.2",
"ts-log": "^2.2.7",
"ts-results-es": "^5.0.1",
"vue": "^3.5.13",
"vue-router": "4",
"zod": "^3.24.2",
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.12",
"@tsconfig/node22": "^22.0.0",
"@types/bun": "^1.2.5",
"@types/node": "^22.13.4",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.20",
"bun-plugin-tailwind": "^0.0.15",
"daisyui": "^5.0.0",
"npm-run-all2": "^7.0.2",
"postcss": "^8.5.3",
"tailwindcss": "^4.0.12",
"typescript": "~5.7.3",
"vite": "^6.1.0",
"vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.2",
},
},
},
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@antfu/utils": ["@antfu/utils@0.7.10", "", {}, "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww=="],
"@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
"@babel/compat-data": ["@babel/compat-data@7.26.8", "", {}, "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="],
"@babel/core": ["@babel/core@7.26.9", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.9", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.9", "@babel/parser": "^7.26.9", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.9", "@babel/types": "^7.26.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw=="],
"@babel/generator": ["@babel/generator@7.26.9", "", { "dependencies": { "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.26.5", "", { "dependencies": { "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA=="],
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.26.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/traverse": "^7.26.9", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg=="],
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="],
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="],
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.26.5", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/traverse": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg=="],
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="],
"@babel/helpers": ["@babel/helpers@7.26.9", "", { "dependencies": { "@babel/template": "^7.26.9", "@babel/types": "^7.26.9" } }, "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA=="],
"@babel/parser": ["@babel/parser@7.26.9", "", { "dependencies": { "@babel/types": "^7.26.9" }, "bin": "./bin/babel-parser.js" }, "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A=="],
"@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/plugin-syntax-decorators": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g=="],
"@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg=="],
"@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.26.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A=="],
"@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA=="],
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ=="],
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.26.8", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bME5J9AC8ChwA7aEPJ6zym3w7aObZULHhbNLU0bKUhKsAkylkzUdq+0kdymh9rzi8nlNFl2bmldFBCKNJBUpuw=="],
"@babel/template": ["@babel/template@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.26.9", "@babel/types": "^7.26.9" } }, "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA=="],
"@babel/traverse": ["@babel/traverse@7.26.9", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.9", "@babel/parser": "^7.26.9", "@babel/template": "^7.26.9", "@babel/types": "^7.26.9", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg=="],
"@babel/types": ["@babel/types@7.26.9", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.0", "", { "os": "android", "cpu": "arm" }, "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.0", "", { "os": "android", "cpu": "arm64" }, "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.0", "", { "os": "android", "cpu": "x64" }, "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.0", "", { "os": "linux", "cpu": "arm" }, "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.0", "", { "os": "none", "cpu": "arm64" }, "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.0", "", { "os": "none", "cpu": "x64" }, "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@polka/url": ["@polka/url@1.0.0-next.28", "", {}, "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.9", "", { "os": "android", "cpu": "arm" }, "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.9", "", { "os": "android", "cpu": "arm64" }, "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.9", "", { "os": "linux", "cpu": "arm" }, "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.9", "", { "os": "linux", "cpu": "arm" }, "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.9", "", { "os": "linux", "cpu": "none" }, "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg=="],
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.9", "", { "os": "linux", "cpu": "none" }, "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.9", "", { "os": "linux", "cpu": "x64" }, "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.9", "", { "os": "linux", "cpu": "x64" }, "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.9", "", { "os": "win32", "cpu": "x64" }, "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw=="],
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
"@tailwindcss/node": ["@tailwindcss/node@4.0.12", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "tailwindcss": "4.0.12" } }, "sha512-a6J11K1Ztdln9OrGfoM75/hChYPcHYGNYimqciMrvKXRmmPaS8XZTHhdvb5a3glz4Kd4ZxE1MnuFE2c0fGGmtg=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.12", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.12", "@tailwindcss/oxide-darwin-arm64": "4.0.12", "@tailwindcss/oxide-darwin-x64": "4.0.12", "@tailwindcss/oxide-freebsd-x64": "4.0.12", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.12", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.12", "@tailwindcss/oxide-linux-arm64-musl": "4.0.12", "@tailwindcss/oxide-linux-x64-gnu": "4.0.12", "@tailwindcss/oxide-linux-x64-musl": "4.0.12", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.12", "@tailwindcss/oxide-win32-x64-msvc": "4.0.12" } }, "sha512-DWb+myvJB9xJwelwT9GHaMc1qJj6MDXRDR0CS+T8IdkejAtu8ctJAgV4r1drQJLPeS7mNwq2UHW2GWrudTf63A=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.12", "", { "os": "android", "cpu": "arm64" }, "sha512-dAXCaemu3mHLXcA5GwGlQynX8n7tTdvn5i1zAxRvZ5iC9fWLl5bGnjZnzrQqT7ttxCvRwdVf3IHUnMVdDBO/kQ=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vPNI+TpJQ7sizselDXIJdYkx9Cu6JBdtmRWujw9pVIxW8uz3O2PjgGGzL/7A0sXI8XDjSyRChrUnEW9rQygmJQ=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-RL/9jM41Fdq4Efr35C5wgLx98BirnrfwuD+zgMFK6Ir68HeOSqBhW9jsEeC7Y/JcGyPd3MEoJVIU4fAb7YLg7A=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7WzWiax+LguJcMEimY0Q4sBLlFXu1tYxVka3+G2M9KmU/3m84J3jAIV4KZWnockbHsbb2XgrEjtlJKVwHQCoRA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.12", "", { "os": "linux", "cpu": "arm" }, "sha512-X9LRC7jjE1QlfIaBbXjY0PGeQP87lz5mEfLSVs2J1yRc9PSg1tEPS9NBqY4BU9v5toZgJgzKeaNltORyTs22TQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-i24IFNq2402zfDdoWKypXz0ZNS2G4NKaA82tgBlE2OhHIE+4mg2JDb5wVfyP6R+MCm5grgXvurcIcKWvo44QiQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-LmOdshJBfAGIBG0DdBWhI0n5LTMurnGGJCHcsm9F//ISfsHtCnnYIKgYQui5oOz1SUCkqsMGfkAzWyNKZqbGNw=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.12", "", { "os": "linux", "cpu": "x64" }, "sha512-OSK667qZRH30ep8RiHbZDQfqkXjnzKxdn0oRwWzgCO8CoTxV+MvIkd0BWdQbYtYuM1wrakARV/Hwp0eA/qzdbw=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uylhWq6OWQ8krV8Jk+v0H/3AZKJW6xYMgNMyNnUbbYXWi7hIVdxRKNUB5UvrlC3RxtgsK5EAV2i1CWTRsNcAnA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-XDLnhMoXZEEOir1LK43/gHHwK84V1GlV8+pAncUAIN2wloeD+nNciI9WRIY/BeFTqES22DhTIGoilSO39xDb2g=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.12", "", { "os": "win32", "cpu": "x64" }, "sha512-I/BbjCLpKDQucvtn6rFuYLst1nfFwSMYyPzkx/095RE+tuzk5+fwXuzQh7T3fIBTcbn82qH/sFka7yPGA50tLw=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.0.12", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.0.12", "@tailwindcss/oxide": "4.0.12", "lightningcss": "^1.29.1", "postcss": "^8.4.41", "tailwindcss": "4.0.12" } }, "sha512-r59Sdr8djCW4dL3kvc4aWU8PHdUAVM3O3te2nbYzXsWwKLlHPCuUoZAc9FafXb/YyNDZOMI7sTbKTKFmwOrMjw=="],
"@trpc/client": ["@trpc/client@10.45.2", "", { "peerDependencies": { "@trpc/server": "10.45.2" } }, "sha512-ykALM5kYWTLn1zYuUOZ2cPWlVfrXhc18HzBDyRhoPYN0jey4iQHEFSEowfnhg1RvYnrAVjNBgHNeSAXjrDbGwg=="],
"@trpc/server": ["@trpc/server@10.45.2", "", {}, "sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg=="],
"@tsconfig/node22": ["@tsconfig/node22@22.0.0", "", {}, "sha512-twLQ77zevtxobBOD4ToAtVmuYrpeYUh3qh+TEp+08IWhpsrIflVHqQ1F1CiPxQGL7doCdBIOOCF+1Tm833faNg=="],
"@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="],
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
"@types/lodash": ["@types/lodash@4.17.16", "", {}, "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g=="],
"@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.1", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ=="],
"@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@4.1.1", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-typescript": "^7.25.9", "@vue/babel-plugin-jsx": "^1.2.5" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.0.0" } }, "sha512-uMJqv/7u1zz/9NbWAD3XdjaY20tKTf17XVfQ9zq4wY1BjsB/PjpJPMe2xiG39QpP4ZdhYNhm4Hvo66uJrykNLA=="],
"@volar/language-core": ["@volar/language-core@2.4.12", "", { "dependencies": { "@volar/source-map": "2.4.12" } }, "sha512-RLrFdXEaQBWfSnYGVxvR2WrO6Bub0unkdHYIdC31HzIEqATIuuhRRzYu76iGPZ6OtA4Au1SnW0ZwIqPP217YhA=="],
"@volar/source-map": ["@volar/source-map@2.4.12", "", {}, "sha512-bUFIKvn2U0AWojOaqf63ER0N/iHIBYZPpNGogfLPQ68F5Eet6FnLlyho7BS0y2HJ1jFhSif7AcuTx1TqsCzRzw=="],
"@volar/typescript": ["@volar/typescript@2.4.12", "", { "dependencies": { "@volar/language-core": "2.4.12", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-HJB73OTJDgPc80K30wxi3if4fSsZZAOScbj2fcicMuOPoOkcf9NNAINb33o+DzhBdF9xTKC1gnPmIRDous5S0g=="],
"@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.3.0", "", {}, "sha512-vrNyYNQcz1gfc87uuN+Z+On9fFOBQTYRlTUEDovpeCmjuwH83lAm6YM0VBvTx6eRTHg3SU5jP2CD+kSXY30PGg=="],
"@vue/babel-plugin-jsx": ["@vue/babel-plugin-jsx@1.3.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.9", "@babel/types": "^7.26.9", "@vue/babel-helper-vue-transform-on": "1.3.0", "@vue/babel-plugin-resolve-type": "1.3.0", "@vue/shared": "^3.5.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" }, "optionalPeers": ["@babel/core"] }, "sha512-ODZSs93FCxLMOiMFAGJXe7QMJp1tk8hkMbk84OcHOTVwYU2cFwFu1z7jjrRv44wCCfPNkflqn6hnexVprb+G7A=="],
"@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@1.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/parser": "^7.26.9", "@vue/compiler-sfc": "^3.5.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-3SmusE11QKNKtnVfbsKegUEArpf1fXE85Dzi/Q6lvaz3MA3tmL8BXyq/vA7GJeZ183XeNpLIZHrHDdUh9V348A=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.13", "", { "dependencies": { "@babel/parser": "^7.25.3", "@vue/shared": "3.5.13", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.13", "", { "dependencies": { "@vue/compiler-core": "3.5.13", "@vue/shared": "3.5.13" } }, "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA=="],
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.13", "", { "dependencies": { "@babel/parser": "^7.25.3", "@vue/compiler-core": "3.5.13", "@vue/compiler-dom": "3.5.13", "@vue/compiler-ssr": "3.5.13", "@vue/shared": "3.5.13", "estree-walker": "^2.0.2", "magic-string": "^0.30.11", "postcss": "^8.4.48", "source-map-js": "^1.2.0" } }, "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ=="],
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.13", "", { "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/shared": "3.5.13" } }, "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA=="],
"@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="],
"@vue/devtools-api": ["@vue/devtools-api@7.7.2", "", { "dependencies": { "@vue/devtools-kit": "^7.7.2" } }, "sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA=="],
"@vue/devtools-core": ["@vue/devtools-core@7.7.2", "", { "dependencies": { "@vue/devtools-kit": "^7.7.2", "@vue/devtools-shared": "^7.7.2", "mitt": "^3.0.1", "nanoid": "^5.0.9", "pathe": "^2.0.2", "vite-hot-client": "^0.2.4" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-lexREWj1lKi91Tblr38ntSsy6CvI8ba7u+jmwh2yruib/ltLUcsIzEjCnrkh1yYGGIKXbAuYV2tOG10fGDB9OQ=="],
"@vue/devtools-kit": ["@vue/devtools-kit@7.7.2", "", { "dependencies": { "@vue/devtools-shared": "^7.7.2", "birpc": "^0.2.19", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.1" } }, "sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ=="],
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.2", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA=="],
"@vue/language-core": ["@vue/language-core@2.2.8", "", { "dependencies": { "@volar/language-core": "~2.4.11", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ=="],
"@vue/reactivity": ["@vue/reactivity@3.5.13", "", { "dependencies": { "@vue/shared": "3.5.13" } }, "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg=="],
"@vue/runtime-core": ["@vue/runtime-core@3.5.13", "", { "dependencies": { "@vue/reactivity": "3.5.13", "@vue/shared": "3.5.13" } }, "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw=="],
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.13", "", { "dependencies": { "@vue/reactivity": "3.5.13", "@vue/runtime-core": "3.5.13", "@vue/shared": "3.5.13", "csstype": "^3.1.3" } }, "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog=="],
"@vue/server-renderer": ["@vue/server-renderer@3.5.13", "", { "dependencies": { "@vue/compiler-ssr": "3.5.13", "@vue/shared": "3.5.13" }, "peerDependencies": { "vue": "3.5.13" } }, "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA=="],
"@vue/shared": ["@vue/shared@3.5.13", "", {}, "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ=="],
"@vue/tsconfig": ["@vue/tsconfig@0.7.0", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg=="],
"alien-signals": ["alien-signals@1.0.4", "", {}, "sha512-DJqqQD3XcsaQcQ1s+iE2jDUZmmQpXwHiR6fCAim/w87luaW+vmLY8fMlrdkmRwzaFXhkxf3rqPCR59tKVv1MDw=="],
"ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"autoprefixer": ["autoprefixer@10.4.20", "", { "dependencies": { "browserslist": "^4.23.3", "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"birpc": ["birpc@0.2.19", "", {}, "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ=="],
"brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="],
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.15", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-qtAXMNGG4R0UGGI8zWrqm2B7BdXqx48vunJXBPzfDOHPA5WkRUZdTSbE7TFwO4jLhYqSE23YMWsM9NhE6ovobw=="],
"bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"caniuse-lite": ["caniuse-lite@1.0.30001702", "", {}, "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"daisyui": ["daisyui@5.0.0", "", {}, "sha512-U0K9Bac3Bi3zZGm6ojrw12F0vBHTpEgf46zv/BYxLe07hF0Xnx7emIQliwaRBgJuYhY0BhwQ6wSnq5cJXHA2yA=="],
"de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="],
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="],
"default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="],
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
"detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
"electron-to-chromium": ["electron-to-chromium@1.5.113", "", {}, "sha512-wjT2O4hX+wdWPJ76gWSkMhcHAV2PTMX+QetUCPYEdCIe+cxmgzzSSiGRCKW8nuh4mwKZlpv0xvoW7OF2X+wmHg=="],
"enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"error-stack-parser-es": ["error-stack-parser-es@0.1.5", "", {}, "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg=="],
"esbuild": ["esbuild@0.25.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", "@esbuild/android-arm64": "0.25.0", "@esbuild/android-x64": "0.25.0", "@esbuild/darwin-arm64": "0.25.0", "@esbuild/darwin-x64": "0.25.0", "@esbuild/freebsd-arm64": "0.25.0", "@esbuild/freebsd-x64": "0.25.0", "@esbuild/linux-arm": "0.25.0", "@esbuild/linux-arm64": "0.25.0", "@esbuild/linux-ia32": "0.25.0", "@esbuild/linux-loong64": "0.25.0", "@esbuild/linux-mips64el": "0.25.0", "@esbuild/linux-ppc64": "0.25.0", "@esbuild/linux-riscv64": "0.25.0", "@esbuild/linux-s390x": "0.25.0", "@esbuild/linux-x64": "0.25.0", "@esbuild/netbsd-arm64": "0.25.0", "@esbuild/netbsd-x64": "0.25.0", "@esbuild/openbsd-arm64": "0.25.0", "@esbuild/openbsd-x64": "0.25.0", "@esbuild/sunos-x64": "0.25.0", "@esbuild/win32-arm64": "0.25.0", "@esbuild/win32-ia32": "0.25.0", "@esbuild/win32-x64": "0.25.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"execa": ["execa@9.5.2", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.3", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.0", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.0.0" } }, "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q=="],
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
"fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
"globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"human-signals": ["human-signals@8.0.0", "", {}, "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA=="],
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@4.0.0", "", {}, "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
"lightningcss": ["lightningcss@1.29.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.2", "", { "os": "linux", "cpu": "arm" }, "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"log-symbols": ["log-symbols@7.0.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-zrc91EDk2M+2AXo/9BTvK91pqb7qrPg2nX/Hy+u8a5qQlbaOflCKO+6SqgZ+M+xUFxGdKTgwnGiL96b1W3ikRA=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
"nanoid": ["nanoid@3.3.9", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg=="],
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
"normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
"npm-normalize-package-bin": ["npm-normalize-package-bin@4.0.0", "", {}, "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w=="],
"npm-run-all2": ["npm-run-all2@7.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "minimatch": "^9.0.0", "pidtree": "^0.6.0", "read-package-json-fast": "^4.0.0", "shell-quote": "^1.7.3", "which": "^5.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-7tXR+r9hzRNOPNTvXegM+QzCuMjzUIIq66VDunL6j60O4RrExx32XUhlrS7UK4VcdGw5/Wxzb3kfNcFix9JKDA=="],
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
"open": ["open@10.1.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw=="],
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
"pinia": ["pinia@3.0.1", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-WXglsDzztOTH6IfcJ99ltYZin2mY8XZCXujkYWVIJlBjqsP6ST7zw+Aarh63E1cDVYeyUcPCxPHzJpEOmzB6Wg=="],
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="],
"read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rollup": ["rollup@4.34.9", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.9", "@rollup/rollup-android-arm64": "4.34.9", "@rollup/rollup-darwin-arm64": "4.34.9", "@rollup/rollup-darwin-x64": "4.34.9", "@rollup/rollup-freebsd-arm64": "4.34.9", "@rollup/rollup-freebsd-x64": "4.34.9", "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", "@rollup/rollup-linux-arm-musleabihf": "4.34.9", "@rollup/rollup-linux-arm64-gnu": "4.34.9", "@rollup/rollup-linux-arm64-musl": "4.34.9", "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", "@rollup/rollup-linux-riscv64-gnu": "4.34.9", "@rollup/rollup-linux-s390x-gnu": "4.34.9", "@rollup/rollup-linux-x64-gnu": "4.34.9", "@rollup/rollup-linux-x64-musl": "4.34.9", "@rollup/rollup-win32-arm64-msvc": "4.34.9", "@rollup/rollup-win32-ia32-msvc": "4.34.9", "@rollup/rollup-win32-x64-msvc": "4.34.9", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ=="],
"run-applescript": ["run-applescript@7.0.0", "", {}, "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
"superjson": ["superjson@2.2.2", "", { "dependencies": { "copy-anything": "^3.0.2" } }, "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q=="],
"tailwindcss": ["tailwindcss@4.0.12", "", {}, "sha512-bT0hJo91FtncsAMSsMzUkoo/iEU0Xs5xgFgVC9XmdM9bw5MhZuQFjPNl6wxAE0SiQF/YTZJa+PndGWYSDtuxAg=="],
"tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
"tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"trpc-bun-adapter": ["trpc-bun-adapter@1.2.2", "", { "peerDependencies": { "@trpc/server": "^11.0.0-rc.566" } }, "sha512-TVhZEDXZvhIM2lfNTVCx9u5fu7/86b7RuhQSM0CUs4vlIan64Sfko5m0stbjqSHNgvLBsvXKtUD8FeQOQGLfpg=="],
"ts-log": ["ts-log@2.2.7", "", {}, "sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg=="],
"ts-results-es": ["ts-results-es@5.0.1", "", {}, "sha512-HjX/7HxQe2bXkbp8pHTjy4Ir9eHIDnDDsLDphhGqy6I9iZ/vD4QXWEIlrVRZsEX+kS2jIiiF/mnl0nKnPTiYFw=="],
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
"vite": ["vite@6.2.1", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q=="],
"vite-hot-client": ["vite-hot-client@0.2.4", "", { "peerDependencies": { "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" } }, "sha512-a1nzURqO7DDmnXqabFOliz908FRmIppkBKsJthS8rbe8hBEXwEwe4C3Pp33Z1JoFCYfVL4kTOMLKk0ZZxREIeA=="],
"vite-plugin-inspect": ["vite-plugin-inspect@0.8.9", "", { "dependencies": { "@antfu/utils": "^0.7.10", "@rollup/pluginutils": "^5.1.3", "debug": "^4.3.7", "error-stack-parser-es": "^0.1.5", "fs-extra": "^11.2.0", "open": "^10.1.0", "perfect-debounce": "^1.0.0", "picocolors": "^1.1.1", "sirv": "^3.0.0" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1" } }, "sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A=="],
"vite-plugin-vue-devtools": ["vite-plugin-vue-devtools@7.7.2", "", { "dependencies": { "@vue/devtools-core": "^7.7.2", "@vue/devtools-kit": "^7.7.2", "@vue/devtools-shared": "^7.7.2", "execa": "^9.5.1", "sirv": "^3.0.0", "vite-plugin-inspect": "0.8.9", "vite-plugin-vue-inspector": "^5.3.1" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0" } }, "sha512-5V0UijQWiSBj32blkyPEqIbzc6HO9c1bwnBhx+ay2dzU0FakH+qMdNUT8nF9BvDE+i6I1U8CqCuJiO20vKEdQw=="],
"vite-plugin-vue-inspector": ["vite-plugin-vue-inspector@5.3.1", "", { "dependencies": { "@babel/core": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.0", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-transform-typescript": "^7.22.15", "@vue/babel-plugin-jsx": "^1.1.5", "@vue/compiler-dom": "^3.3.4", "kolorist": "^1.8.0", "magic-string": "^0.30.4" }, "peerDependencies": { "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0" } }, "sha512-cBk172kZKTdvGpJuzCCLg8lJ909wopwsu3Ve9FsL1XsnLBiRT9U3MePcqrgGHgCX2ZgkqZmAGR8taxw+TV6s7A=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"vue": ["vue@3.5.13", "", { "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13", "@vue/runtime-dom": "3.5.13", "@vue/server-renderer": "3.5.13", "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ=="],
"vue-router": ["vue-router@4.5.0", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w=="],
"vue-tsc": ["vue-tsc@2.2.8", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.8" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ=="],
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="],
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"@vue/devtools-core/nanoid": ["nanoid@5.1.3", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-zAbEOEr7u2CbxwoMRlz/pNSpRP0FdAU4pRaYunCdEezWohXFs+a0Xw7RfkKaezMsmSM1vttcLthJtwRnVtOfHQ=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
}
}

View File

@ -1,2 +0,0 @@
[serve.static]
plugins = ["bun-plugin-tailwind"]

View File

@ -7,7 +7,10 @@
let let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs {
inherit system;
config.permittedInsecurePackages = ["dotnet-sdk-6.0.428"];
};
}); });
in in
{ {
@ -15,7 +18,7 @@
default = pkgs.mkShell { default = pkgs.mkShell {
packages = with pkgs; [ packages = with pkgs; [
# Frontend # Frontend
bun nodejs
sqlite sqlite
sqls sqls
sql-studio sql-studio
@ -26,6 +29,7 @@
dotnetCorePackages.sdk_8_0 dotnetCorePackages.sdk_8_0
]) ])
nuget nuget
# msbuild
omnisharp-roslyn omnisharp-roslyn
csharpier csharpier

3945
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,22 +5,20 @@
"type": "module", "type": "module",
"main": "./server/index.ts", "main": "./server/index.ts",
"scripts": { "scripts": {
"dev": "bunx --bun vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
"preview": "bunx --bun vite preview", "preview": "vite preview",
"build-only": "bunx --bun vite build", "build-only": "vite build",
"type-check": "bunx --bun vue-tsc --build", "type-check": "vue-tsc --build",
"server": "bun run --watch ./server/index.ts" "server": "run --watch ./server/index.ts"
}, },
"dependencies": { "dependencies": {
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"all": "^0.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"log-symbols": "^7.0.0", "log-symbols": "^7.0.0",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"tinypool": "^1.0.2", "tinypool": "^1.0.2",
"trpc-bun-adapter": "^1.2.2",
"ts-log": "^2.2.7", "ts-log": "^2.2.7",
"ts-results-es": "^5.0.1", "ts-results-es": "^5.0.1",
"vue": "^3.5.13", "vue": "^3.5.13",
@ -30,13 +28,11 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.0.12", "@tailwindcss/postcss": "^4.0.12",
"@tsconfig/node22": "^22.0.0", "@tsconfig/node22": "^22.0.0",
"@types/bun": "^1.2.5",
"@types/node": "^22.13.4", "@types/node": "^22.13.4",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1", "@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bun-plugin-tailwind": "^0.0.15",
"daisyui": "^5.0.0", "daisyui": "^5.0.0",
"npm-run-all2": "^7.0.2", "npm-run-all2": "^7.0.2",
"postcss": "^8.5.3", "postcss": "^8.5.3",

3
server.test/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
bin
obj
bitstream

View File

@ -0,0 +1,162 @@
using System.Net;
using System.Text;
using Common;
using Xunit.Abstractions;
namespace server.test;
public sealed class RepeatAttribute : Xunit.Sdk.DataAttribute
{
private readonly int count;
public RepeatAttribute(int count)
{
if (count < 1)
{
throw new System.ArgumentOutOfRangeException(
paramName: nameof(count),
message: "Repeat count must be greater than 0."
);
}
this.count = count;
}
public override System.Collections.Generic.IEnumerable<object[]> GetData(System.Reflection.MethodInfo testMethod)
{
foreach (var iterationNumber in Enumerable.Range(start: 1, count: this.count))
{
yield return new object[] { iterationNumber };
}
}
}
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()));
}
}
}

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<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="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\server\server.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,3 @@
{
"showLiveOutput": true
}

1
server/.gitignore vendored
View File

@ -1,3 +1,4 @@
obj obj
bin bin
bitstream

View File

@ -1,41 +1,113 @@
using System.Net; using Microsoft.AspNetCore.Http.Features;
using Microsoft.OpenApi.Models; using Newtonsoft.Json;
using NLog;
using NLog.Web;
// Early init of NLog to allow startup and exception logging, before host is built
var logger = NLog.LogManager.Setup()
.LoadConfigurationFromAppSettings()
.GetCurrentClassLogger();
logger.Debug("Init Main...");
try
{
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Services Settings
// Add services to the container.
builder.Services.AddControllersWithViews();
// NLog: Setup NLog for Dependency injection
builder.Logging.ClearProviders();
builder.Host.UseNLog();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
builder.Services.Configure<FormOptions>(options =>
{ {
c.SwaggerDoc("v1", new OpenApiInfo options.MultipartBodyLengthLimit = 32 * 1024 * 1024;
});
// Add Json.Net Serializer
builder.Services.AddControllersWithViews().AddNewtonsoftJson(options =>
{ {
// Configure Newtonsoft.Json options here
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
});
// Add Swagger
builder.Services.AddControllers();
builder.Services.AddOpenApiDocument(options =>
{
options.PostProcess = document =>
{
document.Info = new NSwag.OpenApiInfo
{
Version = "v1",
Title = "FPGA Web Lab API", Title = "FPGA Web Lab API",
Description = "Use FPGA in the cloud", Description = "Use FPGA in the cloud",
Version = "v1" // TermsOfService = "https://example.com/terms",
}); // Contact = new NSwag.OpenApiContact
// {
// Name = "Example Contact",
// Url = "https://example.com/contact"
// },
// License = new NSwag.OpenApiLicense
// {
// Name = "Example License",
// Url = "https://example.com/license"
// }
};
};
}); });
// Application Settings
var app = builder.Build(); var app = builder.Build();
if (app.Environment.IsDevelopment()) // Configure the HTTP request pipeline.
{ // app.UseExceptionHandler(new ExceptionHandlerOptions()
app.UseSwagger(); // {
app.UseSwaggerUI(c => // AllowStatusCode404Response = true,
{ // ExceptionHandlingPath = "/error"
c.SwaggerEndpoint("/swagger/v1/swagger.json", "FPAG WebLab API V1"); // });
}); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
} app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
// Setup UDP Server // if (app.Environment.IsDevelopment())
var udpServer = new UDPServer(33000); // {
udpServer.Start(); app.UseOpenApi();
app.UseSwaggerUi();
// }
// Setup Program
MsgBus.Init();
// Router // Router
app.MapGet("/", () => "Hello World!"); // API Get
app.MapPut("/api/SendString", Router.API.SendString); app.MapGet("/", () => Results.Redirect("/swagger"));
app.MapPut("/api/SendAddrPackage", Router.API.SendAddrPackage); app.MapControllers();
app.MapPut("/api/SendDataPackage", Router.API.SendDataPackage);
app.Run("http://localhost:5000"); app.Run("http://localhost:5000");
}
catch (Exception exception)
{
// NLog: catch setup errors
logger.Error(exception, "Stopped program because of exception");
throw;
}
finally
{
// Close UDP Server // Close UDP Server
Console.WriteLine("UDP Server is Closing now..."); logger.Info("Program is Closing now...");
udpServer.Stop();
// Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
NLog.LogManager.Shutdown();
// Close Program
MsgBus.Exit();
}

View File

@ -0,0 +1,28 @@
<Project DefaultTargets="Build">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<!-- Enable roll-forward to latest patch. This allows one restore operation
to apply to all of the self-contained publish operations. -->
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>
<Target Name="PublishAllRids">
<ItemGroup>
<!-- Transform RuntimeIdentifiers property to item -->
<RuntimeIdentifierForPublish Include="$(RuntimeIdentifiers)" />
<!-- Transform RuntimeIdentifierForPublish items to project items to pass to MSBuild task -->
<ProjectToPublish Include="@(RuntimeIdentifierForPublish->'$(MSBuildProjectFullPath)')">
<AdditionalProperties>RuntimeIdentifier=%(RuntimeIdentifierForPublish.Identity)</AdditionalProperties>
</ProjectToPublish>
</ItemGroup>
<MSBuild Projects="@(ProjectToPublish)"
Targets="Publish"
BuildInParallel="true"
/>
</Target>
</Project>

View File

@ -2,8 +2,79 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
},
"NLog": {
"RemoveLoggerFactoryFilter": true
} }
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"NLog": {
"extensions": [
{
"assembly": "NLog.Web.AspNetCore"
}
],
"throwConfigExceptions": true,
"targets": {
"async": true,
"AllFile": {
"type": "File",
"fileName": "./log/all-${shortdate}.log"
},
"WebAllFile": {
"type": "File",
"fileName": "./log/web-all-${shortdate}.log"
},
"EachFile": {
"type": "File",
"fileName": "./log/${logger}-${shortdate}.log"
},
"Console": {
"type": "ColoredConsole"
}
},
"rules": [
{
"logger": "*",
"finalMinLevel": "Trace",
"writeTo": "AllFile"
},
{
"logger": "*",
"finalMinLevel": "Trace",
"writeTo": "WebAllFile"
},
{
"logger": "System.*",
"finalMinLevel": "Warn"
},
{
"logger": "Microsoft.*",
"finalMinLevel": "Warn"
},
{
"logger": "Microsoft.Hosting.Lifetime*",
"finalMinLevel": "Info",
"writeTo": "Console"
},
{
"logger": "server",
"finalMinLevel": "Trace",
"writeTo": "EachFile"
},
{
"logger": "server.*",
"finalMinLevel": "Trace",
"writeTo": "EachFile"
},
{
"logger": "server.*",
"finalMinLevel": "Debug",
"writeTo": "Console"
}
]
}
} }

View File

@ -1,16 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="PublishAllRids.targets" />
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RuntimeIdentifiers>win-x64;linux-x64;</RuntimeIdentifiers>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DotNext" Version="5.19.1" /> <PackageReference Include="DotNext" Version="5.19.1" />
<PackageReference Include="DotNext.Threading" Version="5.19.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.4" />
<PackageReference Include="Microsoft.OpenApi" Version="1.6.23" /> <PackageReference Include="Microsoft.OpenApi" Version="1.6.23" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.0.0" /> <PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" />
<PackageReference Include="NSwag.AspNetCore" Version="14.3.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -2,16 +2,26 @@ using DotNext;
namespace Common namespace Common
{ {
class NumberProcessor /// <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) public static Result<byte[]> NumberToBytes(ulong num, uint length, bool isRightHigh = false)
{ {
if (length > 8) if (length > 8)
{ {
throw new ArgumentException( return new(new ArgumentException(
"Unsigned long number can't over 8 bytes(64 bits).", "Unsigned long number can't over 8 bytes(64 bits).",
nameof(length) nameof(length)
); ));
} }
var arr = new byte[length]; var arr = new byte[length];
@ -27,44 +37,129 @@ namespace Common
{ {
for (var i = 0; i < length; i++) for (var i = 0; i < length; i++)
{ {
arr[i] = Convert.ToByte((num >> (i << 3)) & (0xFF)); arr[i] = Convert.ToByte((num >> ((int)(length - 1 - i) << 3)) & (0xFF));
} }
} }
return arr; return arr;
} }
public static Result<ulong> BytesToNumber(byte[] bytes, bool isRightLeft = false) /// <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) if (bytes.Length > 8)
{ {
throw new ArgumentException( return new(new ArgumentException(
"Unsigned long number can't over 8 bytes(64 bits).", "Unsigned long number can't over 8 bytes(64 bits).",
nameof(bytes) nameof(bytes)
); ));
} }
ulong num = 0; ulong num = 0;
int len = bytes.Length; int len = bytes.Length;
if (isRightLeft) if (isRightHigh)
{ {
for (var i = 0; i < len; i++) for (var i = 0; i < len; i++)
{ {
num += Convert.ToUInt64(bytes[len - 1 - i] << (i << 3)); num += Convert.ToUInt64((UInt64)bytes[len - 1 - i] << (i << 3));
} }
} }
else else
{ {
for (var i = 0; i < len; i++) for (var i = 0; i < len; i++)
{ {
num += Convert.ToUInt64(bytes[i] << (i << 3)); num += Convert.ToUInt64((UInt64)bytes[i] << ((int)(len - 1 - i) << 3));
} }
} }
return num; return num;
} }
public static Result<byte[]> StringToBytes(string str, int numBase = 16)
/// <summary>
/// 比特合并成二进制字节
/// </summary>
/// <param name="bits1">[TODO:parameter]</param>
/// <param name="bits1Len">[TODO:parameter]</param>
/// <param name="bits2">[TODO:parameter]</param>
/// <param name="bits2Len">[TODO:parameter]</param>
/// <returns>[TODO:return]</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">[TODO:parameter]</param>
/// <param name="bits1Len">[TODO:parameter]</param>
/// <param name="bits2">[TODO:parameter]</param>
/// <param name="bits2Len">[TODO:parameter]</param>
/// <returns>[TODO:return]</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">[TODO:parameter]</param>
/// <param name="bits1Len">[TODO:parameter]</param>
/// <param name="bits2">[TODO:parameter]</param>
/// <param name="bits2Len">[TODO:parameter]</param>
/// <returns>[TODO:return]</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">[TODO:parameter]</param>
/// <param name="dstBits">[TODO:parameter]</param>
/// <param name="mask">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public static bool BitsCheck(ulong srcBits, ulong dstBits, ulong mask = 0xFFFF_FFFF_FFFF_FFFF)
{
return (srcBits & mask) == dstBits;
}
/// <summary>
/// 比特位检查
/// </summary>
/// <param name="srcBits">[TODO:parameter]</param>
/// <param name="dstBits">[TODO:parameter]</param>
/// <param name="mask">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public static bool BitsCheck(uint srcBits, uint dstBits, uint mask = 0xFFFF_FFFF)
{
return (srcBits & mask) == dstBits;
}
/// <summary>
/// 字符串转二进制字节数组
/// </summary>
/// <param name="str">[TODO:parameter]</param>
/// <param name="numBase">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public static byte[] StringToBytes(string str, int numBase = 16)
{ {
var len = str.Length; var len = str.Length;
var bytesLen = len / 2; var bytesLen = len / 2;

296
server/src/Controllers.cs Normal file
View File

@ -0,0 +1,296 @@
using System.Net;
using Common;
using DotNext;
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="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];
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];
long totalBytesRead = 0;
// 使用异步流读取文件
using (var memoryStream = new MemoryStream())
{
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await memoryStream.WriteAsync(buffer, 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);
}
}
}

616
server/src/JtagClient.cs Normal file
View File

@ -0,0 +1,616 @@
using System.Net;
using DotNext;
using WebProtocol;
namespace JtagClient;
/// <summary>
/// Global Constant Jtag Address
/// </summary>
public static class JtagAddr
{
/// <summary>
/// Jtag State Reg
/// </summary>
public const UInt32 STATE = 0x10_00_00_00;
/// <summary>
/// Jtag Read Reg
/// </summary>
public const UInt32 READ_DATA = 0x10_00_00_01;
/// <summary>
/// Jtag Write Reg
/// </summary>
public const UInt32 WRITE_DATA = 0x10_00_00_02;
/// <summary>
/// Jtag Write Command
/// </summary>
public const UInt32 WRITE_CMD = 0x10_00_00_03;
}
/// <summary>
/// Jtag 状态寄存器的掩码及其确认函数
/// </summary>
public static class JtagState
{
/*
[0] fifo清空10
[1] fifo读入口-JTAG_STATE_REG的写不改变其值
[2] fifo读入口-JTAG_STATE_REG的写不改变其值
[7:3]
[8] fifo清空10
[9] fifo写入口-JTAG_STATE_REG的写不改变其值
[10] fifo写入口-JTAG_STATE_REG的写不改变其值
[15:11]
[16] fifo清空10
[17] fifo写入口-JTAG_STATE_REG的写不改变其值
[18] fifo写入口-JTAG_STATE_REG的写不改变其值
[23:19]
[24] CMD执行完毕标识JTAG_STATE_REG的写不改变其值
*/
/// <summary>
/// 移位数据读fifo清空1为有效清空后自动置0实际一直为零
/// </summary>
public const UInt32 READ_DATA_FIFO_CLEAR = 0b0001 << 0;
/// <summary>
/// 移位数据fifo读入口-空标识只读对JTAG_STATE_REG的写不改变其值
/// </summary>
public const UInt32 READ_DATA_FIFO_EMPTY = 0b0010 << 0;
/// <summary>
/// 移位数据fifo读入口-满标识只读对JTAG_STATE_REG的写不改变其值
/// </summary>
public const UInt32 READ_DATA_FIFO_AFULL = 0b0100 << 0;
/// <summary>
/// 移位数据读fifo
/// </summary>
public const UInt32 READ_DATA_FIFO = 0b0111 << 0;
/// <summary>
/// 移位数据写fifo清空1为有效清空后自动置0
/// </summary>
public const UInt32 WRITE_DATA_FIFO_CLEAR = 0b0001 << 8;
/// <summary>
/// 移位数据fifo写入口-空标识只读对JTAG_STATE_REG的写不改变其值
/// </summary>
public const UInt32 WRITE_DATA_FIFO_EMPTY = 0b0010 << 8;
/// <summary>
/// 移位数据fifo写入口-满标识只读对JTAG_STATE_REG的写不改变其值
/// </summary>
public const UInt32 WRITE_DATA_FIFO_AFULL = 0b0100 << 8;
/// <summary>
/// 移位数据写fifo
/// </summary>
public const UInt32 WRITE_DATA_FIFO = 0b0111 << 8;
/// <summary>
/// 移位命令写fifo清空1为有效清空后自动置0
/// </summary>
public const UInt32 WRITE_CMD_FIFO_CLEAR = 0b0001 << 16;
/// <summary>
/// 移位命令fifo写入口-空标识只读对JTAG_STATE_REG的写不改变其值
/// </summary>
public const UInt32 WRITE_CMD_FIFO_EMPTY = 0b0010 << 16;
/// <summary>
/// 移位命令fifo写入口-满标识只读对JTAG_STATE_REG的写不改变其值
/// </summary>
public const UInt32 WRITE_CMD_FIFO_AFULL = 0b0100 << 16;
/// <summary>
/// 移位命令写fifo
/// </summary>
public const UInt32 WRITE_CMD_FIFO = 0b0111 << 16;
/// <summary>
/// CMD执行完毕标识只读对JTAG_STATE_REG的写不改变其值
/// </summary>
public const UInt32 CMD_EXEC_FINISH = 0b0001 << 24;
/// <summary>
/// 全部FIFO
/// </summary>
public const UInt32 ALL_FIFO = READ_DATA_FIFO | WRITE_DATA_FIFO | WRITE_CMD_FIFO;
/// <summary>
/// 全部寄存器
/// </summary>
public const UInt32 ALL_REG = READ_DATA_FIFO | WRITE_DATA_FIFO | WRITE_CMD_FIFO | CMD_EXEC_FINISH;
}
/// <summary>
/// The Command bits of Jtag
/// </summary>
public static class JtagCmd
{
/// <summary>
/// The length of JTAG_DR_XXXX
/// </summary>
public const UInt32 LEN_JTAG_DR = 10;
/// <summary>
/// 旁路指令
/// </summary>
public const UInt32 JTAG_DR_BYPASS = 0b1111111111;
/// <summary>
/// 采样指令
/// </summary>
public const UInt32 JTAG_DR_SAMPLE = 0b1010000000;
/// <summary>
/// 预装指令
/// </summary>
public const UInt32 JTAG_DR_PRELOAD = 0b1010000000;
/// <summary>
/// 外测试指令
/// </summary>
public const UInt32 JTAG_DR_EXTEST = 0b1010000001;
/// <summary>
/// 内测试指令
/// </summary>
public const UInt32 JTAG_DR_INTEST = 0b1010000010;
/// <summary>
/// 标识指令
/// </summary>
public const UInt32 JTAG_DR_IDCODE = 0b1010000011;
/// <summary>
/// 高阻指令
/// </summary>
public const UInt32 JTAG_DR_HIGHZ = 0b1010000101;
/// <summary>
/// 复位指令
/// </summary>
public const UInt32 JTAG_DR_JRST = 0b1010001010;
/// <summary>
/// 配置指令
/// </summary>
public const UInt32 JTAG_DR_CFGI = 0b1010001011;
/// <summary>
/// 回读指令
/// </summary>
public const UInt32 JTAG_DR_CFGO = 0b1010001100;
/// <summary>
/// 唤醒指令
/// </summary>
public const UInt32 JTAG_DR_JWAKEUP = 0b1010001101;
/// <summary>
/// 读UID指令
/// </summary>
public const UInt32 JTAG_DR_READ_UID = 0b0101001100;
/// <summary>
/// 读状态寄存器指令
/// </summary>
public const UInt32 JTAG_DR_RDSR = 0b0101011001;
/// <summary>
/// The length of CMD_JTAG_XXXX_XXXX
/// </summary>
public const UInt32 LEN_CMD_JTAG = 4;
/// <summary>
/// 设定JTAG默认状态为TEST_LOGIC_RESET态 JTAG复位
/// </summary>
public const UInt32 CMD_JTAG_CLOSE_TEST = 0b0000;
/// <summary>
/// 设定JTAG默认状态为RUN_TEST_IDLE态 JTAG空闲
/// </summary>
public const UInt32 CMD_JTAG_RUN_TEST = 0b0001;
/// <summary>
/// JTAG进入SHIFTIR循环cycle_num次后回到设定的默认状态
/// </summary>
public const UInt32 CMD_JTAG_LOAD_IR = 0b0010;
/// <summary>
/// JTAG进入SHIFTDR循环cycle_num次后回到设定的默认状态同时输入shift_in
/// </summary>
public const UInt32 CMD_JTAG_LOAD_DR_CAREI = 0b0011;
/// <summary>
/// JTAG进入SHIFTDR循环cycle_num次后回到设定的默认状态同时输出shift_out
/// </summary>
public const UInt32 CMD_JTAG_LOAD_DR_CAREO = 0b0100;
/// <summary>
/// JTAG进入RUN_TEST_IDLE态循环cycle_num次
/// </summary>
public const UInt32 CMD_JTAG_IDLE_DELAY = 0b0101;
}
/// <summary>
/// Jtag控制器
/// </summary>
public class Jtag
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout;
readonly int port;
readonly string address;
private IPEndPoint ep;
/// <summary>
/// Jtag构造函数
/// </summary>
/// <param name="address">目标IP地址</param>
/// <param name="port">目标UDP端口</param>
/// <param name="timeout">超时时间</param>
public Jtag(string address, int port, int timeout = 2000)
{
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
async ValueTask<Result<uint>> ReadFIFO(uint devAddr)
{
var ret = false;
var opts = new SendAddrPackOptions();
opts.BurstType = BurstType.FixedBurst;
opts.BurstLength = 0;
opts.CommandID = 0;
opts.Address = devAddr;
// Read Jtag State Register
opts.IsWrite = false;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send Address Package Failed!"));
// Wait for Read Ack
if (!MsgBus.IsRunning)
return new(new Exception("Message Bus not Working!"));
var retPack = await MsgBus.UDPServer.WaitForDataAsync(address, port);
if (!retPack.IsSuccessful || !retPack.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
var retPackOpts = retPack.Value.Options;
if (retPackOpts.Data is null)
return new(new Exception($"Data is Null, package: {retPackOpts.ToString()}"));
var retPackLen = retPackOpts.Data.Length;
if (retPackLen != 4)
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes"));
return Convert.ToUInt32(Common.Number.BytesToNumber(retPackOpts.Data).Value);
}
public async ValueTask<Result<RecvDataPackage>> WriteFIFO(UInt32 devAddr, UInt32 data)
{
var ret = false;
var opts = new SendAddrPackOptions();
opts.BurstType = BurstType.FixedBurst;
opts.BurstLength = 0;
opts.CommandID = 0;
opts.Address = devAddr;
// Write Jtag State Register
opts.IsWrite = true;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 1st address package failed!"));
// Send Data Package
ret = await UDPClientPool.SendDataPackAsync(ep,
new SendDataPackage(Common.Number.NumberToBytes(data, 4).Value));
if (!ret) return new(new Exception("Send data package failed!"));
// Check Msg Bus
if (!MsgBus.IsRunning)
return new(new Exception("Message bus not working!"));
// Wait for Write Ack
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(address, port);
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
else if (!udpWriteAck.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
// Read Jtag State Register
opts.IsWrite = false;
opts.Address = JtagAddr.STATE;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 2rd Address Package Failed!"));
// Wait for Read Data
var udpDataResp = await MsgBus.UDPServer.WaitForDataAsync(address, port);
if (!udpDataResp.IsSuccessful) return new(udpDataResp.Error);
else if (!udpDataResp.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
return udpDataResp.Value;
}
async ValueTask<Result<RecvDataPackage>> WriteFIFO(UInt32 devAddr, byte[] dataArray)
{
var ret = false;
var opts = new SendAddrPackOptions();
opts.BurstType = BurstType.FixedBurst;
opts.CommandID = 0;
opts.Address = devAddr;
// Check Msg Bus
if (!MsgBus.IsRunning)
return new(new Exception("Message bus not working!"));
var writeTimes = dataArray.Length / (256 * (32 / 8)) + 1;
for (var i = 0; i < writeTimes; i++)
{
var isLastData = i == writeTimes - 1;
// Write Jtag State Register
opts.IsWrite = true;
opts.BurstLength = isLastData ? (byte)(dataArray.Length - i * (256 * (32 / 8)) - 1) : (byte)255;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 1st address package failed!"));
// Send Data Package
ret = await UDPClientPool.SendDataPackAsync(ep,
new SendDataPackage(
isLastData ?
dataArray[(i * (256 * (32 / 8)))..] :
dataArray[(i * (256 * (32 / 8)))..((i + 1) * (256 * (32 / 8)) - 1)]
)
);
if (!ret) return new(new Exception("Send data package failed!"));
// Wait for Write Ack
var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync(address, port);
if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error);
else if (!udpWriteAck.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
}
// Read Jtag State Register
opts.IsWrite = false;
opts.BurstLength = 0;
opts.Address = JtagAddr.STATE;
ret = await UDPClientPool.SendAddrPackAsync(ep, new SendAddrPackage(opts));
if (!ret) return new(new Exception("Send 2rd Address Package Failed!"));
// Wait for Read Data
var udpDataResp = await MsgBus.UDPServer.WaitForDataAsync(address, port);
if (!udpDataResp.IsSuccessful) return new(udpDataResp.Error);
else if (!udpDataResp.Value.IsSuccessful)
return new(new Exception("Send address package failed"));
return udpDataResp.Value;
}
async ValueTask<Result<bool>> WriteFIFO
(UInt32 devAddr, UInt32 data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF)
{
var ret = false;
var retPack = await WriteFIFO(devAddr, data);
if (!retPack.IsSuccessful) return new(retPack.Error);
if (retPack.Value.Options.Data is null)
return new(new Exception($"Data is Null, package: {retPack.Value.Options.ToString()}"));
var retPackLen = retPack.Value.Options.Data.Length;
if (retPackLen != 4)
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes"));
if (Common.Number.BitsCheck(
Common.Number.BytesToNumber(retPack.Value.Options.Data).Value, result, resultMask))
ret = true;
return ret;
}
async ValueTask<Result<bool>> WriteFIFO
(UInt32 devAddr, byte[] data, UInt32 result, UInt32 resultMask = 0xFF_FF_FF_FF)
{
var ret = false;
var retPack = await WriteFIFO(devAddr, data);
if (retPack.Value.Options.Data is null)
return new(new Exception($"Data is Null, package: {retPack.Value.Options.ToString()}"));
var retPackLen = retPack.Value.Options.Data.Length;
if (retPackLen != 4)
return new(new Exception($"RecvDataPackage BodyData Length not Equal to 4: Total {retPackLen} bytes"));
if (Common.Number.BitsCheck(
Common.Number.BytesToNumber(retPack.Value.Options.Data).Value, result, resultMask))
ret = true;
return ret;
}
async ValueTask<Result<bool>> ClearAllRegisters()
{
return await WriteFIFO(JtagAddr.STATE, 0xFF_FF_FF_FF, 0x01_02_02_02, JtagState.ALL_REG);
}
async ValueTask<Result<bool>> ClearWriteDataReg()
{
return await WriteFIFO(JtagAddr.STATE, 0x00_00_11_00, 0x01_00_02_00, JtagState.WRITE_DATA_FIFO | JtagState.CMD_EXEC_FINISH);
}
async ValueTask<Result<bool>> CloseTest()
{
return await WriteFIFO(
JtagAddr.WRITE_CMD,
Common.Number.MultiBitsToNumber(JtagCmd.CMD_JTAG_CLOSE_TEST, JtagCmd.LEN_CMD_JTAG, 0, 28).Value,
0x01_00_00_00, JtagState.CMD_EXEC_FINISH);
}
async ValueTask<Result<bool>> RunTest()
{
return await WriteFIFO(
JtagAddr.WRITE_CMD,
Common.Number.MultiBitsToNumber(JtagCmd.CMD_JTAG_RUN_TEST, JtagCmd.LEN_CMD_JTAG, 0, 28).Value,
0x01_00_00_00, JtagState.CMD_EXEC_FINISH);
}
async ValueTask<Result<bool>> IdleDelay(UInt32 milliseconds)
{
if (milliseconds > Math.Pow(2, 28)) return new(new Exception("Timespan is over 2^28 milliseconds"));
return await WriteFIFO(
JtagAddr.WRITE_CMD,
Common.Number.MultiBitsToNumber(JtagCmd.CMD_JTAG_IDLE_DELAY, JtagCmd.LEN_CMD_JTAG, milliseconds, 28).Value,
0x01_00_00_00, JtagState.CMD_EXEC_FINISH);
}
async ValueTask<Result<bool>> ExecRDCmd(uint command)
{
{
var ret = await WriteFIFO(
JtagAddr.WRITE_DATA,
Common.Number.MultiBitsToNumber(0, 22, command, JtagCmd.LEN_JTAG_DR).Value, 0, 0);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Write JTAG_DR_IDCODE Failed"));
}
{
var ret = await WriteFIFO(
JtagAddr.WRITE_CMD,
Common.Number.MultiBitsToNumber(JtagCmd.CMD_JTAG_LOAD_IR, JtagCmd.LEN_CMD_JTAG, 10, 28).Value,
0x01_00_00_00, JtagState.CMD_EXEC_FINISH);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_IR Failed"));
}
return await ClearWriteDataReg();
}
async ValueTask<Result<bool>> LoadDRCareInput(byte[] bytesArray)
{
var bytesLen = ((uint)(bytesArray.Length * 8));
if (bytesLen > Math.Pow(2, 28)) return new(new Exception("Length is over 2^(28 - 3)"));
{
var ret = await WriteFIFO(
JtagAddr.WRITE_CMD,
Common.Number.MultiBitsToNumber(JtagCmd.CMD_JTAG_LOAD_DR_CAREI, JtagCmd.LEN_CMD_JTAG, bytesLen, 28).Value,
0x01_00_00_00, JtagState.CMD_EXEC_FINISH);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed"));
}
{
var ret = await WriteFIFO(JtagAddr.WRITE_DATA, bytesArray, 0x01_00_00_00, JtagState.CMD_EXEC_FINISH);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Write Data Failed"));
}
return true;
}
async ValueTask<Result<UInt32>> LoadDRCareOutput(UInt32 bytesLen)
{
if (bytesLen > Math.Pow(2, 28)) return new(new Exception("Length is over 2^(28 - 3)"));
var ret = await WriteFIFO(
JtagAddr.WRITE_CMD,
Common.Number.MultiBitsToNumber(JtagCmd.CMD_JTAG_LOAD_DR_CAREO, JtagCmd.LEN_CMD_JTAG, 8 * bytesLen, 28).Value,
0x01_00_00_00, JtagState.CMD_EXEC_FINISH);
if (ret.Value)
return await ReadFIFO(JtagAddr.READ_DATA);
else
return new(new Exception("LoadDRCareo Failed!"));
}
public async ValueTask<Result<uint>> ReadIDCode()
{
// Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace($"Clear up udp server {this.address} receive data");
Result<bool> ret;
ret = await ClearAllRegisters();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Clear All Registers Failed"));
ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
ret = await ExecRDCmd(JtagCmd.JTAG_DR_IDCODE);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_IDCODE Failed"));
ret = await ClearWriteDataReg();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Clear Write Registers Failed"));
var retData = await LoadDRCareOutput(4);
if (!retData.IsSuccessful)
{
return new(new Exception("Get ID Code Failed"));
}
return retData.Value;
}
public async ValueTask<Result<bool>> DownloadBitstream(byte[] bitstream)
{
// Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace($"Clear up udp server {this.address} receive data");
Result<bool> ret;
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
logger.Trace("Jtag initialize");
ret = await ExecRDCmd(JtagCmd.JTAG_DR_JRST);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JRST Failed"));
ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
ret = await ExecRDCmd(JtagCmd.JTAG_DR_CFGI);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_CFGI Failed"));
logger.Trace("Jtag ready to write bitstream");
ret = await IdleDelay(75000);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
ret = await LoadDRCareInput(bitstream);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Load Data Failed"));
logger.Trace("Jtag write bitstream");
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
ret = await RunTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Run Test Failed"));
ret = await ExecRDCmd(JtagCmd.JTAG_DR_JWAKEUP);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Execute Command JTAG_DR_JWAKEUP Failed"));
logger.Trace("Jtag reset device");
ret = await IdleDelay(1000);
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed"));
ret = await CloseTest();
if (!ret.IsSuccessful) return new(ret.Error);
else if (!ret.Value) return new(new Exception("Jtag Close Test Failed"));
logger.Trace("Jtag download bitstream successfully");
return true;
}
}

37
server/src/MsgBus.cs Normal file
View File

@ -0,0 +1,37 @@
/// <summary>
/// 多线程通信总线
/// </summary>
public static class MsgBus
{
private static readonly UDPServer udpServer = new UDPServer(1234);
/// <summary>
/// 获取UDP服务器
/// </summary>
public static UDPServer UDPServer { get { return udpServer; } }
private static bool isRunning = false;
/// <summary>
/// 获取通信总线运行状态
/// </summary>
public static bool IsRunning { get { return isRunning; } }
/// <summary>
/// 通信总线初始化
/// </summary>
/// <returns>无</returns>
public static void Init()
{
udpServer.Start();
isRunning = true;
}
/// <summary>
/// 关闭通信总线
/// </summary>
/// <returns>无</returns>
public static void Exit()
{
udpServer.Stop();
isRunning = false;
}
}

View File

@ -1,41 +0,0 @@
using System.Net;
using Common;
namespace Router
{
class API
{
public static void SendString(string address, int port, string text)
{
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
UDPClientPool.AsyncSendString(endPoint, [text]);
}
public static void SendAddrPackage(
string address,
int port,
WebProtocol.BurstType burstType,
byte commandID,
bool isWrite,
byte burstLength,
UInt32 devAddress)
{
WebProtocol.SendAddrPackOptions opts;
opts.burstType = burstType;
opts.commandID = commandID;
opts.isWrite = isWrite;
opts.burstLength = burstLength;
opts.address = devAddress;
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
UDPClientPool.SendAddrPack(endPoint, new WebProtocol.SendAddrPackage(opts));
}
public static void SendDataPackage(string address, int port, string data)
{
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
UDPClientPool.SendDataPack(endPoint,
new WebProtocol.SendDataPackage(NumberProcessor.StringToBytes(data).Value));
}
}
}

View File

@ -2,78 +2,163 @@ using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
class UDPClientPool /// <summary>
/// UDP客户端发送池
/// </summary>
public class UDPClientPool
{ {
private static IPAddress localhost = IPAddress.Parse("127.0.0.1"); private static IPAddress localhost = IPAddress.Parse("127.0.0.1");
public static void SendString(IPEndPoint endPoint, string[] stringArray) /// <summary>
/// 发送字符串
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="stringArray">字符串数组</param>
/// <returns>是否成功</returns>
public static bool SendString(IPEndPoint endPoint, string[] stringArray)
{ {
var isSuccessful = true;
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
byte[] sendbuf = Encoding.ASCII.GetBytes(stringArray[0]); foreach (var str in stringArray)
socket.SendTo(sendbuf, endPoint);
}
public async static void AsyncSendString(IPEndPoint endPoint, string[] stringArray)
{ {
await Task.Run(() => { SendString(endPoint, stringArray); }); byte[] sendbuf = Encoding.ASCII.GetBytes(str);
var sendLen = socket.SendTo(sendbuf, endPoint);
if (str.Length != sendLen) isSuccessful = false;
} }
public static void SendBytes(IPEndPoint endPoint, byte[] buf) socket.Close();
if (isSuccessful) { return true; }
else { return false; }
}
/// <summary>
/// 异步发送字符串
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="stringArray">字符串数组</param>
/// <returns>是否成功</returns>
public async static ValueTask<bool> SendStringAsync(IPEndPoint endPoint, string[] stringArray)
{
return await Task.Run(() => { return SendString(endPoint, stringArray); });
}
/// <summary>
/// 发送二进制字节
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="buf">二进制字节</param>
/// <returns>是否成功</returns>
public static bool SendBytes(IPEndPoint endPoint, byte[] buf)
{ {
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.SendTo(buf, endPoint); var sendLen = socket.SendTo(buf, endPoint);
socket.Close();
if (sendLen == buf.Length) { return true; }
else { return false; }
} }
public async static void AsyncSendBytes(IPEndPoint endPoint, byte[] buf) /// <summary>
/// 异步发送二进制字节
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="buf">二进制字节</param>
/// <returns>是否成功</returns>
public async static ValueTask<bool> SendBytesAsync(IPEndPoint endPoint, byte[] buf)
{ {
await Task.Run(() => { SendBytes(endPoint, buf); }); return await Task.Run(() => { return SendBytes(endPoint, buf); });
} }
public static void SendAddrPack(IPEndPoint endPoint, WebProtocol.SendAddrPackage pkg) /// <summary>
/// 发送地址包
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="pkg">地址包</param>
/// <returns>是否成功</returns>
public static bool SendAddrPack(IPEndPoint endPoint, WebProtocol.SendAddrPackage pkg)
{ {
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.SendTo(pkg.ToBytes(), endPoint); var sendBytes = pkg.ToBytes();
var sendLen = socket.SendTo(sendBytes, endPoint);
socket.Close();
if (sendLen == sendBytes.Length) { return true; }
else { return false; }
} }
public async static void AsyncSendAddrPack(IPEndPoint endPoint, WebProtocol.SendAddrPackage pkg) /// <summary>
/// 异步发送地址包
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="pkg">地址包</param>
/// <returns>是否成功</returns>
public async static ValueTask<bool> SendAddrPackAsync(IPEndPoint endPoint, WebProtocol.SendAddrPackage pkg)
{ {
await Task.Run(() => { SendAddrPack(endPoint, pkg); }); return await Task.Run(() => { return SendAddrPack(endPoint, pkg); });
} }
public static void SendDataPack(IPEndPoint endPoint, WebProtocol.SendDataPackage pkg)
/// <summary>
/// 发送数据包
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="pkg">数据包</param>
/// <returns>是否成功</returns>
public static bool SendDataPack(IPEndPoint endPoint, WebProtocol.SendDataPackage pkg)
{ {
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.SendTo(pkg.ToBytes(), endPoint); var sendBytes = pkg.ToBytes();
var sendLen = socket.SendTo(sendBytes, endPoint);
socket.Close();
if (sendLen == sendBytes.Length) { return true; }
else { return false; }
} }
public async static void AsyncSendDataPack(IPEndPoint endPoint, WebProtocol.SendDataPackage pkg) /// <summary>
/// 异步发送数据包
/// </summary>
/// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="pkg">数据包</param>
/// <returns>是否成功</returns>
public async static ValueTask<bool> SendDataPackAsync(IPEndPoint endPoint, WebProtocol.SendDataPackage pkg)
{ {
await Task.Run(() => { SendDataPack(endPoint, pkg); }); return await Task.Run(() => { return SendDataPack(endPoint, pkg); });
} }
public static void SendLocalHost(int port, string[] stringArray) /// <summary>
/// 发送字符串到本地
/// </summary>
/// <param name="port">端口</param>
/// <param name="stringArray">字符串数组</param>
/// <returns>是否成功</returns>
public static bool SendStringLocalHost(int port, string[] stringArray)
{ {
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); return SendString(new IPEndPoint(localhost, port), stringArray);
byte[] sendbuf = Encoding.ASCII.GetBytes(stringArray[0]);
IPEndPoint ep = new IPEndPoint(localhost, port);
socket.SendTo(sendbuf, ep);
} }
public static void CycleSendLocalHost(int times, int sleepMilliSeconds, int port, string[] stringArray) /// <summary>
/// 循环发送字符串到本地
/// </summary>
/// <param name="times">发送总次数</param>
/// <param name="sleepMilliSeconds">间隔时间</param>
/// <param name="port">端口</param>
/// <param name="stringArray">字符串数组</param>
/// <returns>是否成功</returns>
public static bool CycleSendStringLocalHost(int times, int sleepMilliSeconds, int port, string[] stringArray)
{ {
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); var isSuccessful = true;
byte[] sendbuf = Encoding.ASCII.GetBytes(stringArray[0]);
IPEndPoint ep = new IPEndPoint(localhost, port);
while (times-- >= 0) while (times-- >= 0)
{ {
socket.SendTo(sendbuf, ep); isSuccessful = SendStringLocalHost(port, stringArray);
if (!isSuccessful) break;
Thread.Sleep(sleepMilliSeconds); Thread.Sleep(sleepMilliSeconds);
} }
return isSuccessful;
} }
} }

View File

@ -1,13 +1,97 @@
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using DotNext;
using DotNext.Threading;
using Newtonsoft.Json;
using WebProtocol;
/// <summary> UDP接受数据包格式 </summary>
public class UDPData
{
/// <summary>
/// 接受到的时间
/// </summary>
public required DateTime DateTime { get; set; }
/// <summary>
/// 发送来源的IP地址
/// </summary>
public required string Address { get; set; }
/// <summary>
/// 发送来源的端口号
/// </summary>
public required int Port { get; set; }
/// <summary>
/// 接受到的数据
/// </summary>
public required byte[] Data { get; set; }
/// <summary>
/// 是否被读取过
/// </summary>
public required bool HasRead { get; set; }
/// <summary>
/// 深度拷贝对象
/// </summary>
/// <returns>UDPData</returns>
public UDPData DeepClone()
{
var cloneData = new byte[this.Data.Length];
Buffer.BlockCopy(this.Data, 0, cloneData, 0, this.Data.Length);
return new UDPData()
{
DateTime = this.DateTime,
Address = new string(this.Address),
Port = this.Port,
Data = cloneData,
HasRead = this.HasRead
};
}
/// <summary>
/// 将UDP Data 转化为Json 格式字符串
/// </summary>
/// <returns>json字符串</returns>
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
/// <summary>
/// UDP 服务器
/// </summary>
public class UDPServer public class UDPServer
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private static Dictionary<string, Queue<UDPData>> udpData = new Dictionary<string, Queue<UDPData>>();
private int listenPort; private int listenPort;
private UdpClient listener; private UdpClient listener;
private IPEndPoint groupEP; private IPEndPoint groupEP;
private bool isRunning = false;
/// <summary>
/// 是否正在工作
/// </summary>
public bool IsRunning { get { return isRunning; } }
/// <summary> UDP 服务器的错误代码 </summary>
public enum ErrorCode
{
Success = 0,
GetNoneAfterTimeout,
ResponseWrong,
NotRecvDataPackage,
}
/// <summary>
/// Construct a udp server with fixed port
/// </summary>
/// <param name="port"> Device UDP Port </param>
/// <returns> UDPServer class </returns>
public UDPServer(int port) public UDPServer(int port)
{ {
// Construction // Construction
@ -21,20 +105,243 @@ public class UDPServer
{ {
Console.WriteLine(e.ToString()); Console.WriteLine(e.ToString());
throw new ArgumentException( throw new ArgumentException(
$"Not currect port num: {port}", $"Failed to set up server with this port: {port}",
nameof(port) nameof(port)
); );
} }
} }
/// <summary>
/// 异步寻找目标发送的内容
/// </summary>
/// <param name="ipAddr"> 目标IP地址 </param>
/// <param name="timeout">超时时间</param>
/// <param name="callerName">调用函数名称</param>
/// <param name="callerLineNum">调用函数位置</param>
/// <returns>
/// 异步Optional 数据包:
/// Optional 为空时,表明找不到数据;
/// Optional 存在时,为最先收到的数据
/// </returns>
public async ValueTask<Optional<UDPData>> FindDataAsync(
string ipAddr, int timeout = 1000,
[CallerMemberName] string callerName = "",
[CallerLineNumber] int callerLineNum = 0)
{
UDPData? data = null;
logger.Debug($"Caller \"{callerName}|{callerLineNum}\": Try to find {ipAddr} UDP Data");
var startTime = DateTime.Now;
var isTimeout = false;
var timeleft = TimeSpan.FromMilliseconds(timeout);
while (!isTimeout)
{
var elapsed = DateTime.Now - startTime;
isTimeout = elapsed >= TimeSpan.FromMilliseconds(timeout);
if (isTimeout) break;
timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
using (await udpData.AcquireWriteLockAsync(timeleft))
{
if (udpData.ContainsKey(ipAddr) &&
udpData.TryGetValue(ipAddr, out var dataQueue) &&
dataQueue.Count > 0)
{
data = dataQueue.Dequeue();
logger.Debug($"Find UDP Data: {data.ToString()}");
break;
}
}
}
if (data is null)
{
logger.Trace("Get nothing even after time out");
return new(null);
}
else
{
return new(data.DeepClone());
}
}
/// <summary>
/// 获取还未被读取的数据列表
/// </summary>
/// <param name="ipAddr">IP地址</param>
/// <param name="timeout">超时时间</param>
/// <returns>数据列表</returns>
public async ValueTask<Optional<List<UDPData>>> GetDataArrayAsync(string ipAddr, int timeout = 1000)
{
List<UDPData>? data = null;
var startTime = DateTime.Now;
var isTimeout = false;
var timeleft = TimeSpan.FromMilliseconds(timeout);
while (!isTimeout)
{
var elapsed = DateTime.Now - startTime;
isTimeout = elapsed >= TimeSpan.FromMilliseconds(timeout);
if (isTimeout) break;
timeleft = TimeSpan.FromMilliseconds(timeout) - elapsed;
using (await udpData.AcquireReadLockAsync(timeleft))
{
if (udpData.ContainsKey(ipAddr) &&
udpData.TryGetValue(ipAddr, out var dataQueue) &&
dataQueue.Count > 0)
{
data = dataQueue.ToList();
logger.Debug($"Find UDP Data Array: {JsonConvert.SerializeObject(data)}");
break;
}
}
}
if (data is null)
{
logger.Trace("Get nothing even after time out");
return new(null);
}
else
{
return new(data);
}
}
/// <summary>
/// 异步等待写响应
/// </summary>
/// <param name="address">IP地址</param>
/// <param name="port">UDP 端口</param>
/// <param name="timeout">超时时间范围</param>
/// <returns>接收响应包</returns>
public async ValueTask<Result<WebProtocol.RecvRespPackage>> WaitForAckAsync
(string address, int port = -1, int timeout = 1000)
{
var data = await FindDataAsync(address, timeout);
if (!data.HasValue)
return new(new Exception("Get None even after time out!"));
var recvData = data.Value;
if (recvData.Address != address || (port > 0 && recvData.Port != port))
return new(new Exception("Receive Data From Wrong Board!"));
var retPack = WebProtocol.RecvRespPackage.FromBytes(recvData.Data);
if (!retPack.IsSuccessful)
return new(new Exception("Not RecvDataPackage!", retPack.Error));
return retPack.Value;
}
/// <summary>
/// 异步等待数据
/// </summary>
/// <param name="address">IP地址</param>
/// <param name="port">UDP 端口</param>
/// <param name="timeout">超时时间范围</param>
/// <returns>接收数据包</returns>
public async ValueTask<Result<RecvDataPackage>> WaitForDataAsync
(string address, int port = -1, int timeout = 1000)
{
var data = await FindDataAsync(address, timeout);
if (!data.HasValue)
return new(new Exception("Get None even after time out!"));
var recvData = data.Value;
if (recvData.Address != address || (port >= 0 && recvData.Port != port))
return new(new Exception("Receive Data From Wrong Board!"));
var retPack = WebProtocol.RecvDataPackage.FromBytes(recvData.Data);
if (!retPack.IsSuccessful)
return new(new Exception("Not RecvDataPackage!", retPack.Error));
return retPack.Value;
}
private void ReceiveHandler(IAsyncResult res) private void ReceiveHandler(IAsyncResult res)
{ {
logger.Trace("Enter handler");
var remoteEP = new IPEndPoint(IPAddress.Any, listenPort); var remoteEP = new IPEndPoint(IPAddress.Any, listenPort);
byte[] bytes = listener.EndReceive(res, ref remoteEP); byte[] bytes = listener.EndReceive(res, ref remoteEP);
var sign = bytes[0]; // Handle RemoteEP
if (remoteEP is null)
{
logger.Debug($"Receive Data from Unknown at {DateTime.Now.ToString()}:");
logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
goto BEGIN_RECEIVE;
}
string recvData;
// Handle Package
var udpData = RecordUDPData(bytes, remoteEP);
PrintData(udpData);
BEGIN_RECEIVE:
listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
}
private bool SendBytes(IPEndPoint endPoint, byte[] buf)
{
var sendLen = listener.Send(buf, endPoint);
if (sendLen == buf.Length) { return true; }
else { return false; }
}
private bool SendString(IPEndPoint endPoint, string text)
{
byte[] buf = Encoding.ASCII.GetBytes(text);
var sendLen = listener.Send(buf, endPoint);
if (sendLen == buf.Length) { return true; }
else { return false; }
}
private UDPData RecordUDPData(byte[] bytes, IPEndPoint remoteEP)
{
var remoteAddress = remoteEP.Address.ToString();
var remotePort = remoteEP.Port;
var data = new UDPData()
{
Address = remoteAddress,
Port = remotePort,
Data = bytes,
DateTime = DateTime.Now,
HasRead = false,
};
using (udpData.AcquireWriteLock())
{
// Record UDP Receive Data
if (udpData.ContainsKey(remoteAddress) && udpData.TryGetValue(remoteAddress, out var dataQueue))
{
dataQueue.Enqueue(data);
logger.Trace("Receive data from old client");
}
else
{
var queue = new Queue<UDPData>();
queue.Enqueue(data);
udpData.Add(remoteAddress, queue);
logger.Trace("Receive data from new client");
}
}
return data;
}
/// <summary>
/// 输出UDP Data到log中
/// </summary>
/// <param name="data">UDP数据</param>
public void PrintData(UDPData data)
{
var bytes = data.Data;
var sign = bytes[0];
string recvData = "";
if (sign == (byte)WebProtocol.PackSign.SendAddr) if (sign == (byte)WebProtocol.PackSign.SendAddr)
{ {
var resData = WebProtocol.SendAddrPackage.FromBytes(bytes); var resData = WebProtocol.SendAddrPackage.FromBytes(bytes);
@ -43,37 +350,81 @@ public class UDPServer
else else
recvData = resData.Error.ToString(); recvData = resData.Error.ToString();
} }
else if (sign == (byte)WebProtocol.PackSign.SendData) else if (sign == (byte)WebProtocol.PackSign.SendData) { }
{
recvData = "";
}
else if (sign == (byte)WebProtocol.PackSign.RecvData) else if (sign == (byte)WebProtocol.PackSign.RecvData)
{ {
recvData = ""; var resData = WebProtocol.RecvDataPackage.FromBytes(bytes);
if (resData.IsSuccessful)
recvData = resData.Value.Options.ToString();
else
recvData = resData.Error.ToString();
} }
else if (sign == (byte)WebProtocol.PackSign.RecvResp) else if (sign == (byte)WebProtocol.PackSign.RecvResp)
{ {
recvData = ""; var resData = WebProtocol.RecvRespPackage.FromBytes(bytes);
if (resData.IsSuccessful)
recvData = resData.Value.Options.ToString();
else
recvData = resData.Error.ToString();
} }
else else
{ {
recvData = Encoding.ASCII.GetString(bytes, 0, bytes.Length); recvData = Encoding.ASCII.GetString(bytes, 0, bytes.Length);
} }
string remoteStr = (remoteEP is null) ? "Unknown" : $"{remoteEP.Address.ToString()}:{remoteEP.Port.ToString()}"; logger.Debug($"Receive Data from {data.Address}:{data.Port} at {data.DateTime.ToString()}:");
Console.WriteLine($"Receive Data from {remoteStr} at {DateTime.Now.ToString()}:"); logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
Console.WriteLine($"Original Data: {BitConverter.ToString(bytes).Replace("-", " ")}"); if (recvData.Length != 0) logger.Debug($" Decoded Data : {recvData}");
if (recvData.Length != 0) Console.WriteLine(recvData);
Console.WriteLine();
listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
} }
/// <summary>
/// 将所有数据输出到log中
/// </summary>
/// <returns> void </returns>
public void PrintAllData()
{
using (udpData.AcquireReadLock())
{
logger.Debug("Ready Data:");
foreach (var ip in udpData)
{
foreach (var data in ip.Value)
{
logger.Debug(data.ToString());
}
}
}
}
/// <summary>
/// 清空指定IP地址的数据
/// </summary>
/// <param name="ipAddr">IP地址</param>
/// <returns>无</returns>
public async Task ClearUDPData(string ipAddr)
{
using (await udpData.AcquireWriteLockAsync())
{
if (udpData.ContainsKey(ipAddr) &&
udpData.TryGetValue(ipAddr, out var dataQueue) &&
dataQueue.Count > 0)
{
dataQueue.Clear();
}
}
}
/// <summary>
/// Start UDP Server
/// </summary>
/// <returns>None</returns>
public void Start() public void Start()
{ {
try try
{ {
listener.BeginReceive(new AsyncCallback(ReceiveHandler), null); this.listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
} }
catch (Exception e) catch (Exception e)
{ {
@ -81,13 +432,18 @@ public class UDPServer
} }
finally finally
{ {
this.isRunning = true;
} }
} }
/// <summary>
/// Close UDP Server
/// </summary>
/// <returns>None</returns>
public void Stop() public void Stop()
{ {
listener.Close(); this.listener.Close();
this.isRunning = false;
} }
} }

View File

@ -3,41 +3,123 @@ using Newtonsoft.Json;
namespace WebProtocol namespace WebProtocol
{ {
/// <summary> The Sign of Package </summary>
public enum PackSign public enum PackSign
{ {
/// <summary> Package: Send Read or Write Address to Device </summary>
SendAddr = 0x00, SendAddr = 0x00,
/// <summary> Package: Send Data Which Update the flash of Device</summary>
SendData = 0xFF, SendData = 0xFF,
/// <summary> </summary>
RecvData = 0x0F, RecvData = 0x0F,
/// <summary> </summary>
RecvResp = 0xF0, RecvResp = 0xF0,
} }
/// <summary> Package Burst Type </summary>
public enum BurstType public enum BurstType
{ {
/// <summary> Extended Type </summary>
ExtendBurst = 0b00, ExtendBurst = 0b00,
/// <summary> Fixed Type </summary>
FixedBurst = 0b01, FixedBurst = 0b01,
} }
public struct SendAddrPackOptions
{
public BurstType burstType;
public byte commandID;
public bool isWrite;
public byte burstLength;
public UInt32 address;
/// <summary> Package options which to send address to read or write </summary>
public class SendAddrPackOptions
{
/// <summary>
/// 突发类型
/// </summary>
/// <example>0</example>
public BurstType BurstType { get; set; }
/// <summary>
/// 任务ID
/// </summary>
/// <example>1</example>
public byte CommandID { get; set; }
/// <summary>
/// 标识写入还是读取
/// </summary>
/// <example>true</example>
public bool IsWrite { get; set; }
/// <summary>
/// 突发长度0是32bits255是32bits x 256
/// </summary>
/// <example>255</example>
public byte BurstLength { get; set; }
/// <summary>
/// 目标地址
/// </summary>
/// <example>0</example>
public UInt32 Address { get; set; }
/// <summary>
/// 转换为Json格式字符串
/// </summary>
/// <returns>Json String</returns>
public override string ToString() public override string ToString()
{ {
return JsonConvert.SerializeObject(this); return JsonConvert.SerializeObject(this);
} }
} }
public struct RecvPackOptions /// <summary> Package Options which to receive from boards </summary>
public class RecvPackOptions
{ {
public byte commandID; /// <summary> 数据包类型 </summary>
public bool isSuccess; public enum PackType
{
/// <summary> 读响应包 </summary>
ReadResp,
/// <summary> 写响应包 </summary>
WriteResp
};
/// <summary>
/// 数据包类型
/// </summary>
/// <example>0</example>
public PackType Type { get; set; }
/// <summary>
/// Task ID
/// </summary>
/// <example>0</example>
public byte CommandID { get; set; }
/// <summary>
/// Whether is succeed to finish command
/// </summary>
/// <example>true</example>
public bool IsSuccess { get; set; }
/// <summary>
/// Return Data
/// </summary>
/// <example>[]</example>
public byte[]? Data { get; set; }
/// <summary>
/// Convert to Json String
/// </summary>
/// <returns> Json String </returns>
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
} }
public struct SendAddrPackage /// <summary> Server->FPGA 地址包 </summary>
public class SendAddrPackage
{ {
readonly byte sign = (byte)PackSign.SendAddr; readonly byte sign = (byte)PackSign.SendAddr;
readonly byte commandType; readonly byte commandType;
@ -45,24 +127,44 @@ namespace WebProtocol
readonly byte _reserved = 0; readonly byte _reserved = 0;
readonly UInt32 address; readonly UInt32 address;
/// <summary>
/// 使用地址包选项构造地址包
/// </summary>
/// <param name="opts"> 地址包选项 </param>
public SendAddrPackage(SendAddrPackOptions opts) public SendAddrPackage(SendAddrPackOptions opts)
{ {
byte byteBurstType = Convert.ToByte((byte)opts.burstType << 5); byte byteBurstType = Convert.ToByte((byte)opts.BurstType << 6);
byte byteCommandID = Convert.ToByte((opts.commandID & 0x03) << 3); byte byteCommandID = Convert.ToByte((opts.CommandID & 0x03) << 4);
byte byteIsWrite = (opts.isWrite ? (byte)0x01 : (byte)0x00); byte byteIsWrite = (opts.IsWrite ? (byte)0x01 : (byte)0x00);
this.commandType = Convert.ToByte(byteBurstType | byteCommandID | byteIsWrite); this.commandType = Convert.ToByte(byteBurstType | byteCommandID | byteIsWrite);
this.burstLength = opts.burstLength; this.burstLength = opts.BurstLength;
this.address = opts.address; this.address = opts.Address;
} }
public SendAddrPackage(BurstType burstType, byte commandID, bool isWrite) /// <summary>
/// 使用完整参数构造地址包
/// </summary>
/// <param name="burstType"> 突发类型 </param>
/// <param name="commandID"> 任务ID </param>
/// <param name="isWrite"> 是否是写数据 </param>
/// <param name="burstLength"> 突发长度 </param>
/// <param name="address"> 设备地址 </param>
public SendAddrPackage(BurstType burstType, byte commandID, bool isWrite, byte burstLength, UInt32 address)
{ {
byte byteBurstType = Convert.ToByte((byte)burstType << 5); byte byteBurstType = Convert.ToByte((byte)burstType << 6);
byte byteCommandID = Convert.ToByte((commandID & 0x03) << 3); byte byteCommandID = Convert.ToByte((commandID & 0x03) << 4);
byte byteIsWrite = (isWrite ? (byte)0x01 : (byte)0x00); byte byteIsWrite = (isWrite ? (byte)0x01 : (byte)0x00);
this.commandType = Convert.ToByte(byteBurstType | byteCommandID | byteIsWrite); this.commandType = Convert.ToByte(byteBurstType | byteCommandID | byteIsWrite);
this.burstLength = burstLength;
this.address = address;
} }
/// <summary>
/// 使用二进制参数构建地址包
/// </summary>
/// <param name="commandType">二进制命令类型</param>
/// <param name="burstLength">突发长度</param>
/// <param name="address">写入或读取的地址</param>
public SendAddrPackage(byte commandType, byte burstLength, UInt32 address) public SendAddrPackage(byte commandType, byte burstLength, UInt32 address)
{ {
this.commandType = commandType; this.commandType = commandType;
@ -70,6 +172,28 @@ namespace WebProtocol
this.address = address; this.address = address;
} }
/// <summary>
/// 获取对应地址包选项
/// </summary>
public SendAddrPackOptions Options
{
get
{
return new SendAddrPackOptions()
{
Address = this.address,
BurstLength = this.burstLength,
BurstType = (BurstType)(this.commandType >> 6),
CommandID = Convert.ToByte((this.commandType >> 4) & 0b11),
IsWrite = Convert.ToBoolean(this.commandType & 1)
};
}
}
/// <summary>
/// 将地址包转化为字节数组
/// </summary>
/// <returns> 字节数组 </returns>
public byte[] ToBytes() public byte[] ToBytes()
{ {
var arr = new byte[8]; var arr = new byte[8];
@ -78,58 +202,82 @@ namespace WebProtocol
arr[2] = burstLength; arr[2] = burstLength;
arr[3] = _reserved; arr[3] = _reserved;
var bytesAddr = Common.NumberProcessor.NumberToBytes(address, 4).Value; var bytesAddr = Common.Number.NumberToBytes(address, 4).Value;
Array.Copy(bytesAddr, 0, arr, 4, bytesAddr.Length); Array.Copy(bytesAddr, 0, arr, 4, bytesAddr.Length);
return arr; return arr;
} }
/// <summary>
/// 讲地址包转化为Json格式的地址包选项
/// </summary>
/// <returns> 字符串 </returns>
public override string ToString() public override string ToString()
{ {
SendAddrPackOptions opts; var opts = new SendAddrPackOptions();
opts.burstType = (BurstType)(commandType >> 5); opts.BurstType = (BurstType)(commandType >> 6);
opts.commandID = Convert.ToByte((commandType >> 3) & 0b0011); opts.CommandID = Convert.ToByte((commandType >> 4) & 0b0011);
opts.isWrite = Convert.ToBoolean(commandType & 0x01); opts.IsWrite = Convert.ToBoolean(commandType & 0x01);
opts.burstLength = burstLength; opts.BurstLength = burstLength;
opts.address = address; opts.Address = address;
return JsonConvert.SerializeObject(opts); return JsonConvert.SerializeObject(opts);
} }
/// <summary>
/// 根据字节数组构建地址包
/// </summary>
/// <param name="bytes">字节数组</param>
/// <param name="checkSign">是否校验地址包包头</param>
/// <returns>地址包</returns>
public static Result<SendAddrPackage> FromBytes(byte[] bytes, bool checkSign = true) public static Result<SendAddrPackage> FromBytes(byte[] bytes, bool checkSign = true)
{ {
if (bytes.Length != 8) if (bytes.Length != 8)
{ {
throw new ArgumentException( return new(new ArgumentException(
"Bytes are not equal to 8 bytes.", "Bytes are not equal to 8 bytes.",
nameof(bytes) nameof(bytes)
); ));
} }
if (checkSign && bytes[0] != (byte)PackSign.SendAddr) if (checkSign && bytes[0] != (byte)PackSign.SendAddr)
{ {
throw new ArgumentException( return new(new ArgumentException(
"The sign of bytes is not SendAddr Package, but you can disable it by set checkSign false.", "The sign of bytes is not SendAddr Package, but you can disable it by set checkSign false.",
nameof(bytes) nameof(bytes)
); ));
} }
var address = Common.NumberProcessor.BytesToNumber(bytes[4..]).Value; var address = Common.Number.BytesToNumber(bytes[4..]).Value;
return new SendAddrPackage(bytes[1], bytes[2], Convert.ToUInt32(address)); return new SendAddrPackage(bytes[1], bytes[2], Convert.ToUInt32(address));
} }
} }
/// <summary> 数据包 </summary>
public struct SendDataPackage public struct SendDataPackage
{ {
readonly byte sign = (byte)PackSign.SendData; readonly byte sign = (byte)PackSign.SendData;
readonly byte[] _reserved = new byte[3]; readonly byte[] _reserved = new byte[3];
readonly byte[] bodyData; readonly byte[] bodyData;
/// <summary>
/// 根据数据内容构建数据包
/// </summary>
/// <param name="bodyData">数据</param>
public SendDataPackage(byte[] bodyData) public SendDataPackage(byte[] bodyData)
{ {
if (bodyData.Length > 256 * (32 / 8))
throw new Exception("The data of SendDataPackage can't over 256 * 32bits");
this.bodyData = bodyData; this.bodyData = bodyData;
_ = _reserved;
} }
/// <summary>
/// 将数据包转化为字节数组
/// </summary>
/// <returns>字节数组</returns>
public byte[] ToBytes() public byte[] ToBytes()
{ {
var bodyDataLen = bodyData.Length; var bodyDataLen = bodyData.Length;
@ -145,6 +293,7 @@ namespace WebProtocol
} }
/// <summary> FPGA->Server 读响应包 </summary>
public struct RecvDataPackage public struct RecvDataPackage
{ {
readonly byte sign = (byte)PackSign.RecvData; readonly byte sign = (byte)PackSign.RecvData;
@ -153,23 +302,109 @@ namespace WebProtocol
readonly byte _reserved = 0; readonly byte _reserved = 0;
readonly byte[] bodyData; readonly byte[] bodyData;
/// <summary>
/// FPGA->Server 读响应包
/// 构造函数
/// </summary>
/// <param name="commandID"> 任务ID号 </param>
/// <param name="resp"> 读响应包响应 </param>
/// <param name="bodyData"> 数据 </param>
public RecvDataPackage(byte commandID, byte resp, byte[] bodyData) public RecvDataPackage(byte commandID, byte resp, byte[] bodyData)
{ {
this.commandID = commandID; this.commandID = commandID;
this.resp = resp; this.resp = resp;
this.bodyData = bodyData; this.bodyData = bodyData;
_ = this.sign;
_ = this._reserved;
} }
public RecvPackOptions Options() /// <summary>
/// FPGA->Server 读响应包
/// 构造函数
/// </summary>
/// <param name="commandID"> 任务ID号 </param>
/// <param name="isSuccess">是否读取成功</param>
/// <param name="bodyData"> 数据 </param>
public RecvDataPackage(byte commandID, bool isSuccess, byte[] bodyData)
{ {
RecvPackOptions opts; this.commandID = commandID;
opts.commandID = commandID; this.resp = Convert.ToByte(isSuccess);
opts.isSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? true : false); this.bodyData = bodyData;
}
/// <summary>
/// 通过接受包选项构建读响应包
/// </summary>
/// <param name="opts">接收包(读响应包和写响应包)选项</param>
public RecvDataPackage(RecvPackOptions opts)
{
this.commandID = opts.CommandID;
this.resp = Convert.ToByte(opts.IsSuccess ? 0b10 : 0b00);
this.bodyData = opts.Data ?? (byte[])[0, 0, 0, 0];
}
/// <summary>
/// 获取读响应包选项
/// </summary>
public RecvPackOptions Options
{
get
{
var opts = new RecvPackOptions();
opts.Type = RecvPackOptions.PackType.ReadResp;
opts.CommandID = commandID;
opts.IsSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? false : true);
opts.Data = bodyData;
return opts; return opts;
} }
} }
/// <summary>
/// 读取是否成功
/// </summary>
public bool IsSuccessful
{
get { return Convert.ToBoolean((resp >> 1) == 0b01 ? false : true); }
}
/// <summary>
/// 从字节数组构建读响应包
/// </summary>
/// <param name="bytes">字节数组</param>
/// <returns>读响应包</returns>
public static Result<RecvDataPackage> FromBytes(byte[] bytes)
{
if (bytes[0] != (byte)PackSign.RecvData)
return new(new ArgumentException(
$"The sign of bytes is not RecvData Package, Sign: 0x{BitConverter.ToString([bytes[0]])}",
nameof(bytes)
));
return new RecvDataPackage(bytes[1], bytes[2], bytes[4..]);
}
/// <summary>
/// 将数据包转化为字节数组
/// </summary>
/// <returns>字节数组</returns>
public byte[] ToBytes()
{
var bodyDataLen = bodyData.Length;
var arr = new byte[4 + bodyDataLen];
arr[0] = this.sign;
arr[1] = this.commandID;
arr[2] = this.resp;
Array.Copy(bodyData, 0, arr, 4, bodyDataLen);
return arr;
}
}
/// <summary> 写响应包 </summary>
public struct RecvRespPackage public struct RecvRespPackage
{ {
readonly byte sign = (byte)PackSign.RecvResp; readonly byte sign = (byte)PackSign.RecvResp;
@ -177,19 +412,94 @@ namespace WebProtocol
readonly byte resp; readonly byte resp;
readonly byte _reserved = 0; readonly byte _reserved = 0;
/// <summary>
/// 构建写响应包
/// </summary>
/// <param name="commandID">任务ID</param>
/// <param name="resp">写响应</param>
public RecvRespPackage(byte commandID, byte resp) public RecvRespPackage(byte commandID, byte resp)
{ {
this.commandID = commandID; this.commandID = commandID;
this.resp = resp; this.resp = resp;
_ = this.sign;
_ = this._reserved;
} }
public RecvPackOptions Options() /// <summary>
/// 构建写响应包
/// </summary>
/// <param name="commandID">任务ID</param>
/// <param name="isSuccess">是否写成功</param>
public RecvRespPackage(byte commandID, bool isSuccess)
{ {
RecvPackOptions opts; this.commandID = commandID;
opts.commandID = commandID; this.resp = Convert.ToByte(isSuccess);
opts.isSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? true : false); }
/// <summary>
/// 通过接受包选项构建写响应包
/// </summary>
/// <param name="opts">接收包(读响应包和写响应包)选项</param>
public RecvRespPackage(RecvPackOptions opts)
{
this.commandID = opts.CommandID;
this.resp = Convert.ToByte(opts.IsSuccess ? 0b10 : 0b00);
}
/// <summary>
/// 获取写响应包选项
/// </summary>
public RecvPackOptions Options
{
get
{
var opts = new RecvPackOptions();
opts.Type = RecvPackOptions.PackType.WriteResp;
opts.CommandID = commandID;
opts.IsSuccess = Convert.ToBoolean((resp >> 1) == 0b01 ? false : true);
opts.Data = null;
return opts; return opts;
} }
} }
/// <summary>
/// 写入是否成功
/// </summary>
public bool IsSuccessful
{
get { return Convert.ToBoolean((resp >> 1) == 0b01 ? false : true); }
}
/// <summary>
/// 从字节数组构建写响应包
/// </summary>
/// <param name="bytes">字节数组</param>
/// <returns>写响应包</returns>
public static Result<RecvRespPackage> FromBytes(byte[] bytes)
{
if (bytes[0] != (byte)PackSign.RecvResp)
return new(new ArgumentException(
$"The sign of bytes is not RecvResp Package, Sign: 0x{BitConverter.ToString([bytes[0]])}",
nameof(bytes)
));
return new RecvRespPackage(bytes[1], bytes[2]);
}
/// <summary>
/// 将数据包转化为字节数组
/// </summary>
/// <returns>字节数组</returns>
public byte[] ToBytes()
{
var arr = new byte[4];
arr[0] = this.sign;
arr[1] = this.commandID;
arr[2] = this.resp;
return arr;
}
}
} }

View File

@ -1,17 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import iconMenu from "./assets/menu.svg";
import Sidebar from "./components/Sidebar.vue";
import { useThemeStore } from "./stores/theme"; import { useThemeStore } from "./stores/theme";
const theme = useThemeStore(); const theme = useThemeStore();
const items = [
{ id: 1, icon: iconMenu, text: "用户界面", page: "/user" },
{ id: 2, icon: iconMenu, text: "ComponentTest", page: "/test" },
{ id: 3, icon: iconMenu, text: "JtagTest", page: "/test/jtag" },
];
</script> </script>
<template> <template>
<div :data-theme="theme.currentTheme"> <div :data-theme="theme.currentTheme">
<header> <header class="relative">
<RouterLink to="/user"> Go to User</RouterLink> <div class="fixed left-0 top-0 z-50">
<RouterLink to="/login"> Go to Login</RouterLink> <Sidebar :items="items" />
<router-link to="/test"> Go to Test</router-link> </div>
</header> </header>
<div></div>
<main> <main>
<RouterView /> <RouterView />

View File

@ -1,16 +0,0 @@
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/router';
export const client = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3002',
// You can pass any HTTP headers you wish here
async headers() {
return {
authorization: document.cookie,
};
},
}),
],
});

View File

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

View File

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

View File

@ -1,30 +1,29 @@
<template> <template>
<div class="card card-dash shadow-xl w-90 h-60"> <div class="card card-dash shadow-xl w-90 h-60">
<div class="card-body flex"> <div class="card-body flex">
<!-- Title --> <!-- Title -->
<h1 class="card-title place-self-center font-bold text-2xl">上传比特流文件</h1> <h1 class="card-title place-self-center font-bold text-2xl">
上传比特流文件
</h1>
<!-- Input File --> <!-- Input File -->
<fieldset class="fieldset w-full"> <fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">选择或拖拽上传文件</legend> <legend class="fieldset-legend text-sm">选择或拖拽上传文件</legend>
<input type="file" class="file-input" @change="handleFileChange" /> <input type="file" class="file-input" @change="handleFileChange" />
<label class="fieldset-label">Max size 2MB</label> <label class="fieldset-label">文件最大容量: 2MB</label>
</fieldset> </fieldset>
<!-- Upload Button --> <!-- Upload Button -->
<div class="card-actions"> <div class="card-actions">
<button @click="uploadBitStream" class="btn btn-primary grow">上传</button> <button @click="uploadBitStream" class="btn btn-primary grow">
上传
</button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { client } from '@/client';
import { TRPCClientError } from '@trpc/client';
var bitstream = null; var bitstream = null;
function handleFileChange(event: Event): void { function handleFileChange(event: Event): void {
@ -32,7 +31,7 @@ function handleFileChange(event: Event): void {
const file = target.files?.[0]; // const file = target.files?.[0]; //
if (!file) { if (!file) {
console.error('未选择文件'); console.error("未选择文件");
return; return;
} }
@ -41,26 +40,9 @@ function handleFileChange(event: Event): void {
console.log(bitstream); console.log(bitstream);
} }
async function uploadBitStream() { async function uploadBitStream() {}
try {
const serverStatus = await client.api.status.query();
if (serverStatus != "OK") {
throw new Error("Server Busy...")
}
} catch (error) { function checkFileType(file: File) {}
if (error instanceof TRPCClientError) {
console.error("Can't connect to Server!")
} else {
console.error(error)
}
}
}
function checkFileType(file: File) {
}
</script> </script>
<style scoped lang="postcss"> <style scoped lang="postcss">

View File

@ -46,30 +46,31 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from "vue";
interface Props { interface Props {
width?: string | number width?: string | number;
height?: string | number height?: string | number;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
width: 160, width: 160,
height: 160, height: 160,
}) });
const btnStatus = ref([false, false, false, false, false, false]) const btnStatus = ref([false, false, false, false, false, false]);
const btnLocation = computed(() => { const btnLocation = computed(() => {
return btnStatus.value.map((status) => { return status ? 7.025 : 8.325 }) return btnStatus.value.map((status) => {
}) return status ? 7.025 : 8.325;
});
});
function setBtnStatus(btnNum: number, isOn: boolean): void { function setBtnStatus(btnNum: number, isOn: boolean): void {
btnStatus.value[btnNum] = isOn btnStatus.value[btnNum] = isOn;
} }
function toggleBtnStatus(btnNum: number): void { function toggleBtnStatus(btnNum: number): void {
btnStatus.value[btnNum] = !btnStatus.value[btnNum] btnStatus.value[btnNum] = !btnStatus.value[btnNum];
} }
</script> </script>
<style scoped lang="postcss"> <style scoped lang="postcss">

View File

@ -1,18 +1,20 @@
import { createMemoryHistory, createRouter } from 'vue-router' import { createMemoryHistory, createRouter } from "vue-router";
import LoginView from "@/views/LoginView.vue" import LoginView from "../views/LoginView.vue";
import UserView from '@/views/UserView.vue' import UserView from "../views/UserView.vue";
import TestView from '@/views/TestView.vue' import TestView from "../views/TestView.vue";
import JtagTest from "../views/JtagTest.vue";
const routes = [ const routes = [
{ path: "/", redirect: "/user" }, { path: "/", redirect: "/user" },
{ path: "/login", name: "Login", component: LoginView }, { path: "/login", name: "Login", component: LoginView },
{ path: "/user", name: "User", component: UserView }, { path: "/user", name: "User", component: UserView },
{ path: "/test", name: "Test", component: TestView }, { path: "/test", name: "Test", component: TestView },
] { path: "/test/jtag", name:"JtagTest", component: JtagTest}
];
const router = createRouter({ const router = createRouter({
history: createMemoryHistory(), history: createMemoryHistory(),
routes, routes,
}) });
export default router export default router;

29
src/stores/sidebar.ts Normal file
View File

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

16
src/views/JtagTest.vue Normal file
View File

@ -0,0 +1,16 @@
<template>
<div class="h-screen w-screen flex justify-center">
<div class="h-full w-32"></div>
<div class="h-full w-[70%] shadow-2xl flex">
<button class="btn btn-primary h-10 w-30">获取ID Code</button>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style scoped lang="postcss">
@import "../assets/main.css";
</style>

View File

@ -7,7 +7,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import LoginCard from '@/components/LoginCard.vue'; import LoginCard from "@/components/LoginCard.vue";
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -2,16 +2,16 @@
<div class="w-screen h-screen"> <div class="w-screen h-screen">
<!-- <Switch width="720" height="720" /> --> <!-- <Switch width="720" height="720" /> -->
<MechanicalButton width="720" height="720" /> <MechanicalButton width="720" height="720" />
<PopButton></PopButton>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import MechanicalButton from '@/components/equipments/MechanicalButton.vue'; import PopButton from "@/components/PopButton.vue";
import Switch from '@/components/equipments/Switch.vue'; import MechanicalButton from "@/components/equipments/MechanicalButton.vue";
// import Switch from "@/components/equipments/Switch.vue";
</script> </script>
<style scoped lang="postcss"> <style scoped lang="postcss">
@import "../assets/main.css" @import "../assets/main.css";
</style> </style>

View File

@ -1,10 +1,6 @@
<template> <template>
<header> <header></header>
</header>
<main class="relative"> <main class="relative">
<div class="fixed left-0 top-0 z-50">
<Sidebar />
</div>
<div class="w-screen h-screen flex items-center justify-center"> <div class="w-screen h-screen flex items-center justify-center">
<UploadCard /> <UploadCard />
</div> </div>
@ -12,10 +8,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import UploadCard from '@/components/UploadCard.vue'; import UploadCard from "@/components/UploadCard.vue";
import Sidebar from '../components/Sidebar.vue' import Sidebar from "../components/Sidebar.vue";
</script> </script>
<style scoped> <style scoped>
@import "../assets/main.css" @import "../assets/main.css";
</style> </style>

View File

@ -6,9 +6,6 @@
}, },
{ {
"path": "./tsconfig.app.json" "path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.bun.json"
} }
] ]
} }