diff --git a/flake.nix b/flake.nix index 7f9b44b..4c86967 100644 --- a/flake.nix +++ b/flake.nix @@ -7,7 +7,7 @@ let supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { - pkgs = import nixpkgs { + pkgs = import nixpkgs { inherit system; config.permittedInsecurePackages = ["dotnet-sdk-6.0.428"]; }; @@ -16,7 +16,7 @@ { devShells = forEachSupportedSystem ({ pkgs }: { default = pkgs.mkShell { - packages = with pkgs; [ + packages = with pkgs; [ # Frontend nodejs sqlite @@ -39,10 +39,10 @@ typescript-language-server ]; shellHook = '' - export PATH=$PATH: + export PATH=$PATH:/home/sikongjueluo/.dotnet/tools export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${pkgs.zlib}/lib + export DOTNET_ROOT=${pkgs.dotnetCorePackages.sdk_9_0}/share/dotnet ''; - }; }); }; diff --git a/package-lock.json b/package-lock.json index 43a4c54..077ddd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "fpga-weblab", "version": "0.1.0", "dependencies": { + "@microsoft/signalr": "^9.0.6", "@svgdotjs/svg.js": "^3.2.4", "@tanstack/vue-table": "^8.21.3", "@types/lodash": "^4.17.16", + "@types/signalr": "^2.4.3", "@vueuse/core": "^13.5.0", "async-mutex": "^0.5.0", "axios": "^1.11.0", @@ -1128,6 +1130,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@microsoft/signalr": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-9.0.6.tgz", + "integrity": "sha512-DrhgzFWI9JE4RPTsHYRxh4yr+OhnwKz8bnJe7eIi7mLLjqhJpEb62CiUy/YbFvLqLzcGzlzz1QWgVAW0zyipMQ==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.5.10" + } + }, + "node_modules/@microsoft/signalr/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -1845,6 +1880,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jquery": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz", + "integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==", + "license": "MIT", + "dependencies": { + "@types/sizzle": "*" + } + }, "node_modules/@types/lodash": { "version": "4.17.16", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", @@ -1861,6 +1905,21 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/signalr": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@types/signalr/-/signalr-2.4.3.tgz", + "integrity": "sha512-W6C1wMRIIhJV9nsw19yhw4h9zlkLnJzsu9dYlH35aHUQblPsDF6UpCcAVu4Ljy4RS3c3uJyV88wf2M2SOWqqZg==", + "license": "MIT", + "dependencies": { + "@types/jquery": "*" + } + }, + "node_modules/@types/sizzle": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", + "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", + "license": "MIT" + }, "node_modules/@types/web-bluetooth": { "version": "0.0.21", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", @@ -2257,6 +2316,18 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2988,6 +3059,24 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/execa": { "version": "9.5.2", "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz", @@ -3061,6 +3150,16 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -4475,6 +4574,27 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/quansync": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", @@ -4492,6 +4612,12 @@ ], "license": "MIT" }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/read-package-json-fast": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", @@ -4577,6 +4703,12 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -4662,6 +4794,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4832,6 +4970,36 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/ts-log": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.7.tgz", @@ -5073,6 +5241,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -5392,6 +5570,12 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -5399,6 +5583,16 @@ "dev": true, "license": "MIT" }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -5415,6 +5609,27 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index cc6a14a..1c6a314 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ "gen-api": "npx tsx scripts/GenerateWebAPI.ts" }, "dependencies": { + "@microsoft/signalr": "^9.0.6", "@svgdotjs/svg.js": "^3.2.4", "@tanstack/vue-table": "^8.21.3", "@types/lodash": "^4.17.16", + "@types/signalr": "^2.4.3", "@vueuse/core": "^13.5.0", "async-mutex": "^0.5.0", "axios": "^1.11.0", diff --git a/scripts/GenerateWebAPI.ts b/scripts/GenerateWebAPI.ts index 01a2ab7..1b84950 100644 --- a/scripts/GenerateWebAPI.ts +++ b/scripts/GenerateWebAPI.ts @@ -1,36 +1,42 @@ -import { spawn, exec, ChildProcess } from 'child_process'; -import { promisify } from 'util'; -import fetch from 'node-fetch'; -import * as fs from 'fs'; +import { spawn, exec, ChildProcess } from "child_process"; +import { promisify } from "util"; +import fetch from "node-fetch"; +import * as fs from "fs"; const execAsync = promisify(exec); // Windows 支持函数 function getCommand(command: string): string { // dotnet 在 Windows 上不需要 .cmd 后缀 - if (command === 'dotnet') { - return 'dotnet'; + if (command === "dotnet") { + return "dotnet"; } - return process.platform === 'win32' ? `${command}.cmd` : command; + return process.platform === "win32" ? `${command}.cmd` : command; } function getSpawnOptions() { - return process.platform === 'win32' ? { stdio: 'pipe', shell: true } : { stdio: 'pipe' }; + return process.platform === "win32" + ? { stdio: "pipe", shell: true } + : { stdio: "pipe" }; } -async function waitForServer(url: string, maxRetries: number = 30, interval: number = 1000): Promise { +async function waitForServer( + url: string, + maxRetries: number = 30, + interval: number = 1000, +): Promise { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url); if (response.ok) { - console.log('✓ Server is ready'); + console.log("✓ Server is ready"); return true; } } catch (error) { // Server not ready yet } console.log(`Waiting for server... (${i + 1}/${maxRetries})`); - await new Promise(resolve => setTimeout(resolve, interval)); + await new Promise((resolve) => setTimeout(resolve, interval)); } return false; } @@ -40,32 +46,39 @@ let serverProcess: ChildProcess | null = null; let webProcess: ChildProcess | null = null; async function startWeb(): Promise { - console.log('Starting Vite frontend...'); + console.log("Starting Vite frontend..."); return new Promise((resolve, reject) => { - const process = spawn(getCommand('npm'), ['run', 'dev'], getSpawnOptions() as any); + const process = spawn( + getCommand("npm"), + ["run", "dev"], + getSpawnOptions() as any, + ); let webStarted = false; - process.stdout?.on('data', (data) => { + process.stdout?.on("data", (data) => { const output = data.toString(); console.log(`Web: ${output}`); - + // 检查 Vite 是否已启动 - if ((output.includes('Local:') || output.includes('ready in')) && !webStarted) { + if ( + (output.includes("Local:") || output.includes("ready in")) && + !webStarted + ) { webStarted = true; resolve(process); } }); - process.stderr?.on('data', (data) => { + process.stderr?.on("data", (data) => { console.error(`Web Error: ${data}`); }); - process.on('error', (error) => { + process.on("error", (error) => { reject(error); }); - process.on('exit', (code, signal) => { + process.on("exit", (code, signal) => { console.log(`Web process exited with code ${code} and signal ${signal}`); if (!webStarted) { reject(new Error(`Web process exited unexpectedly with code ${code}`)); @@ -74,222 +87,211 @@ async function startWeb(): Promise { // 存储进程引用 webProcess = process; - + // 超时处理 setTimeout(() => { if (!webStarted) { - reject(new Error('Web server failed to start within timeout')); + reject(new Error("Web server failed to start within timeout")); } - }, 30000); // 30秒超时 + }, 10000); // 10秒超时 }); } async function startServer(): Promise { - console.log('Starting .NET server...'); + console.log("Starting .NET server..."); return new Promise((resolve, reject) => { - const process = spawn(getCommand('dotnet'), ['run', '--property:Configuration=Release'], { - cwd: 'server', - ...getSpawnOptions() - } as any); + const process = spawn( + getCommand("dotnet"), + ["run", "--property:Configuration=Release"], + { + cwd: "server", + ...getSpawnOptions(), + } as any, + ); let serverStarted = false; - process.stdout?.on('data', (data) => { + process.stdout?.on("data", (data) => { const output = data.toString(); console.log(`Server: ${output}`); - + // 检查服务器是否已启动 - if (output.includes('Now listening on:') && !serverStarted) { + if (output.includes("Now listening on:") && !serverStarted) { serverStarted = true; resolve(process); } }); - process.stderr?.on('data', (data) => { + process.stderr?.on("data", (data) => { console.error(`Server Error: ${data}`); }); - process.on('error', (error) => { + process.on("error", (error) => { reject(error); }); - process.on('exit', (code, signal) => { - console.log(`Server process exited with code ${code} and signal ${signal}`); + process.on("exit", (code, signal) => { + console.log( + `Server process exited with code ${code} and signal ${signal}`, + ); if (!serverStarted) { - reject(new Error(`Server process exited unexpectedly with code ${code}`)); + reject( + new Error(`Server process exited unexpectedly with code ${code}`), + ); } }); // 存储进程引用 serverProcess = process; - + // 超时处理 setTimeout(() => { if (!serverStarted) { - reject(new Error('Server failed to start within timeout')); + reject(new Error("Server failed to start within timeout")); } - }, 30000); // 30秒超时 + }, 10000); // 10秒超时 }); } async function stopServer(): Promise { - console.log('Stopping server...'); - + console.log("Stopping server..."); + if (!serverProcess) { - console.log('No server process to stop'); + console.log("No server process to stop"); return; } try { // 检查进程是否还存在 if (serverProcess.killed || serverProcess.exitCode !== null) { - console.log('✓ Server process already terminated'); + console.log("✓ Server process already terminated"); serverProcess = null; return; } // 发送 SIGTERM 信号 - const killed = serverProcess.kill('SIGTERM'); + const killed = serverProcess.kill("SIGTERM"); if (!killed) { - console.warn('Failed to send SIGTERM to server process'); + console.warn("Failed to send SIGTERM to server process"); return; } - // 等待进程优雅退出 - const exitPromise = new Promise((resolve) => { - if (serverProcess) { - serverProcess.on('exit', () => { - console.log('✓ Server stopped gracefully'); - resolve(); - }); - } else { - resolve(); - } - }); - // 设置超时,如果 3 秒内没有退出则强制终止 const timeoutPromise = new Promise((resolve) => { setTimeout(() => { - if (serverProcess && !serverProcess.killed && serverProcess.exitCode === null) { - console.log('Force killing server process...'); - serverProcess.kill('SIGKILL'); + if ( + serverProcess && + !serverProcess.killed && + serverProcess.exitCode === null + ) { + console.log("Force killing server process..."); + serverProcess.kill("SIGKILL"); } resolve(); }, 3000); // 减少超时时间到3秒 }); - await Promise.race([exitPromise, timeoutPromise]); - + await Promise.race([timeoutPromise]); } catch (error) { - console.warn('Warning: Could not stop server process:', error); + console.warn("Warning: Could not stop server process:", error); } finally { serverProcess = null; - + // 只有在进程可能没有正常退出时才执行清理 // 移除自动清理逻辑,因为正常退出时不需要 } } async function stopWeb(): Promise { - console.log('Stopping web server...'); - + console.log("Stopping web server..."); + if (!webProcess) { - console.log('No web process to stop'); + console.log("No web process to stop"); return; } try { // 检查进程是否还存在 if (webProcess.killed || webProcess.exitCode !== null) { - console.log('✓ Web process already terminated'); + console.log("✓ Web process already terminated"); webProcess = null; return; } // 发送 SIGTERM 信号 - const killed = webProcess.kill('SIGTERM'); + const killed = webProcess.kill("SIGTERM"); if (!killed) { - console.warn('Failed to send SIGTERM to web process'); + console.warn("Failed to send SIGTERM to web process"); return; } - // 等待进程优雅退出 - const exitPromise = new Promise((resolve) => { - if (webProcess) { - webProcess.on('exit', () => { - console.log('✓ Web server stopped gracefully'); - resolve(); - }); - } else { - resolve(); - } - }); - // 设置超时,如果 3 秒内没有退出则强制终止 const timeoutPromise = new Promise((resolve) => { setTimeout(() => { if (webProcess && !webProcess.killed && webProcess.exitCode === null) { - console.log('Force killing web process...'); - webProcess.kill('SIGKILL'); + console.log("Force killing web process..."); + webProcess.kill("SIGKILL"); } resolve(); - }, 3000); // 减少超时时间到3秒 + }, 3000); }); - await Promise.race([exitPromise, timeoutPromise]); - + await Promise.race([timeoutPromise]); } catch (error) { - console.warn('Warning: Could not stop web process:', error); + console.warn("Warning: Could not stop web process:", error); } finally { webProcess = null; - - // 只有在进程可能没有正常退出时才执行清理 - // 移除自动清理逻辑,因为正常退出时不需要 } } async function postProcessApiClient(): Promise { - console.log('Post-processing API client...'); + console.log("Post-processing API client..."); try { - const filePath = 'src/APIClient.ts'; - + const filePath = "src/APIClient.ts"; + // 检查文件是否存在 if (!fs.existsSync(filePath)) { throw new Error(`API client file not found: ${filePath}`); } - + // 读取文件内容 - let content = fs.readFileSync(filePath, 'utf8'); - + let content = fs.readFileSync(filePath, "utf8"); + // 替换 ArgumentException 中的 message 属性声明 content = content.replace( /(\s+)message!:\s*string;/g, - '$1declare message: string;' + "$1declare message: string;", ); - + content = content.replace( + "{ AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelToken }", + "{ AxiosError, type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type CancelToken }", + ); + // 写回文件 - fs.writeFileSync(filePath, content, 'utf8'); - - console.log('✓ API client post-processing completed'); + fs.writeFileSync(filePath, content, "utf8"); + + console.log("✓ API client post-processing completed"); } catch (error) { throw new Error(`Failed to post-process API client: ${error}`); } } async function generateApiClient(): Promise { - console.log('Generating API client...'); + console.log("Generating API client..."); try { - const url = 'http://127.0.0.1:5000/GetAPIClientCode'; + const url = "http://127.0.0.1:5000/GetAPIClientCode"; const response = await fetch(url); if (!response.ok) { - throw new Error(`Failed to fetch API client code: ${response.status} ${response.statusText}`); + throw new Error( + `Failed to fetch API client code: ${response.status} ${response.statusText}`, + ); } const code = await response.text(); // 写入 APIClient.ts - const filePath = 'src/APIClient.ts'; - fs.writeFileSync(filePath, code, 'utf8'); - console.log('✓ API client code fetched and written successfully'); + const filePath = "src/APIClient.ts"; + fs.writeFileSync(filePath, code, "utf8"); + console.log("✓ API client code fetched and written successfully"); // 添加后处理步骤 await postProcessApiClient(); @@ -298,35 +300,55 @@ async function generateApiClient(): Promise { } } +async function generateSignalRClient(): Promise { + console.log("Generating SignalR TypeScript client..."); + try { + const { stdout, stderr } = await execAsync( + "dotnet tsrts --project ./server/server.csproj --output ./src", + ); + if (stdout) console.log(stdout); + if (stderr) console.error(stderr); + console.log("✓ SignalR TypeScript client generated successfully"); + } catch (error) { + throw new Error(`Failed to generate SignalR client: ${error}`); + } +} + async function main(): Promise { try { + // Generate SignalR client + await generateSignalRClient(); + console.log("✓ SignalR TypeScript client generated successfully"); + // Start web frontend first await startWeb(); - console.log('✓ Frontend started'); - + console.log("✓ Frontend started"); + // Wait a bit for frontend to fully initialize - await new Promise(resolve => setTimeout(resolve, 3000)); - + await new Promise((resolve) => setTimeout(resolve, 3000)); + // Start server await startServer(); - console.log('✓ Backend started'); - + console.log("✓ Backend started"); + // Wait for server to be ready (给服务器额外时间完全启动) - await new Promise(resolve => setTimeout(resolve, 2000)); - + await new Promise((resolve) => setTimeout(resolve, 2000)); + // Check if swagger endpoint is available - const serverReady = await waitForServer('http://localhost:5000/swagger/v1/swagger.json'); - + const serverReady = await waitForServer( + "http://localhost:5000/swagger/v1/swagger.json", + ); + if (!serverReady) { - throw new Error('Server failed to start within the expected time'); + throw new Error("Server failed to start within the expected time"); } - + // Generate API client await generateApiClient(); - - console.log('✓ API generation completed successfully'); + + console.log("✓ API generation completed successfully"); } catch (error) { - console.error('❌ Error:', error); + console.error("❌ Error:", error); process.exit(1); } finally { // Always try to stop processes in order: server first, then web @@ -340,80 +362,68 @@ let isCleaningUp = false; const cleanup = async (signal: string) => { if (isCleaningUp) { - console.log('Cleanup already in progress, ignoring signal'); + console.log("Cleanup already in progress, ignoring signal"); return; } - + isCleaningUp = true; console.log(`\nReceived ${signal}, cleaning up...`); - + try { - await Promise.all([ - stopServer(), - stopWeb() - ]); + await Promise.all([stopServer(), stopWeb()]); } catch (error) { - console.error('Error during cleanup:', error); + console.error("Error during cleanup:", error); } - + // 立即退出,不等待 process.exit(0); }; -process.on('SIGINT', () => cleanup('SIGINT')); -process.on('SIGTERM', () => cleanup('SIGTERM')); +process.on("SIGINT", () => cleanup("SIGINT")); +process.on("SIGTERM", () => cleanup("SIGTERM")); // 处理未捕获的异常 -process.on('uncaughtException', async (error) => { +process.on("uncaughtException", async (error) => { if (isCleaningUp) return; - - console.error('❌ Uncaught exception:', error); + + console.error("❌ Uncaught exception:", error); isCleaningUp = true; - + try { - await Promise.all([ - stopServer(), - stopWeb() - ]); + await Promise.all([stopServer(), stopWeb()]); } catch (cleanupError) { - console.error('Error during cleanup:', cleanupError); + console.error("Error during cleanup:", cleanupError); } - + process.exit(1); }); -process.on('unhandledRejection', async (reason, promise) => { +process.on("unhandledRejection", async (reason, promise) => { if (isCleaningUp) return; - - console.error('❌ Unhandled rejection at:', promise, 'reason:', reason); + + console.error("❌ Unhandled rejection at:", promise, "reason:", reason); isCleaningUp = true; - + try { - await Promise.all([ - stopServer(), - stopWeb() - ]); + await Promise.all([stopServer(), stopWeb()]); } catch (cleanupError) { - console.error('Error during cleanup:', cleanupError); + console.error("Error during cleanup:", cleanupError); } - + process.exit(1); }); main().catch(async (error) => { if (isCleaningUp) return; - - console.error('❌ Unhandled error:', error); + + console.error("❌ Unhandled error:", error); isCleaningUp = true; - + try { - await Promise.all([ - stopServer(), - stopWeb() - ]); + await Promise.all([stopServer(), stopWeb()]); } catch (cleanupError) { - console.error('Error during cleanup:', cleanupError); + console.error("Error during cleanup:", cleanupError); } - + process.exit(1); -}); \ No newline at end of file +}); diff --git a/server/Program.cs b/server/Program.cs index ef87db2..088d5b4 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -5,13 +5,13 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.FileProviders; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; -using NJsonSchema.CodeGeneration.TypeScript; using NLog; using NLog.Web; using NSwag; using NSwag.CodeGeneration.TypeScript; using NSwag.Generation.Processors.Security; using server.Services; +using TypedSignalR.Client.DevTools; // Early init of NLog to allow startup and exception logging, before host is built var logger = NLog.LogManager.Setup() @@ -97,6 +97,9 @@ try ); }); + // Use SignalR + builder.Services.AddSignalR(); + // Add Swagger builder.Services.AddSwaggerDocument(options => { @@ -187,9 +190,12 @@ try }; }); app.UseSwaggerUi(); + app.UseSignalRHubSpecification(); + app.UseSignalRHubDevelopmentUI(); // Router app.MapControllers(); + app.MapHub("hubs/JtagHub").RequireCors("Users"); // Setup Program MsgBus.Init(); @@ -233,7 +239,7 @@ try logger.Error(err); return Results.Problem(err.ToString()); } - }); + }).RequireCors("Development"); app.Run(); } @@ -254,4 +260,3 @@ finally // Close Program MsgBus.Exit(); } - diff --git a/server/server.csproj b/server/server.csproj index 1341654..b4bbe9a 100644 --- a/server/server.csproj +++ b/server/server.csproj @@ -32,6 +32,16 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + diff --git a/server/src/Database.cs b/server/src/Database.cs index da31649..be71e6a 100644 --- a/server/src/Database.cs +++ b/server/src/Database.cs @@ -133,7 +133,7 @@ public class Board /// public enum BoardStatus { - /// + /// /// 未启用状态,无法被使用 /// Disabled, @@ -558,6 +558,31 @@ public class AppDataConnection : DataConnection return new(boards[0]); } + /// + /// 根据用户名获取实验板信息 + /// + /// 用户名 + /// 包含实验板信息的结果,如果未找到则返回空 + public Result> GetBoardByUserName(string userName) + { + var boards = this.BoardTable.Where(board => board.OccupiedUserName == userName).ToArray(); + + if (boards.Length > 1) + { + logger.Error($"数据库中存在多个相同用户名的实验板: {userName}"); + return new(new Exception($"数据库中存在多个相同用户名的实验板: {userName}")); + } + + if (boards.Length == 0) + { + logger.Info($"未找到用户名对应的实验板: {userName}"); + return new(Optional.None); + } + + logger.Debug($"成功获取实验板信息: {userName}"); + return new(boards[0]); + } + /// /// 获取所有实验板信息 /// diff --git a/server/src/Hubs/JtagHub.cs b/server/src/Hubs/JtagHub.cs new file mode 100644 index 0000000..0568585 --- /dev/null +++ b/server/src/Hubs/JtagHub.cs @@ -0,0 +1,176 @@ +using Microsoft.AspNetCore.Authorization; +using System.Security.Claims; +using Microsoft.AspNetCore.SignalR; +using DotNext; +using System.Collections.Concurrent; +using TypedSignalR.Client; +using Tapper; + +namespace server.Hubs.JtagHub; + +[Hub] +public interface IJtagHub +{ + Task SetBoundaryScanFreq(int freq); + Task StartBoundaryScan(int freq = 100); + Task StopBoundaryScan(); +} + +[Receiver] +public interface IJtagReceiver +{ + Task OnReceiveBoundaryScanData(Dictionary msg); +} + +[Authorize] +public class JtagHub : Hub, IJtagHub +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private ConcurrentDictionary FreqTable = new(); + private ConcurrentDictionary CancellationTokenSourceTable = new(); + + private Optional GetJtagClient(string userName) + { + try + { + using var db = new Database.AppDataConnection(); + var board = db.GetBoardByUserName(userName); + if (!board.IsSuccessful) + { + logger.Error($"Find Board {board.Value.Value.ID} failed because {board.Error}"); + return new(null); + } + if (!board.Value.HasValue) + { + logger.Error($"Board {board.Value.Value.ID} not found"); + return new(null); + } + + var jtag = new Peripherals.JtagClient.Jtag(board.Value.Value.IpAddr, board.Value.Value.Port); + return new(jtag); + } + catch (Exception error) + { + logger.Error(error); + return new(null); + } + } + + public async Task SetBoundaryScanFreq(int freq) + { + try + { + var userName = Context.User?.FindFirstValue(ClaimTypes.Name); + if (userName is null) + { + logger.Error("Can't get user info"); + return false; + } + + FreqTable.AddOrUpdate(userName, freq, (key, value) => freq); + return true; + } + catch (Exception error) + { + logger.Error(error); + return false; + } + } + + public async Task StartBoundaryScan(int freq = 100) + { + try + { + var userName = Context.User?.FindFirstValue(ClaimTypes.Name); + if (userName is null) + { + logger.Error("No Such User"); + return false; + } + + await SetBoundaryScanFreq(freq); + var cts = CancellationTokenSource.CreateLinkedTokenSource(); + CancellationTokenSourceTable.AddOrUpdate(userName, cts, (key, value) => cts); + + _ = Task + .Run( + () => BoundaryScanLogicPorts( + Context.ConnectionId, + userName, + cts.Token), + cts.Token) + .ContinueWith((task) => + { + if (!task.IsFaulted) + { + return; + } + + if (task.Exception.InnerException is OperationCanceledException) + { + logger.Info($"Boundary scan operation cancelled for user {userName}"); + } + else + { + logger.Error(task.Exception); + } + }); + + return true; + } + catch (Exception error) + { + logger.Error(error); + return false; + } + } + + public async Task StopBoundaryScan() + { + var userName = Context.User?.FindFirstValue(ClaimTypes.Name); + if (userName is null) + { + logger.Error("No Such User"); + return false; + } + + if (!CancellationTokenSourceTable.TryGetValue(userName, out var cts)) + { + return false; + } + + cts.Cancel(); + cts.Token.WaitHandle.WaitOne(); + return true; + } + + private async void BoundaryScanLogicPorts(string connectionID, string userName, CancellationToken cancellationToken) + { + var jtagCtrl = GetJtagClient(userName).OrThrow(() => new InvalidOperationException("JTAG client not found")); + var cntFail = 0; + + while (true && cntFail < 5) + { + cancellationToken.ThrowIfCancellationRequested(); + + var ret = await jtagCtrl.BoundaryScanLogicalPorts(); + if (!ret.IsSuccessful) + { + logger.Error($"User {userName} boundary scan failed for device {jtagCtrl.address}: {ret.Error}"); + cntFail++; + } + + await this.Clients.Client(connectionID).OnReceiveBoundaryScanData(ret.Value); + // logger.Info($"User {userName} successfully completed boundary scan for device {jtagCtrl.address}"); + + await Task.Delay(FreqTable.TryGetValue(userName, out var freq) ? 1000 / freq : 1000 / 100, cancellationToken); + } + + if (cntFail >= 5) + { + logger.Error($"User {userName} boundary scan failed for device {jtagCtrl.address} after 5 attempts"); + throw new InvalidOperationException("Boundary scan failed"); + } + } + +} diff --git a/server/src/Peripherals/JtagClient.cs b/server/src/Peripherals/JtagClient.cs index 6a6f581..d2a7f8e 100644 --- a/server/src/Peripherals/JtagClient.cs +++ b/server/src/Peripherals/JtagClient.cs @@ -386,7 +386,10 @@ public class Jtag readonly int timeout; readonly int port; - readonly string address; + /// + /// Jtag控制器IP地址 + /// + public readonly string address; private IPEndPoint ep; /// diff --git a/server/src/Services/ProgressService.cs b/server/src/Services/ProgressService.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/APIClient.ts b/src/APIClient.ts index 324260b..85fc595 100644 --- a/src/APIClient.ts +++ b/src/APIClient.ts @@ -23,6 +23,50 @@ export class Client { } + getSignalrDevSpec_json( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/signalr-dev/spec.json"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + method: "GET", + url: url_, + headers: { + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetSignalrDevSpec_json(_response); + }); + } + + protected processGetSignalrDevSpec_json(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200) { + const _responseText = response.data; + return Promise.resolve(null as any); + + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + getGetAPIClientCode( cancelToken?: CancelToken): Promise { let url_ = this.baseUrl + "/GetAPIClientCode"; url_ = url_.replace(/[?&]$/, ""); @@ -7239,20 +7283,15 @@ export enum CaptureMode { /** 调试器整体配置信息 */ export class DebuggerConfig implements IDebuggerConfig { - /** 时钟频率 - */ + /** 时钟频率 */ clkFreq!: number; - /** 总端口数量 - */ + /** 总端口数量 */ totalPortNum!: number; - /** 捕获深度(采样点数) - */ + /** 捕获深度(采样点数) */ captureDepth!: number; - /** 触发器数量 - */ + /** 触发器数量 */ triggerNum!: number; - /** 所有信号通道的配置信息 - */ + /** 所有信号通道的配置信息 */ channelConfigs!: ChannelConfig[]; constructor(data?: IDebuggerConfig) { @@ -7305,42 +7344,31 @@ export class DebuggerConfig implements IDebuggerConfig { /** 调试器整体配置信息 */ export interface IDebuggerConfig { - /** 时钟频率 - */ + /** 时钟频率 */ clkFreq: number; - /** 总端口数量 - */ + /** 总端口数量 */ totalPortNum: number; - /** 捕获深度(采样点数) - */ + /** 捕获深度(采样点数) */ captureDepth: number; - /** 触发器数量 - */ + /** 触发器数量 */ triggerNum: number; - /** 所有信号通道的配置信息 - */ + /** 所有信号通道的配置信息 */ channelConfigs: ChannelConfig[]; } /** 表示单个信号通道的配置信息 */ export class ChannelConfig implements IChannelConfig { - /** 通道名称 - */ + /** 通道名称 */ name!: string; - /** 通道显示颜色(如前端波形显示用) - */ + /** 通道显示颜色(如前端波形显示用) */ color!: string; - /** 通道信号线宽度(位数) - */ + /** 通道信号线宽度(位数) */ wireWidth!: number; - /** 信号线在父端口中的起始索引(bit) - */ + /** 信号线在父端口中的起始索引(bit) */ wireStartIndex!: number; - /** 父端口编号 - */ + /** 父端口编号 */ parentPort!: number; - /** 捕获模式(如上升沿、下降沿等) - */ + /** 捕获模式(如上升沿、下降沿等) */ mode!: CaptureMode; constructor(data?: IChannelConfig) { @@ -7384,33 +7412,25 @@ export class ChannelConfig implements IChannelConfig { /** 表示单个信号通道的配置信息 */ export interface IChannelConfig { - /** 通道名称 - */ + /** 通道名称 */ name: string; - /** 通道显示颜色(如前端波形显示用) - */ + /** 通道显示颜色(如前端波形显示用) */ color: string; - /** 通道信号线宽度(位数) - */ + /** 通道信号线宽度(位数) */ wireWidth: number; - /** 信号线在父端口中的起始索引(bit) - */ + /** 信号线在父端口中的起始索引(bit) */ wireStartIndex: number; - /** 父端口编号 - */ + /** 父端口编号 */ parentPort: number; - /** 捕获模式(如上升沿、下降沿等) - */ + /** 捕获模式(如上升沿、下降沿等) */ mode: CaptureMode; } /** 单个通道的捕获数据 */ export class ChannelCaptureData implements IChannelCaptureData { - /** 通道名称 - */ + /** 通道名称 */ name!: string; - /** 通道捕获到的数据(Base64编码的UInt32数组) - */ + /** 通道捕获到的数据(Base64编码的UInt32数组) */ data!: string; constructor(data?: IChannelCaptureData) { @@ -7446,11 +7466,9 @@ export class ChannelCaptureData implements IChannelCaptureData { /** 单个通道的捕获数据 */ export interface IChannelCaptureData { - /** 通道名称 - */ + /** 通道名称 */ name: string; - /** 通道捕获到的数据(Base64编码的UInt32数组) - */ + /** 通道捕获到的数据(Base64编码的UInt32数组) */ data: string; } diff --git a/src/TypedSignalR.Client/index.ts b/src/TypedSignalR.Client/index.ts new file mode 100644 index 0000000..393fcab --- /dev/null +++ b/src/TypedSignalR.Client/index.ts @@ -0,0 +1,118 @@ +/* THIS (.ts) FILE IS GENERATED BY TypedSignalR.Client.TypeScript */ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr'; +import type { IJtagHub, IJtagReceiver } from './server.Hubs.JtagHub'; + + +// components + +export type Disposable = { + dispose(): void; +} + +export type HubProxyFactory = { + createHubProxy(connection: HubConnection): T; +} + +export type ReceiverRegister = { + register(connection: HubConnection, receiver: T): Disposable; +} + +type ReceiverMethod = { + methodName: string, + method: (...args: any[]) => void +} + +class ReceiverMethodSubscription implements Disposable { + + public constructor( + private connection: HubConnection, + private receiverMethod: ReceiverMethod[]) { + } + + public readonly dispose = () => { + for (const it of this.receiverMethod) { + this.connection.off(it.methodName, it.method); + } + } +} + +// API + +export type HubProxyFactoryProvider = { + (hubType: "IJtagHub"): HubProxyFactory; +} + +export const getHubProxyFactory = ((hubType: string) => { + if(hubType === "IJtagHub") { + return IJtagHub_HubProxyFactory.Instance; + } +}) as HubProxyFactoryProvider; + +export type ReceiverRegisterProvider = { + (receiverType: "IJtagReceiver"): ReceiverRegister; +} + +export const getReceiverRegister = ((receiverType: string) => { + if(receiverType === "IJtagReceiver") { + return IJtagReceiver_Binder.Instance; + } +}) as ReceiverRegisterProvider; + +// HubProxy + +class IJtagHub_HubProxyFactory implements HubProxyFactory { + public static Instance = new IJtagHub_HubProxyFactory(); + + private constructor() { + } + + public readonly createHubProxy = (connection: HubConnection): IJtagHub => { + return new IJtagHub_HubProxy(connection); + } +} + +class IJtagHub_HubProxy implements IJtagHub { + + public constructor(private connection: HubConnection) { + } + + public readonly setBoundaryScanFreq = async (freq: number): Promise => { + return await this.connection.invoke("SetBoundaryScanFreq", freq); + } + + public readonly startBoundaryScan = async (freq: number): Promise => { + return await this.connection.invoke("StartBoundaryScan", freq); + } + + public readonly stopBoundaryScan = async (): Promise => { + return await this.connection.invoke("StopBoundaryScan"); + } +} + + +// Receiver + +class IJtagReceiver_Binder implements ReceiverRegister { + + public static Instance = new IJtagReceiver_Binder(); + + private constructor() { + } + + public readonly register = (connection: HubConnection, receiver: IJtagReceiver): Disposable => { + + const __onReceiveBoundaryScanData = (...args: [Partial>]) => receiver.onReceiveBoundaryScanData(...args); + + connection.on("OnReceiveBoundaryScanData", __onReceiveBoundaryScanData); + + const methodList: ReceiverMethod[] = [ + { methodName: "OnReceiveBoundaryScanData", method: __onReceiveBoundaryScanData } + ] + + return new ReceiverMethodSubscription(connection, methodList); + } +} + diff --git a/src/TypedSignalR.Client/server.Hubs.JtagHub.ts b/src/TypedSignalR.Client/server.Hubs.JtagHub.ts new file mode 100644 index 0000000..bee5c63 --- /dev/null +++ b/src/TypedSignalR.Client/server.Hubs.JtagHub.ts @@ -0,0 +1,31 @@ +/* THIS (.ts) FILE IS GENERATED BY TypedSignalR.Client.TypeScript */ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +import type { IStreamResult, Subject } from '@microsoft/signalr'; + +export type IJtagHub = { + /** + * @param freq Transpiled from int + * @returns Transpiled from System.Threading.Tasks.Task + */ + setBoundaryScanFreq(freq: number): Promise; + /** + * @param freq Transpiled from int + * @returns Transpiled from System.Threading.Tasks.Task + */ + startBoundaryScan(freq: number): Promise; + /** + * @returns Transpiled from System.Threading.Tasks.Task + */ + stopBoundaryScan(): Promise; +} + +export type IJtagReceiver = { + /** + * @param msg Transpiled from System.Collections.Generic.Dictionary + * @returns Transpiled from System.Threading.Tasks.Task + */ + onReceiveBoundaryScanData(msg: Partial>): Promise; +} + diff --git a/src/components/PopButton.vue b/src/components/PopButton.vue deleted file mode 100644 index 183c7fa..0000000 --- a/src/components/PopButton.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue deleted file mode 100644 index 1de2ee0..0000000 --- a/src/components/Sidebar.vue +++ /dev/null @@ -1,162 +0,0 @@ - - - - - diff --git a/src/components/equipments/MotherBoardCaps.vue b/src/components/equipments/MotherBoardCaps.vue index ed1cf34..c664d8d 100644 --- a/src/components/equipments/MotherBoardCaps.vue +++ b/src/components/equipments/MotherBoardCaps.vue @@ -8,20 +8,35 @@

IDCode: 0x{{ jtagIDCode.toString(16).padStart(8, "0").toUpperCase() }}

-
- +
Jtag运行频率 - @@ -30,12 +45,23 @@
边界扫描刷新率 / Hz - +

输入一个1 ~ 1000的数

-
@@ -43,8 +69,12 @@

外设

- +

启用矩阵键盘

@@ -117,7 +147,7 @@ async function handleMatrixkeyCheckboxChange(event: Event) { } async function toggleJtagBoundaryScan() { - eqps.enableJtagBoundaryScan = !eqps.enableJtagBoundaryScan; + eqps.jtagBoundaryScanSetOnOff(!eqps.enableJtagBoundaryScan); } const isGettingIDCode = ref(false); diff --git a/src/stores/constraints.ts b/src/stores/constraints.ts index ddaa4d8..b6b6d63 100644 --- a/src/stores/constraints.ts +++ b/src/stores/constraints.ts @@ -1,26 +1,25 @@ -import { ref, computed, reactive } from 'vue' -import { defineStore } from 'pinia' -import { isBoolean } from 'lodash'; +import { ref, computed, reactive } from "vue"; +import { defineStore } from "pinia"; +import { isBoolean } from "lodash"; // 约束电平状态类型 -export type ConstraintLevel = 'high' | 'low' | 'undefined'; - -export const useConstraintsStore = defineStore('constraints', () => { +export type ConstraintLevel = "high" | "low" | "undefined"; +export const useConstraintsStore = defineStore("constraints", () => { // 约束状态存储 const constraintStates = reactive>({}); // 约束颜色映射 const constraintColors = { - high: '#ff3333', // 高电平为红色 - low: '#3333ff', // 低电平为蓝色 - undefined: '#999999' // 未定义为灰色 + high: "#ff3333", // 高电平为红色 + low: "#3333ff", // 低电平为蓝色 + undefined: "#999999", // 未定义为灰色 }; // 获取约束状态 function getConstraintState(constraint: string): ConstraintLevel { - if (!constraint) return 'undefined'; - return constraintStates[constraint] || 'undefined'; + if (!constraint) return "undefined"; + return constraintStates[constraint] || "undefined"; } // 设置约束状态 @@ -30,7 +29,9 @@ export const useConstraintsStore = defineStore('constraints', () => { } // 批量设置约束状态 - function batchSetConstraintStates(states: Record | Record) { + function batchSetConstraintStates( + states: Record | Partial>, + ) { // 收集发生变化的约束 const changedConstraints: [string, ConstraintLevel][] = []; @@ -38,6 +39,8 @@ export const useConstraintsStore = defineStore('constraints', () => { Object.entries(states).forEach(([constraint, level]) => { if (isBoolean(level)) { level = level ? "high" : "low"; + } else { + level = "low"; } if (constraintStates[constraint] !== level) { @@ -48,7 +51,7 @@ export const useConstraintsStore = defineStore('constraints', () => { // 通知所有变化 changedConstraints.forEach(([constraint, level]) => { - stateChangeCallbacks.forEach(callback => callback(constraint, level)); + stateChangeCallbacks.forEach((callback) => callback(constraint, level)); }); } @@ -60,7 +63,7 @@ export const useConstraintsStore = defineStore('constraints', () => { // 清除所有约束状态 function clearAllConstraintStates() { - Object.keys(constraintStates).forEach(key => { + Object.keys(constraintStates).forEach((key) => { delete constraintStates[key]; }); } @@ -71,9 +74,14 @@ export const useConstraintsStore = defineStore('constraints', () => { } // 注册约束状态变化回调 - const stateChangeCallbacks: ((constraint: string, level: ConstraintLevel) => void)[] = []; + const stateChangeCallbacks: (( + constraint: string, + level: ConstraintLevel, + ) => void)[] = []; - function onConstraintStateChange(callback: (constraint: string, level: ConstraintLevel) => void) { + function onConstraintStateChange( + callback: (constraint: string, level: ConstraintLevel) => void, + ) { stateChangeCallbacks.push(callback); return () => { const index = stateChangeCallbacks.indexOf(callback); @@ -86,7 +94,7 @@ export const useConstraintsStore = defineStore('constraints', () => { // 触发约束变化 function notifyConstraintChange(constraint: string, level: ConstraintLevel) { setConstraintState(constraint, level); - stateChangeCallbacks.forEach(callback => callback(constraint, level)); + stateChangeCallbacks.forEach((callback) => callback(constraint, level)); } return { @@ -98,6 +106,5 @@ export const useConstraintsStore = defineStore('constraints', () => { getAllConstraintStates, onConstraintStateChange, notifyConstraintChange, - } -}) - + }; +}); diff --git a/src/stores/equipments.ts b/src/stores/equipments.ts index 6bc7652..f98a302 100644 --- a/src/stores/equipments.ts +++ b/src/stores/equipments.ts @@ -1,15 +1,16 @@ -import { ref, reactive, watchPostEffect } from "vue"; +import { ref, reactive, watchPostEffect, onMounted, onUnmounted } from "vue"; import { defineStore } from "pinia"; import { useLocalStorage } from "@vueuse/core"; -import { isString, toNumber } from "lodash"; +import { isString, toNumber, type Dictionary } from "lodash"; import z from "zod"; import { isNumber } from "mathjs"; -import { JtagClient, MatrixKeyClient, PowerClient } from "@/APIClient"; import { Mutex, withTimeout } from "async-mutex"; import { useConstraintsStore } from "@/stores/constraints"; import { useDialogStore } from "./dialog"; import { toFileParameterOrUndefined } from "@/utils/Common"; import { AuthManager } from "@/utils/AuthManager"; +import { HubConnectionBuilder } from "@microsoft/signalr"; +import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client"; export const useEquipments = defineStore("equipments", () => { // Global Stores @@ -22,13 +23,28 @@ export const useEquipments = defineStore("equipments", () => { // Jtag const jtagBitstream = ref(); const jtagBoundaryScanFreq = ref(100); - const jtagBoundaryScanErrorCount = ref(0); // 边界扫描连续错误计数 - const maxJtagBoundaryScanErrors = 5; // 最大允许连续错误次数 const jtagClientMutex = withTimeout( new Mutex(), 1000, new Error("JtagClient Mutex Timeout!"), ); + const jtagHubConnection = new HubConnectionBuilder() + .withUrl("/hubs/JtagHub") + .withAutomaticReconnect() + .build(); + const jtagHubProxy = + getHubProxyFactory("IJtagHub").createHubProxy(jtagHubConnection); + const jtagHubSubscription = getReceiverRegister("IJtagReceiver").register( + jtagHubConnection, + { + onReceiveBoundaryScanData: async (msg) => { + constrainsts.batchSetConstraintStates(msg); + }, + }, + ); + onMounted(() => { + jtagHubConnection.start(); + }); // Matrix Key const matrixKeyStates = reactive(new Array(16).fill(false)); @@ -50,41 +66,6 @@ export const useEquipments = defineStore("equipments", () => { const enableMatrixKey = ref(false); const enablePower = ref(false); - // Watch - watchPostEffect(async () => { - if (true === enableJtagBoundaryScan.value) { - // 重新启用时重置错误计数器 - jtagBoundaryScanErrorCount.value = 0; - jtagBoundaryScan(); - } - }); - - // Parse and Set - function setAddr(address: string | undefined): boolean { - if (isString(address) && z.string().ip("4").safeParse(address).success) { - boardAddr.value = address; - return true; - } - - return false; - } - - function setPort(port: string | number | undefined): boolean { - if (isString(port) && port.length != 0) { - const portNumber = toNumber(port); - if (z.number().nonnegative().max(65535).safeParse(portNumber).success) { - boardPort.value = portNumber; - return true; - } - } else if (isNumber(port)) { - if (z.number().nonnegative().max(65535).safeParse(port).success) { - boardPort.value = port; - return true; - } - } - return false; - } - function setMatrixKey( keyNum: number | string | undefined, keyValue: boolean, @@ -105,38 +86,12 @@ export const useEquipments = defineStore("equipments", () => { return false; } - async function jtagBoundaryScan() { - const release = await jtagClientMutex.acquire(); - try { - // 自动开启电源 - await powerSetOnOff(true); - - const jtagClient = AuthManager.createAuthenticatedJtagClient(); - const portStates = await jtagClient.boundaryScanLogicalPorts( - boardAddr.value, - boardPort.value, - ); - - constrainsts.batchSetConstraintStates(portStates); - - // 扫描成功,重置错误计数器 - jtagBoundaryScanErrorCount.value = 0; - } catch (error) { - jtagBoundaryScanErrorCount.value++; - - console.error(`边界扫描错误 (${jtagBoundaryScanErrorCount.value}/${maxJtagBoundaryScanErrors}):`, error); - - // 如果错误次数超过最大允许次数,才停止扫描并显示错误 - if (jtagBoundaryScanErrorCount.value >= maxJtagBoundaryScanErrors) { - dialog.error("边界扫描发生连续错误,已自动停止"); - enableJtagBoundaryScan.value = false; - jtagBoundaryScanErrorCount.value = 0; // 重置错误计数器 - } - } finally { - release(); - - if (enableJtagBoundaryScan.value) - setTimeout(jtagBoundaryScan, 1000 / jtagBoundaryScanFreq.value); + async function jtagBoundaryScanSetOnOff(enable: boolean) { + enableJtagBoundaryScan.value = enable; + if (enable) { + jtagHubProxy.startBoundaryScan(jtagBoundaryScanFreq.value); + } else { + jtagHubProxy.stopBoundaryScan(); } } @@ -144,7 +99,7 @@ export const useEquipments = defineStore("equipments", () => { try { // 自动开启电源 await powerSetOnOff(true); - + const jtagClient = AuthManager.createAuthenticatedJtagClient(); const resp = await jtagClient.uploadBitstream( boardAddr.value, @@ -163,7 +118,7 @@ export const useEquipments = defineStore("equipments", () => { try { // 自动开启电源 await powerSetOnOff(true); - + const jtagClient = AuthManager.createAuthenticatedJtagClient(); const resp = await jtagClient.downloadBitstream( boardAddr.value, @@ -184,7 +139,7 @@ export const useEquipments = defineStore("equipments", () => { try { // 自动开启电源 await powerSetOnOff(true); - + const jtagClient = AuthManager.createAuthenticatedJtagClient(); const resp = await jtagClient.getDeviceIDCode( boardAddr.value, @@ -204,7 +159,7 @@ export const useEquipments = defineStore("equipments", () => { try { // 自动开启电源 await powerSetOnOff(true); - + const jtagClient = AuthManager.createAuthenticatedJtagClient(); const resp = await jtagClient.setSpeed( boardAddr.value, @@ -224,7 +179,8 @@ export const useEquipments = defineStore("equipments", () => { const release = await matrixKeypadClientMutex.acquire(); console.log("set Key !!!!!!!!!!!!"); try { - const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient(); + const matrixKeypadClient = + AuthManager.createAuthenticatedMatrixKeyClient(); const resp = await matrixKeypadClient.setMatrixKeyStatus( boardAddr.value, boardPort.value, @@ -243,7 +199,8 @@ export const useEquipments = defineStore("equipments", () => { const release = await matrixKeypadClientMutex.acquire(); try { if (enable) { - const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient(); + const matrixKeypadClient = + AuthManager.createAuthenticatedMatrixKeyClient(); const resp = await matrixKeypadClient.enabelMatrixKey( boardAddr.value, boardPort.value, @@ -251,7 +208,8 @@ export const useEquipments = defineStore("equipments", () => { enableMatrixKey.value = resp; return resp; } else { - const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient(); + const matrixKeypadClient = + AuthManager.createAuthenticatedMatrixKeyClient(); const resp = await matrixKeypadClient.disableMatrixKey( boardAddr.value, boardPort.value, @@ -290,16 +248,13 @@ export const useEquipments = defineStore("equipments", () => { return { boardAddr, boardPort, - setAddr, - setPort, setMatrixKey, // Jtag enableJtagBoundaryScan, + jtagBoundaryScanSetOnOff, jtagBitstream, jtagBoundaryScanFreq, - jtagBoundaryScanErrorCount, - jtagClientMutex, jtagUploadBitstream, jtagDownloadBitstream, jtagGetIDCode, diff --git a/src/stores/sidebar.ts b/src/stores/sidebar.ts deleted file mode 100644 index 88a0f28..0000000 --- a/src/stores/sidebar.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ref, computed } from 'vue' -import { defineStore } from 'pinia' - -export const useSidebarStore = defineStore('sidebar', () => { - const isClose = ref(false); - - function closeSidebar() { - isClose.value = true; - console.info("Close sidebar"); - } - - function openSidebar() { - isClose.value = false; - console.info("Open sidebar"); - } - - function toggleSidebar() { - if (isClose.value) { - openSidebar(); - // themeSidebar.value = "card-dash sidebar-base sidebar-open" - } else { - closeSidebar(); - // themeSidebar.value = "card-dash sidebar-base sidebar-close" - } - } - - return { isClose, closeSidebar, openSidebar, toggleSidebar } -}) - diff --git a/vite.config.ts b/vite.config.ts index 51a0492..c225610 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,13 @@ -import { fileURLToPath, URL } from 'node:url' +import { fileURLToPath, URL } from "node:url"; -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' -import vueJsx from '@vitejs/plugin-vue-jsx' -import vueDevTools from 'vite-plugin-vue-devtools' -import tailwindcss from '@tailwindcss/postcss' -import autoprefixer from 'autoprefixer' -import Components from 'unplugin-vue-components/vite' -import RekaResolver from 'reka-ui/resolver' +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import vueJsx from "@vitejs/plugin-vue-jsx"; +import vueDevTools from "vite-plugin-vue-devtools"; +import tailwindcss from "@tailwindcss/postcss"; +import autoprefixer from "autoprefixer"; +import Components from "unplugin-vue-components/vite"; +import RekaResolver from "reka-ui/resolver"; // https://vite.dev/config/ export default defineConfig({ @@ -16,49 +16,48 @@ export default defineConfig({ template: { compilerOptions: { // 将所有 wokwi- 开头的标签视为自定义元素 - isCustomElement: (tag) => tag.startsWith('wokwi-') - } - } + isCustomElement: (tag) => tag.startsWith("wokwi-"), + }, + }, }), vueJsx(), vueDevTools(), - Components( - { + Components({ dts: true, resolvers: [ - RekaResolver() + RekaResolver(), // RekaResolver({ // prefix: '' // use the prefix option to add Prefix to the imported components // }) ], - } - ) + }), ], resolve: { alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)) + "@": fileURLToPath(new URL("./src", import.meta.url)), }, }, css: { postcss: { - plugins: [ - tailwindcss(), - autoprefixer() - ] - } + plugins: [tailwindcss(), autoprefixer()], + }, }, build: { - outDir: 'wwwroot', + outDir: "wwwroot", emptyOutDir: true, // also necessary }, server: { proxy: { "/swagger": { - target: 'http://localhost:5000', - changeOrigin: true - } + target: "http://localhost:5000", + changeOrigin: true, + }, + "/hubs": { + target: "http://localhost:5000", + changeOrigin: true, + }, }, port: 5173, - } -}) + }, +});