From 0a1e0982c20be5c70ff8171fa960a29ab0f85910 Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sat, 16 Aug 2025 11:56:27 +0800 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=E4=B8=83?= =?UTF-8?q?=E6=AE=B5=E6=95=B0=E7=A0=81=E7=AE=A1=E6=B7=BB=E5=8A=A0=E6=95=B0?= =?UTF-8?q?=E5=AD=97=E5=AD=AA=E7=94=9F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- TODO.md | 13 - .../LabCanvas/composable/diagramManager.ts | 189 ++++---- src/components/LabCanvas/index.ts | 4 +- .../equipments/SevenSegmentDisplay.vue | 154 ++++--- .../SevenSegmentDisplayUltimate.vue | 413 ++++++++++++++++++ src/stores/equipments.ts | 161 +++++-- src/utils/AuthManager.ts | 9 + src/utils/Common.ts | 9 + .../signalR/TypedSignalR.Client/index.ts | 6 +- .../TypedSignalR.Client/server.Hubs.ts | 6 +- src/utils/signalR/server.Hubs.ts | 8 + 12 files changed, 775 insertions(+), 199 deletions(-) delete mode 100644 TODO.md create mode 100644 src/components/equipments/SevenSegmentDisplayUltimate.vue diff --git a/.gitignore b/.gitignore index 6d8d639..58e26c1 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,7 @@ DebuggerCmd.md *.ntvs* *.njsproj *.sw? - +prompt.md *.tsbuildinfo # Generated Files diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 50554da..0000000 --- a/TODO.md +++ /dev/null @@ -1,13 +0,0 @@ -# TODO - -1. 后端HTTP视频流 - -640*480, RGB565 -0x0000_0000 + 25800 - - -2. 信号发生器界面导入.dat文件 -3. 示波器后端交互、前端界面 -4. 逻辑分析仪后端交互、前端界面 -5. 前端重构 -6. 数据库 —— 用户登录、板卡资源分配、板卡IP地址分配 \ No newline at end of file diff --git a/src/components/LabCanvas/composable/diagramManager.ts b/src/components/LabCanvas/composable/diagramManager.ts index a03403b..a1e88d7 100644 --- a/src/components/LabCanvas/composable/diagramManager.ts +++ b/src/components/LabCanvas/composable/diagramManager.ts @@ -1,3 +1,6 @@ +import { ResourcePurpose } from "@/APIClient"; +import { AuthManager } from "@/utils/AuthManager"; + // 定义 diagram.json 的类型结构 export interface DiagramData { version: number; @@ -26,40 +29,43 @@ export interface DiagramPart { // 连接类型定义 - 使用元组类型表示四元素数组 export type ConnectionArray = [string, string, number, string[]]; -import { AuthManager } from '@/utils/AuthManager'; - // 解析连接字符串为组件ID和引脚ID -export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } { - const [componentId, pinId] = connectionPin.split(':'); +export function parseConnectionPin(connectionPin: string): { + componentId: string; + pinId: string; +} { + const [componentId, pinId] = connectionPin.split(":"); return { componentId, pinId }; } // 将连接数组转换为适用于渲染的格式 export function connectionArrayToWireItem( - connection: ConnectionArray, - index: number, - startPos = { x: 0, y: 0 }, - endPos = { x: 0, y: 0 } + connection: ConnectionArray, + index: number, + startPos = { x: 0, y: 0 }, + endPos = { x: 0, y: 0 }, ): WireItem { const [startPinStr, endPinStr, width, path] = connection; - const { componentId: startComponentId, pinId: startPinId } = parseConnectionPin(startPinStr); - const { componentId: endComponentId, pinId: endPinId } = parseConnectionPin(endPinStr); - + const { componentId: startComponentId, pinId: startPinId } = + parseConnectionPin(startPinStr); + const { componentId: endComponentId, pinId: endPinId } = + parseConnectionPin(endPinStr); + return { id: `wire-${index}`, startX: startPos.x, startY: startPos.y, - endX: endPos.x, + endX: endPos.x, endY: endPos.y, startComponentId, startPinId, endComponentId, endPinId, strokeWidth: width, - color: '#4a5568', // 默认颜色 - routingMode: 'path', + color: "#4a5568", // 默认颜色 + routingMode: "path", pathCommands: path, - showLabel: false + showLabel: false, }; } @@ -76,7 +82,7 @@ export interface WireItem { endPinId?: string; strokeWidth: number; color: string; - routingMode: 'orthogonal' | 'path'; + routingMode: "orthogonal" | "path"; constraint?: string; pathCommands?: string[]; showLabel: boolean; @@ -89,57 +95,63 @@ export async function loadDiagramData(examId?: string): Promise { if (examId) { try { const resourceClient = AuthManager.createAuthenticatedResourceClient(); - + // 获取diagram类型的资源列表 - const resources = await resourceClient.getResourceList(examId, 'canvas', 'template'); - + const resources = await resourceClient.getResourceList( + examId, + "canvas", + ResourcePurpose.Template, + ); + if (resources && resources.length > 0) { // 获取第一个diagram资源 const diagramResource = resources[0]; - + // 使用动态API获取资源文件内容 - const response = await resourceClient.getResourceById(diagramResource.id); - + const response = await resourceClient.getResourceById( + diagramResource.id, + ); + if (response && response.data) { const text = await response.data.text(); const data = JSON.parse(text); - + // 验证数据格式 const validation = validateDiagramData(data); if (validation.isValid) { - console.log('成功从API加载实验diagram:', examId); + console.log("成功从API加载实验diagram:", examId); return data; } else { - console.warn('API返回的diagram数据格式无效:', validation.errors); + console.warn("API返回的diagram数据格式无效:", validation.errors); } } } else { - console.log('未找到实验diagram资源,使用默认加载方式'); + console.log("未找到实验diagram资源,使用默认加载方式"); } } catch (error) { - console.warn('从API加载实验diagram失败,使用默认加载方式:', error); + console.warn("从API加载实验diagram失败,使用默认加载方式:", error); } } - + // 如果没有examId或API加载失败,尝试从静态文件加载(不再使用本地存储) - + // 从静态文件加载(作为备选方案) - const response = await fetch('/src/components/diagram.json'); + const response = await fetch("/src/components/diagram.json"); if (!response.ok) { throw new Error(`Failed to load diagram.json: ${response.statusText}`); } const data = await response.json(); - + // 验证静态文件数据 const validation = validateDiagramData(data); if (validation.isValid) { return data; } else { - console.warn('静态diagram文件数据格式无效:', validation.errors); - throw new Error('所有diagram数据源都无效'); + console.warn("静态diagram文件数据格式无效:", validation.errors); + throw new Error("所有diagram数据源都无效"); } } catch (error) { - console.error('Error loading diagram data:', error); + console.error("Error loading diagram data:", error); // 返回空的默认数据结构 return createEmptyDiagram(); } @@ -149,33 +161,31 @@ export async function loadDiagramData(examId?: string): Promise { export function createEmptyDiagram(): DiagramData { return { version: 1, - author: 'user', - editor: 'user', + author: "user", + editor: "user", parts: [], - connections: [] + connections: [], }; } // 保存图表数据(已禁用本地存储) export function saveDiagramData(data: DiagramData): void { // 本地存储功能已禁用 - 不再保存到localStorage - console.debug('saveDiagramData called but localStorage saving is disabled'); + console.debug("saveDiagramData called but localStorage saving is disabled"); } // 更新组件位置 export function updatePartPosition( - data: DiagramData, - partId: string, - x: number, - y: number + data: DiagramData, + partId: string, + x: number, + y: number, ): DiagramData { return { ...data, - parts: data.parts.map(part => - part.id === partId - ? { ...part, x, y } - : part - ) + parts: data.parts.map((part) => + part.id === partId ? { ...part, x, y } : part, + ), }; } @@ -184,21 +194,21 @@ export function updatePartAttribute( data: DiagramData, partId: string, attrName: string, - value: any + value: any, ): DiagramData { return { ...data, - parts: data.parts.map(part => - part.id === partId - ? { - ...part, - attrs: { - ...part.attrs, - [attrName]: value - } - } - : part - ) + parts: data.parts.map((part) => + part.id === partId + ? { + ...part, + attrs: { + ...part.attrs, + [attrName]: value, + }, + } + : part, + ), }; } @@ -210,72 +220,79 @@ export function addConnection( endComponentId: string, endPinId: string, width: number = 2, - path: string[] = [] + path: string[] = [], ): DiagramData { const newConnection: ConnectionArray = [ `${startComponentId}:${startPinId}`, `${endComponentId}:${endPinId}`, width, - path + path, ]; - + return { ...data, - connections: [...data.connections, newConnection] + connections: [...data.connections, newConnection], }; } // 删除连接 export function deleteConnection( data: DiagramData, - connectionIndex: number + connectionIndex: number, ): DiagramData { return { ...data, - connections: data.connections.filter((_, index) => index !== connectionIndex) + connections: data.connections.filter( + (_, index) => index !== connectionIndex, + ), }; } // 查找与组件关联的所有连接 export function findConnectionsByPart( data: DiagramData, - partId: string + partId: string, ): { connection: ConnectionArray; index: number }[] { return data.connections .map((connection, index) => ({ connection, index })) .filter(({ connection }) => { const [startPin, endPin] = connection; - const startCompId = startPin.split(':')[0]; - const endCompId = endPin.split(':')[0]; + const startCompId = startPin.split(":")[0]; + const endCompId = endPin.split(":")[0]; return startCompId === partId || endCompId === partId; }); } // 添加验证diagram.json文件的函数 -export function validateDiagramData(data: any): { isValid: boolean; errors: string[] } { +export function validateDiagramData(data: any): { + isValid: boolean; + errors: string[]; +} { const errors: string[] = []; - + // 检查版本号 if (!data.version) { - errors.push('缺少version字段'); + errors.push("缺少version字段"); } - + // 检查parts数组 if (!Array.isArray(data.parts)) { - errors.push('parts字段不是数组'); + errors.push("parts字段不是数组"); } else { // 验证parts中的每个对象 data.parts.forEach((part: any, index: number) => { if (!part.id) errors.push(`parts[${index}]缺少id`); if (!part.type) errors.push(`parts[${index}]缺少type`); - if (typeof part.x !== 'number') errors.push(`parts[${index}]缺少有效的x坐标`); - if (typeof part.y !== 'number') errors.push(`parts[${index}]缺少有效的y坐标`); + if (typeof part.x !== "number") + errors.push(`parts[${index}]缺少有效的x坐标`); + if (typeof part.y !== "number") + errors.push(`parts[${index}]缺少有效的y坐标`); }); } - + // 检查connections数组 if (!Array.isArray(data.connections)) { - errors.push('connections字段不是数组'); + errors.push("connections字段不是数组"); } else { // 验证connections中的每个数组 data.connections.forEach((conn: any, index: number) => { @@ -283,25 +300,25 @@ export function validateDiagramData(data: any): { isValid: boolean; errors: stri errors.push(`connections[${index}]不是有效的连接数组`); return; } - + const [startPin, endPin, width] = conn; - - if (typeof startPin !== 'string' || !startPin.includes(':')) { + + if (typeof startPin !== "string" || !startPin.includes(":")) { errors.push(`connections[${index}]的起始针脚格式无效`); } - - if (typeof endPin !== 'string' || !endPin.includes(':')) { + + if (typeof endPin !== "string" || !endPin.includes(":")) { errors.push(`connections[${index}]的结束针脚格式无效`); } - - if (typeof width !== 'number') { + + if (typeof width !== "number") { errors.push(`connections[${index}]的宽度不是有效的数字`); } }); } - + return { isValid: errors.length === 0, - errors + errors, }; } diff --git a/src/components/LabCanvas/index.ts b/src/components/LabCanvas/index.ts index 8ccb84f..c5b180e 100644 --- a/src/components/LabCanvas/index.ts +++ b/src/components/LabCanvas/index.ts @@ -31,7 +31,7 @@ export const previewSizes: Record = { Switch: 0.35, Pin: 0.8, SMT_LED: 0.7, - SevenSegmentDisplay: 0.4, + SevenSegmentDisplayUltimate: 0.4, HDMI: 0.5, DDR: 0.5, ETH: 0.5, @@ -50,7 +50,7 @@ export const availableComponents: ComponentConfig[] = [ { type: "Switch", name: "开关" }, { type: "Pin", name: "引脚" }, { type: "SMT_LED", name: "贴片LED" }, - { type: "SevenSegmentDisplay", name: "数码管" }, + { type: "SevenSegmentDisplayUltimate", name: "数码管" }, { type: "HDMI", name: "HDMI接口" }, { type: "DDR", name: "DDR内存" }, { type: "ETH", name: "以太网接口" }, diff --git a/src/components/equipments/SevenSegmentDisplay.vue b/src/components/equipments/SevenSegmentDisplay.vue index 3a5475b..dd5e0b1 100644 --- a/src/components/equipments/SevenSegmentDisplay.vue +++ b/src/components/equipments/SevenSegmentDisplay.vue @@ -1,65 +1,114 @@ @@ -217,12 +266,12 @@ function isSegmentActive( if (isInAfterglowMode.value) { return afterglowStates.value[segment]; } - + // 如果COM口未激活,所有段都不显示 if (!currentComActive.value) { return false; } - + // 否则使用稳定状态 return stableSegmentStates.value[segment]; } @@ -232,7 +281,7 @@ function updateSegmentStates() { // 先获取COM口状态 const comPin = props.pins.find((p) => p.pinId === "COM"); let comActive = false; // 默认未激活 - + if (comPin && comPin.constraint) { const comState = getConstraintState(comPin.constraint); if (props.cathodeType === "anode") { @@ -274,7 +323,8 @@ function updateSegmentStates() { for (const pin of props.pins) { if (["a", "b", "c", "d", "e", "f", "g", "dp"].includes(pin.pinId)) { if (!pin.constraint) { - segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = false; + segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = + false; continue; } const pinState = getConstraintState(pin.constraint); @@ -285,7 +335,8 @@ function updateSegmentStates() { newState = pinState === "low"; } // 段状态只有在COM激活时才有效 - segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = newState; + segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = + newState; } } @@ -328,22 +379,25 @@ function updateAfterglowBuffers() { // 进入余晖模式 function enterAfterglowMode() { isInAfterglowMode.value = true; - + // 保存当前稳定状态作为余晖状态 for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) { const typedSegmentId = segmentId as keyof typeof stableSegmentStates.value; - afterglowStates.value[typedSegmentId] = stableSegmentStates.value[typedSegmentId]; - + afterglowStates.value[typedSegmentId] = + stableSegmentStates.value[typedSegmentId]; + // 设置定时器,在余晖持续时间后退出余晖模式 if (afterglowTimers.value[segmentId]) { clearTimeout(afterglowTimers.value[segmentId]!); } - + afterglowTimers.value[segmentId] = setTimeout(() => { afterglowStates.value[typedSegmentId] = false; - + // 检查是否所有段都已经关闭 - const allSegmentsOff = Object.values(afterglowStates.value).every(state => !state); + const allSegmentsOff = Object.values(afterglowStates.value).every( + (state) => !state, + ); if (allSegmentsOff) { exitAfterglowMode(); } @@ -354,14 +408,14 @@ function enterAfterglowMode() { // 退出余晖模式 function exitAfterglowMode() { isInAfterglowMode.value = false; - + // 清除所有定时器 for (const segmentId of ["a", "b", "c", "d", "e", "f", "g", "dp"]) { if (afterglowTimers.value[segmentId]) { clearTimeout(afterglowTimers.value[segmentId]!); afterglowTimers.value[segmentId] = null; } - + // 重置余晖状态 const typedSegmentId = segmentId as keyof typeof afterglowStates.value; afterglowStates.value[typedSegmentId] = false; @@ -397,11 +451,6 @@ onUnmounted(() => { } } }); - -// 暴露属性和方法 -defineExpose({ - updateSegmentStates, -}); diff --git a/src/components/equipments/SevenSegmentDisplayUltimate.vue b/src/components/equipments/SevenSegmentDisplayUltimate.vue new file mode 100644 index 0000000..681dba4 --- /dev/null +++ b/src/components/equipments/SevenSegmentDisplayUltimate.vue @@ -0,0 +1,413 @@ + + + + + + + diff --git a/src/stores/equipments.ts b/src/stores/equipments.ts index fd959a4..ae95632 100644 --- a/src/stores/equipments.ts +++ b/src/stores/equipments.ts @@ -7,15 +7,21 @@ import { isNumber } from "mathjs"; import { Mutex, withTimeout } from "async-mutex"; import { useConstraintsStore } from "@/stores/constraints"; import { useDialogStore } from "./dialog"; -import { toFileParameterOrUndefined } from "@/utils/Common"; +import { + base64ToArrayBuffer, + toFileParameterOrUndefined, +} from "@/utils/Common"; import { AuthManager } from "@/utils/AuthManager"; -import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr"; +import { HubConnection } from "@microsoft/signalr"; import { getHubProxyFactory, getReceiverRegister, } from "@/utils/signalR/TypedSignalR.Client"; import { ResourcePurpose, type ResourceInfo } from "@/APIClient"; -import type { IJtagHub } from "@/utils/signalR/TypedSignalR.Client/server.Hubs"; +import type { + IDigitalTubesHub, + IJtagHub, +} from "@/utils/signalR/TypedSignalR.Client/server.Hubs"; export const useEquipments = defineStore("equipments", () => { // Global Stores @@ -26,6 +32,7 @@ export const useEquipments = defineStore("equipments", () => { const boardPort = useLocalStorage("fpga-board-port", 1234); // Jtag + const enableJtagBoundaryScan = ref(false); const jtagBitstream = ref(); const jtagBoundaryScanFreq = ref(100); const jtagUserBitstreams = ref([]); @@ -62,46 +69,6 @@ export const useEquipments = defineStore("equipments", () => { } }); - // Matrix Key - const matrixKeyStates = reactive(new Array(16).fill(false)); - const matrixKeypadClientMutex = withTimeout( - new Mutex(), - 1000, - new Error("Matrixkeyclient Mutex Timeout!"), - ); - - // Power - const powerClientMutex = withTimeout( - new Mutex(), - 1000, - new Error("Matrixkeyclient Mutex Timeout!"), - ); - - // Enable Setting - const enableJtagBoundaryScan = ref(false); - const enableMatrixKey = ref(false); - const enablePower = ref(false); - - function setMatrixKey( - keyNum: number | string | undefined, - keyValue: boolean, - ): boolean { - let _keyNum: number; - if (isString(keyNum)) { - _keyNum = toNumber(keyNum); - } else if (isNumber(keyNum)) { - _keyNum = keyNum; - } else { - return false; - } - - if (z.number().nonnegative().max(16).safeParse(_keyNum).success) { - matrixKeyStates[_keyNum] = keyValue; - return true; - } - return false; - } - async function jtagBoundaryScanSetOnOff(enable: boolean) { if (isUndefined(jtagHubProxy.value)) { console.error("JtagHub Not Initialize..."); @@ -223,6 +190,34 @@ export const useEquipments = defineStore("equipments", () => { } } + // Matrix Key + const enableMatrixKey = ref(false); + const matrixKeyStates = reactive(new Array(16).fill(false)); + const matrixKeypadClientMutex = withTimeout( + new Mutex(), + 1000, + new Error("Matrixkeyclient Mutex Timeout!"), + ); + function setMatrixKey( + keyNum: number | string | undefined, + keyValue: boolean, + ): boolean { + let _keyNum: number; + if (isString(keyNum)) { + _keyNum = toNumber(keyNum); + } else if (isNumber(keyNum)) { + _keyNum = keyNum; + } else { + return false; + } + + if (z.number().nonnegative().max(16).safeParse(_keyNum).success) { + matrixKeyStates[_keyNum] = keyValue; + return true; + } + return false; + } + async function matrixKeypadSetKeyStates(keyStates: boolean[]) { const release = await matrixKeypadClientMutex.acquire(); console.log("set Key !!!!!!!!!!!!"); @@ -274,6 +269,13 @@ export const useEquipments = defineStore("equipments", () => { } } + // Power + const powerClientMutex = withTimeout( + new Mutex(), + 1000, + new Error("Matrixkeyclient Mutex Timeout!"), + ); + const enablePower = ref(false); async function powerSetOnOff(enable: boolean) { const release = await powerClientMutex.acquire(); try { @@ -293,6 +295,71 @@ export const useEquipments = defineStore("equipments", () => { } } + // Seven Segment Display + const enableSevenSegmentDisplay = ref(false); + const sevenSegmentDisplayFrequency = ref(100); + const sevenSegmentDisplayData = ref(); + const sevenSegmentDisplayHub = ref(); + const sevenSegmentDisplayHubProxy = ref(); + + async function sevenSegmentDisplaySetOnOff(enable: boolean) { + if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value) + return; + await sevenSegmentDisplayHub.value.start(); + + if (enable) { + await sevenSegmentDisplayHubProxy.value.startScan(); + } else { + await sevenSegmentDisplayHubProxy.value.stopScan(); + } + } + + async function sevenSegmentDisplaySetFrequency(frequency: number) { + if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value) + return; + await sevenSegmentDisplayHub.value.start(); + + await sevenSegmentDisplayHubProxy.value.setFrequency(frequency); + } + + async function sevenSegmentDisplayGetStatus() { + if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value) + return; + await sevenSegmentDisplayHub.value.start(); + + return await sevenSegmentDisplayHubProxy.value.getStatus(); + } + + async function handleSevenSegmentDisplayOnReceive(msg: string) { + const bytes = base64ToArrayBuffer(msg); + sevenSegmentDisplayData.value = new Uint8Array(bytes); + } + + onMounted(async () => { + // 每次挂载都重新创建连接 + sevenSegmentDisplayHub.value = + AuthManager.createAuthenticatedJtagHubConnection(); + sevenSegmentDisplayHubProxy.value = getHubProxyFactory( + "IDigitalTubesHub", + ).createHubProxy(sevenSegmentDisplayHub.value); + + getReceiverRegister("IDigitalTubesReceiver").register( + sevenSegmentDisplayHub.value, + { + onReceive: handleSevenSegmentDisplayOnReceive, + }, + ); + }); + + onUnmounted(() => { + // 断开连接,清理资源 + if (sevenSegmentDisplayHub.value) { + sevenSegmentDisplayHub.value.stop(); + sevenSegmentDisplayHub.value = undefined; + sevenSegmentDisplayHubProxy.value = undefined; + } + }); + return { boardAddr, boardPort, @@ -320,5 +387,13 @@ export const useEquipments = defineStore("equipments", () => { enablePower, powerClientMutex, powerSetOnOff, + + // Seven Segment Display + enableSevenSegmentDisplay, + sevenSegmentDisplayData, + sevenSegmentDisplayFrequency, + sevenSegmentDisplaySetOnOff, + sevenSegmentDisplaySetFrequency, + sevenSegmentDisplayGetStatus, }; }); diff --git a/src/utils/AuthManager.ts b/src/utils/AuthManager.ts index c2f8544..85eaf9a 100644 --- a/src/utils/AuthManager.ts +++ b/src/utils/AuthManager.ts @@ -229,6 +229,15 @@ export class AuthManager { .build(); } + public static createAuthenticatedDigitalTubesHubConnection() { + return new HubConnectionBuilder() + .withUrl("http://127.0.0.1:5000/hubs/DigitalTubesHub", { + accessTokenFactory: () => this.getToken() ?? "", + }) + .withAutomaticReconnect() + .build(); + } + // 登录函数 public static async login( username: string, diff --git a/src/utils/Common.ts b/src/utils/Common.ts index a8ff5cd..a937873 100644 --- a/src/utils/Common.ts +++ b/src/utils/Common.ts @@ -59,3 +59,12 @@ export function formatDate(date: Date | string) { minute: "2-digit", }); } + +export function base64ToArrayBuffer(base64: string) { + var binaryString = atob(base64); + var bytes = new Uint8Array(binaryString.length); + for (var i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; +} diff --git a/src/utils/signalR/TypedSignalR.Client/index.ts b/src/utils/signalR/TypedSignalR.Client/index.ts index 7992780..cbe2a39 100644 --- a/src/utils/signalR/TypedSignalR.Client/index.ts +++ b/src/utils/signalR/TypedSignalR.Client/index.ts @@ -4,7 +4,7 @@ // @ts-nocheck import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr'; import type { IDigitalTubesHub, IJtagHub, IProgressHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver } from './server.Hubs'; -import type { ProgressInfo } from '../server.Hubs'; +import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs'; // components @@ -107,6 +107,10 @@ class IDigitalTubesHub_HubProxy implements IDigitalTubesHub { public readonly setFrequency = async (frequency: number): Promise => { return await this.connection.invoke("SetFrequency", frequency); } + + public readonly getStatus = async (): Promise => { + return await this.connection.invoke("GetStatus"); + } } class IJtagHub_HubProxyFactory implements HubProxyFactory { diff --git a/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts b/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts index fb87cd9..87d4745 100644 --- a/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts +++ b/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts @@ -3,7 +3,7 @@ /* tslint:disable */ // @ts-nocheck import type { IStreamResult, Subject } from '@microsoft/signalr'; -import type { ProgressInfo } from '../server.Hubs'; +import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs'; export type IDigitalTubesHub = { /** @@ -19,6 +19,10 @@ export type IDigitalTubesHub = { * @returns Transpiled from System.Threading.Tasks.Task */ setFrequency(frequency: number): Promise; + /** + * @returns Transpiled from System.Threading.Tasks.Task + */ + getStatus(): Promise; } export type IJtagHub = { diff --git a/src/utils/signalR/server.Hubs.ts b/src/utils/signalR/server.Hubs.ts index b20811e..4b6b4b6 100644 --- a/src/utils/signalR/server.Hubs.ts +++ b/src/utils/signalR/server.Hubs.ts @@ -2,6 +2,14 @@ /* eslint-disable */ /* tslint:disable */ +/** Transpiled from server.Hubs.DigitalTubeTaskStatus */ +export type DigitalTubeTaskStatus = { + /** Transpiled from int */ + frequency: number; + /** Transpiled from bool */ + isRunning: boolean; +} + /** Transpiled from server.Hubs.ProgressStatus */ export enum ProgressStatus { Pending = 0, From 9bd3fb29e333cdf216f0a43ee4707d45bdac7e67 Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sat, 16 Aug 2025 13:05:01 +0800 Subject: [PATCH 02/10] =?UTF-8?q?fix:=20=E5=89=8D=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=83=E6=AE=B5=E6=95=B0=E7=A0=81=E7=AE=A1?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=AD=A3=E5=B8=B8=E5=B7=A5=E4=BD=9C=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Peripherals/SevenDigitalTubesClient.cs | 2 +- .../SevenSegmentDisplayUltimate.vue | 26 +++++++++---------- src/stores/equipments.ts | 20 +++++++------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/server/src/Peripherals/SevenDigitalTubesClient.cs b/server/src/Peripherals/SevenDigitalTubesClient.cs index 733e8a6..7028c00 100644 --- a/server/src/Peripherals/SevenDigitalTubesClient.cs +++ b/server/src/Peripherals/SevenDigitalTubesClient.cs @@ -6,7 +6,7 @@ namespace Peripherals.SevenDigitalTubesClient; static class SevenDigitalTubesAddr { - public const UInt32 BASE = 0x0000_0000; + public const UInt32 BASE = 0xB000_0000; } public class SevenDigitalTubesCtrl diff --git a/src/components/equipments/SevenSegmentDisplayUltimate.vue b/src/components/equipments/SevenSegmentDisplayUltimate.vue index 681dba4..98b4343 100644 --- a/src/components/equipments/SevenSegmentDisplayUltimate.vue +++ b/src/components/equipments/SevenSegmentDisplayUltimate.vue @@ -174,17 +174,18 @@ function isSegmentActive(segmentId: keyof typeof SEGMENT_BITS): boolean { // SignalR数字孪生集成 // ============================================================================ -const { - sevenSegmentDisplaySetOnOff, - sevenSegmentDisplayData, - sevenSegmentDisplaySetFrequency, -} = useEquipments(); +const eqps = useEquipments(); async function initDigitalTwin() { - if (!props.enableDigitalTwin || !props.digitalTwinNum) return; + if ( + !props.enableDigitalTwin || + props.digitalTwinNum < 0 || + props.digitalTwinNum > 31 + ) + return; try { - sevenSegmentDisplaySetOnOff(props.enableDigitalTwin); + eqps.sevenSegmentDisplaySetOnOff(props.enableDigitalTwin); console.log( `Digital twin initialized for address: ${props.digitalTwinNum}`, @@ -195,16 +196,16 @@ async function initDigitalTwin() { } watch( - () => [sevenSegmentDisplayData], + () => [eqps.sevenSegmentDisplayData], () => { if ( - !sevenSegmentDisplayData || + !eqps.sevenSegmentDisplayData || props.digitalTwinNum < 0 || props.digitalTwinNum > 31 ) return; - handleDigitalTwinData(sevenSegmentDisplayData[props.digitalTwinNum]); + handleDigitalTwinData(eqps.sevenSegmentDisplayData[props.digitalTwinNum]); }, ); @@ -253,7 +254,7 @@ function startAfterglow(byte: number) { } function cleanupDigitalTwin() { - sevenSegmentDisplaySetOnOff(false); + eqps.sevenSegmentDisplaySetOnOff(false); } // ============================================================================ @@ -349,7 +350,7 @@ onUnmounted(() => { // 监听模式切换 watch( - () => [props.enableDigitalTwin, props.digitalTwinNum], + () => [props.enableDigitalTwin], async () => { // 清理旧模式 cleanupDigitalTwin(); @@ -366,7 +367,6 @@ watch( updateConstraintStates(); } }, - { immediate: false }, ); diff --git a/src/stores/equipments.ts b/src/stores/equipments.ts index ae95632..09ad5d3 100644 --- a/src/stores/equipments.ts +++ b/src/stores/equipments.ts @@ -12,7 +12,7 @@ import { toFileParameterOrUndefined, } from "@/utils/Common"; import { AuthManager } from "@/utils/AuthManager"; -import { HubConnection } from "@microsoft/signalr"; +import { HubConnection, HubConnectionState } from "@microsoft/signalr"; import { getHubProxyFactory, getReceiverRegister, @@ -220,7 +220,6 @@ export const useEquipments = defineStore("equipments", () => { async function matrixKeypadSetKeyStates(keyStates: boolean[]) { const release = await matrixKeypadClientMutex.acquire(); - console.log("set Key !!!!!!!!!!!!"); try { const matrixKeypadClient = AuthManager.createAuthenticatedMatrixKeyClient(); @@ -241,9 +240,9 @@ export const useEquipments = defineStore("equipments", () => { async function matrixKeypadEnable(enable: boolean) { const release = await matrixKeypadClientMutex.acquire(); try { + const matrixKeypadClient = + AuthManager.createAuthenticatedMatrixKeyClient(); if (enable) { - const matrixKeypadClient = - AuthManager.createAuthenticatedMatrixKeyClient(); const resp = await matrixKeypadClient.enabelMatrixKey( boardAddr.value, boardPort.value, @@ -251,8 +250,6 @@ export const useEquipments = defineStore("equipments", () => { enableMatrixKey.value = resp; return resp; } else { - const matrixKeypadClient = - AuthManager.createAuthenticatedMatrixKeyClient(); const resp = await matrixKeypadClient.disableMatrixKey( boardAddr.value, boardPort.value, @@ -305,7 +302,8 @@ export const useEquipments = defineStore("equipments", () => { async function sevenSegmentDisplaySetOnOff(enable: boolean) { if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value) return; - await sevenSegmentDisplayHub.value.start(); + if (sevenSegmentDisplayHub.value.state === HubConnectionState.Disconnected) + await sevenSegmentDisplayHub.value.start(); if (enable) { await sevenSegmentDisplayHubProxy.value.startScan(); @@ -317,7 +315,8 @@ export const useEquipments = defineStore("equipments", () => { async function sevenSegmentDisplaySetFrequency(frequency: number) { if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value) return; - await sevenSegmentDisplayHub.value.start(); + if (sevenSegmentDisplayHub.value.state === HubConnectionState.Disconnected) + await sevenSegmentDisplayHub.value.start(); await sevenSegmentDisplayHubProxy.value.setFrequency(frequency); } @@ -325,7 +324,8 @@ export const useEquipments = defineStore("equipments", () => { async function sevenSegmentDisplayGetStatus() { if (!sevenSegmentDisplayHub.value || !sevenSegmentDisplayHubProxy.value) return; - await sevenSegmentDisplayHub.value.start(); + if (sevenSegmentDisplayHub.value.state === HubConnectionState.Disconnected) + await sevenSegmentDisplayHub.value.start(); return await sevenSegmentDisplayHubProxy.value.getStatus(); } @@ -338,7 +338,7 @@ export const useEquipments = defineStore("equipments", () => { onMounted(async () => { // 每次挂载都重新创建连接 sevenSegmentDisplayHub.value = - AuthManager.createAuthenticatedJtagHubConnection(); + AuthManager.createAuthenticatedDigitalTubesHubConnection(); sevenSegmentDisplayHubProxy.value = getHubProxyFactory( "IDigitalTubesHub", ).createHubProxy(sevenSegmentDisplayHub.value); From c974de593ae74d9a27418630ab6d24064fd130ef Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sat, 16 Aug 2025 14:21:26 +0800 Subject: [PATCH 03/10] =?UTF-8?q?fix:=20=E5=B0=9D=E8=AF=95=E5=8E=BB?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=90=8E=E7=AB=AF=E6=97=A0=E6=B3=95=E5=81=9C?= =?UTF-8?q?=E6=AD=A2=E6=89=AB=E6=8F=8F=E6=95=B0=E7=A0=81=E7=AE=A1=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/Hubs/DigitalTubesHub.cs | 168 ++++++++++-------- .../TypedSignalR.Client/server.Hubs.ts | 2 +- 2 files changed, 94 insertions(+), 76 deletions(-) diff --git a/server/src/Hubs/DigitalTubesHub.cs b/server/src/Hubs/DigitalTubesHub.cs index ab82713..1187e90 100644 --- a/server/src/Hubs/DigitalTubesHub.cs +++ b/server/src/Hubs/DigitalTubesHub.cs @@ -8,6 +8,8 @@ using DotNext; using Peripherals.SevenDigitalTubesClient; using System.Collections.Concurrent; +#pragma warning disable 1998 + namespace server.Hubs; [Hub] @@ -16,7 +18,7 @@ public interface IDigitalTubesHub Task StartScan(); Task StopScan(); Task SetFrequency(int frequency); - Task GetStatus(); + Task GetStatus(); } [Receiver] @@ -31,23 +33,27 @@ public class DigitalTubeTaskStatus public int Frequency { get; set; } = 100; public bool IsRunning { get; set; } = false; - public DigitalTubeTaskStatus(DigitalTubeInfo info) + public DigitalTubeTaskStatus(ScanTaskInfo info) { Frequency = info.Frequency; IsRunning = info.IsRunning; } } -public class DigitalTubeInfo +public class ScanTaskInfo { + public string BoardID { get; set; } public string ClientID { get; set; } + public Task? ScanTask { get; set; } public SevenDigitalTubesCtrl TubeClient { get; set; } - public CancellationTokenSource CTS { get; set; } = new CancellationTokenSource(); + public CancellationTokenSource CTS { get; set; } = new(); public int Frequency { get; set; } = 100; public bool IsRunning { get; set; } = false; - public DigitalTubeInfo(string clientID, SevenDigitalTubesCtrl client) + public ScanTaskInfo( + string boardID, string clientID, SevenDigitalTubesCtrl client) { + BoardID = boardID; ClientID = clientID; TubeClient = client; } @@ -58,11 +64,10 @@ public class DigitalTubeInfo public class DigitalTubesHub : Hub, IDigitalTubesHub { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - private readonly IHubContext _hubContext; private readonly Database.UserManager _userManager = new(); - private ConcurrentDictionary _infoDict = new(); + private ConcurrentDictionary<(string, string), ScanTaskInfo> _scanTasks = new(); public DigitalTubesHub(IHubContext hubContext) { @@ -95,17 +100,18 @@ public class DigitalTubesHub : Hub, IDigitalTubesHub return boardRet.Value.Value; } - private Task ScanAllTubes(DigitalTubeInfo info) + private Task ScanAllTubes(ScanTaskInfo scanInfo) { + var token = scanInfo.CTS.Token; return Task.Run(async () => { var cntError = 0; - while (!info.CTS.IsCancellationRequested) + while (!token.IsCancellationRequested) { var beginTime = DateTime.Now; - var waitTime = TimeSpan.FromMilliseconds(1000 / info.Frequency); + var waitTime = TimeSpan.FromMilliseconds(1000 / scanInfo.Frequency); - var dataRet = await info.TubeClient.ScanAllTubes(); + var dataRet = await scanInfo.TubeClient.ScanAllTubes(); if (!dataRet.IsSuccessful) { logger.Error($"Failed to scan tubes: {dataRet.Error}"); @@ -113,126 +119,138 @@ public class DigitalTubesHub : Hub, IDigitalTubesHub if (cntError > 3) { logger.Error($"Too many errors, stopping scan"); - info.IsRunning = false; + break; } } - await _hubContext.Clients.Client(info.ClientID).OnReceive(dataRet.Value); + await _hubContext.Clients.Client(scanInfo.ClientID).OnReceive(dataRet.Value); var processTime = DateTime.Now - beginTime; if (processTime < waitTime) { - await Task.Delay(waitTime - processTime); + await Task.Delay(waitTime - processTime, token); } } - }, info.CTS.Token); + scanInfo.IsRunning = false; + }, token) + .ContinueWith((task) => + { + if (task.IsFaulted) + { + logger.Error( + $"Digital tubes scan operation failesj for board {task.Exception}"); + } + else if (task.IsCanceled) + { + logger.Info( + $"Digital tubes scan operation cancelled for board {scanInfo.BoardID}"); + } + else + { + logger.Info( + $"Digital tubes scan completed successfully for board {scanInfo.BoardID}"); + } + }); } - public Task StartScan() + public async Task StartScan() { try { var board = TryGetBoard().OrThrow(() => new Exception("Board not found")); - if (_infoDict.GetOrAdd( - board.ID.ToString(), - (_) => new DigitalTubeInfo( - Context.ConnectionId, - new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 2)) - ) is DigitalTubeInfo info) - { - if (!info.IsRunning) - { - info.IsRunning = true; - if (info.CTS.IsCancellationRequested) - { - info.CTS.Dispose(); - info.CTS = new CancellationTokenSource(); - } - _ = ScanAllTubes(info); - } - } + var key = (board.ID.ToString(), Context.ConnectionId); - return Task.FromResult(true); + if (_scanTasks.TryGetValue(key, out var existing) && existing.IsRunning) + return true; + + var cts = new CancellationTokenSource(); + var scanTaskInfo = new ScanTaskInfo( + board.ID.ToString(), Context.ConnectionId, + new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 0) + ); + scanTaskInfo.ScanTask = ScanAllTubes(scanTaskInfo); + + _scanTasks[key] = scanTaskInfo; + return true; } catch (Exception ex) { logger.Error(ex, "Failed to start scan"); - return Task.FromResult(false); + return false; } } - public Task StopScan() + public async Task StopScan() { try { var board = TryGetBoard().OrThrow(() => new Exception("Board not found")); - if (_infoDict.GetOrAdd( - board.ID.ToString(), - (_) => new DigitalTubeInfo( - Context.ConnectionId, - new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 2)) - ) is DigitalTubeInfo info) - { - info.IsRunning = false; - info.CTS.Cancel(); - } + var key = (board.ID.ToString(), Context.ConnectionId); - return Task.FromResult(true); + if (_scanTasks.TryRemove(key, out var scanInfo)) + { + scanInfo.IsRunning = false; + scanInfo.CTS.Cancel(); + if (scanInfo.ScanTask != null) + await scanInfo.ScanTask; + scanInfo.CTS.Dispose(); + } + return true; } catch (Exception ex) { logger.Error(ex, "Failed to stop scan"); - return Task.FromResult(false); + return false; } } - public Task SetFrequency(int frequency) + public async Task SetFrequency(int frequency) { try { - if (frequency < 1 || frequency > 1000) return Task.FromException( - new ArgumentException("Frequency must be between 1 and 1000")); + if (frequency < 1 || frequency > 1000) + return false; var board = TryGetBoard().OrThrow(() => new Exception("Board not found")); - if (_infoDict.GetOrAdd( - board.ID.ToString(), - (_) => new DigitalTubeInfo( - Context.ConnectionId, - new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 2)) - ) is DigitalTubeInfo info) - { - info.Frequency = frequency; - } + var key = (board.ID.ToString(), Context.ConnectionId); - return Task.FromResult(true); + if (_scanTasks.TryGetValue(key, out var scanInfo) && scanInfo.IsRunning) + { + scanInfo.Frequency = frequency; + return true; + } + else + { + logger.Warn($"SetFrequency called but no running scan for board {board.ID} and client {Context.ConnectionId}"); + return false; + } } catch (Exception ex) { logger.Error(ex, "Failed to set frequency"); - return Task.FromResult(false); + return false; } } - public Task GetStatus() + public async Task GetStatus() { try { var board = TryGetBoard().OrThrow(() => new Exception("Board not found")); - if (_infoDict.GetOrAdd( - board.ID.ToString(), - (_) => new DigitalTubeInfo( - Context.ConnectionId, - new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 2)) - ) is DigitalTubeInfo info) - { - return Task.FromResult(new DigitalTubeTaskStatus(info)); - } + var key = (board.ID.ToString(), Context.ConnectionId); - return Task.FromException(new ArgumentException("Wrong argument")); + if (_scanTasks.TryGetValue(key, out var scanInfo)) + { + return new DigitalTubeTaskStatus(scanInfo); + } + else + { + return null; + } } catch (Exception ex) { logger.Error(ex, "Failed to get status"); - return Task.FromException(new Exception("Failed to get status")); + throw new Exception("Failed to get status", ex); } } diff --git a/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts b/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts index 87d4745..c4966dc 100644 --- a/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts +++ b/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts @@ -20,7 +20,7 @@ export type IDigitalTubesHub = { */ setFrequency(frequency: number): Promise; /** - * @returns Transpiled from System.Threading.Tasks.Task + * @returns Transpiled from System.Threading.Tasks.Task */ getStatus(): Promise; } From e61cf96c07c6b0e1d91a67a17c2a8c504f332aba Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sat, 16 Aug 2025 14:53:28 +0800 Subject: [PATCH 04/10] =?UTF-8?q?refactor:=20=E4=BD=BF=E7=94=A8=E6=9B=B4?= =?UTF-8?q?=E7=AE=80=E6=B4=81=E7=9A=84=E6=96=B9=E5=BC=8F=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E8=AE=A4=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LabCanvas/composable/diagramManager.ts | 4 +- .../LogicAnalyzer/LogicAnalyzerManager.ts | 228 ++- src/components/MarkdownRenderer.vue | 1556 +++++++++-------- src/components/Navbar.vue | 3 +- .../Oscilloscope/OscilloscopeManager.ts | 479 ++--- src/components/TutorialCarousel.vue | 6 +- src/components/UploadCard.vue | 8 +- .../equipments/DDSPropertyEditor.vue | 3 +- src/components/equipments/Switch.vue | 256 +-- src/stores/equipments.ts | 30 +- src/utils/AuthManager.ts | 347 +--- src/utils/BoardManager.ts | 16 +- src/views/AuthView.vue | 2 +- src/views/Exam/ExamEditModal.vue | 13 +- src/views/Exam/ExamInfoModal.vue | 12 +- src/views/Exam/Index.vue | 8 +- src/views/Project/Debugger.vue | 9 +- src/views/Project/HdmiVideoStream.vue | 1053 +++++------ src/views/Project/Index.vue | 117 +- src/views/Project/RequestBoardDialog.vue | 6 +- src/views/Project/VideoStream.vue | 2 +- src/views/User/AddBoardDialog.vue | 19 +- src/views/User/Index.vue | 12 +- src/views/User/UserInfo.vue | 18 +- 24 files changed, 2118 insertions(+), 2089 deletions(-) diff --git a/src/components/LabCanvas/composable/diagramManager.ts b/src/components/LabCanvas/composable/diagramManager.ts index a1e88d7..6a652ac 100644 --- a/src/components/LabCanvas/composable/diagramManager.ts +++ b/src/components/LabCanvas/composable/diagramManager.ts @@ -1,4 +1,4 @@ -import { ResourcePurpose } from "@/APIClient"; +import { ResourceClient, ResourcePurpose } from "@/APIClient"; import { AuthManager } from "@/utils/AuthManager"; // 定义 diagram.json 的类型结构 @@ -94,7 +94,7 @@ export async function loadDiagramData(examId?: string): Promise { // 如果提供了examId,优先从API加载实验的diagram if (examId) { try { - const resourceClient = AuthManager.createAuthenticatedResourceClient(); + const resourceClient = AuthManager.createClient(ResourceClient); // 获取diagram类型的资源列表 const resources = await resourceClient.getResourceList( diff --git a/src/components/LogicAnalyzer/LogicAnalyzerManager.ts b/src/components/LogicAnalyzer/LogicAnalyzerManager.ts index c830a7a..02ded7d 100644 --- a/src/components/LogicAnalyzer/LogicAnalyzerManager.ts +++ b/src/components/LogicAnalyzer/LogicAnalyzerManager.ts @@ -31,8 +31,16 @@ export type Channel = { // 全局模式选项 const globalModes = [ - {value: GlobalCaptureMode.AND,label: "AND",description: "所有条件都满足时触发",}, - {value: GlobalCaptureMode.OR,label: "OR",description: "任一条件满足时触发",}, + { + value: GlobalCaptureMode.AND, + label: "AND", + description: "所有条件都满足时触发", + }, + { + value: GlobalCaptureMode.OR, + label: "OR", + description: "任一条件满足时触发", + }, { value: GlobalCaptureMode.NAND, label: "NAND", description: "AND的非" }, { value: GlobalCaptureMode.NOR, label: "NOR", description: "OR的非" }, ]; @@ -70,21 +78,53 @@ const channelDivOptions = [ ]; const ClockDivOptions = [ - { value: AnalyzerClockDiv.DIV1, label: "120MHz", description: "采样频率120MHz" }, - { value: AnalyzerClockDiv.DIV2, label: "60MHz", description: "采样频率60MHz" }, - { value: AnalyzerClockDiv.DIV4, label: "30MHz", description: "采样频率30MHz" }, - { value: AnalyzerClockDiv.DIV8, label: "15MHz", description: "采样频率15MHz" }, - { value: AnalyzerClockDiv.DIV16, label: "7.5MHz", description: "采样频率7.5MHz" }, - { value: AnalyzerClockDiv.DIV32, label: "3.75MHz", description: "采样频率3.75MHz" }, - { value: AnalyzerClockDiv.DIV64, label: "1.875MHz", description: "采样频率1.875MHz" }, - { value: AnalyzerClockDiv.DIV128, label: "937.5KHz", description: "采样频率937.5KHz" }, + { + value: AnalyzerClockDiv.DIV1, + label: "120MHz", + description: "采样频率120MHz", + }, + { + value: AnalyzerClockDiv.DIV2, + label: "60MHz", + description: "采样频率60MHz", + }, + { + value: AnalyzerClockDiv.DIV4, + label: "30MHz", + description: "采样频率30MHz", + }, + { + value: AnalyzerClockDiv.DIV8, + label: "15MHz", + description: "采样频率15MHz", + }, + { + value: AnalyzerClockDiv.DIV16, + label: "7.5MHz", + description: "采样频率7.5MHz", + }, + { + value: AnalyzerClockDiv.DIV32, + label: "3.75MHz", + description: "采样频率3.75MHz", + }, + { + value: AnalyzerClockDiv.DIV64, + label: "1.875MHz", + description: "采样频率1.875MHz", + }, + { + value: AnalyzerClockDiv.DIV128, + label: "937.5KHz", + description: "采样频率937.5KHz", + }, ]; // 捕获深度限制常量 const CAPTURE_LENGTH_MIN = 1024; // 最小捕获深度 1024 const CAPTURE_LENGTH_MAX = 0x10000000 - 0x01000000; // 最大捕获深度 -// 预捕获深度限制常量 +// 预捕获深度限制常量 const PRE_CAPTURE_LENGTH_MIN = 2; // 最小预捕获深度 2 // 默认颜色数组 @@ -170,40 +210,64 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( // 转换通道数字到枚举值 const getChannelDivEnum = (channelCount: number): AnalyzerChannelDiv => { switch (channelCount) { - case 1: return AnalyzerChannelDiv.ONE; - case 2: return AnalyzerChannelDiv.TWO; - case 4: return AnalyzerChannelDiv.FOUR; - case 8: return AnalyzerChannelDiv.EIGHT; - case 16: return AnalyzerChannelDiv.XVI; - case 32: return AnalyzerChannelDiv.XXXII; - default: return AnalyzerChannelDiv.EIGHT; + case 1: + return AnalyzerChannelDiv.ONE; + case 2: + return AnalyzerChannelDiv.TWO; + case 4: + return AnalyzerChannelDiv.FOUR; + case 8: + return AnalyzerChannelDiv.EIGHT; + case 16: + return AnalyzerChannelDiv.XVI; + case 32: + return AnalyzerChannelDiv.XXXII; + default: + return AnalyzerChannelDiv.EIGHT; } }; // 验证捕获深度 - const validateCaptureLength = (value: number): { valid: boolean; message?: string } => { + const validateCaptureLength = ( + value: number, + ): { valid: boolean; message?: string } => { if (!Number.isInteger(value)) { return { valid: false, message: "捕获深度必须是整数" }; } if (value < CAPTURE_LENGTH_MIN) { - return { valid: false, message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}` }; + return { + valid: false, + message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}`, + }; } if (value > CAPTURE_LENGTH_MAX) { - return { valid: false, message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}` }; + return { + valid: false, + message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}`, + }; } return { valid: true }; }; // 验证预捕获深度 - const validatePreCaptureLength = (value: number, currentCaptureLength: number): { valid: boolean; message?: string } => { + const validatePreCaptureLength = ( + value: number, + currentCaptureLength: number, + ): { valid: boolean; message?: string } => { if (!Number.isInteger(value)) { return { valid: false, message: "预捕获深度必须是整数" }; } if (value < PRE_CAPTURE_LENGTH_MIN) { - return { valid: false, message: `预捕获深度不能小于 ${PRE_CAPTURE_LENGTH_MIN}` }; + return { + valid: false, + message: `预捕获深度不能小于 ${PRE_CAPTURE_LENGTH_MIN}`, + }; } if (value >= currentCaptureLength) { - return { valid: false, message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})` }; + return { + valid: false, + message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})`, + }; } return { valid: true }; }; @@ -215,13 +279,13 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( alert?.error(validation.message!, 3000); return false; } - + // 检查预捕获深度是否仍然有效 if (preCaptureLength.value >= value) { preCaptureLength.value = Math.max(0, value - 1); alert?.warn(`预捕获深度已自动调整为 ${preCaptureLength.value}`, 3000); } - + captureLength.value = value; return true; }; @@ -233,7 +297,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( alert?.error(validation.message!, 3000); return false; } - + preCaptureLength.value = value; return true; }; @@ -241,12 +305,12 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( // 设置通道组 const setChannelDiv = (channelCount: number) => { // 验证通道数量是否有效 - if (!channelDivOptions.find(option => option.value === channelCount)) { + if (!channelDivOptions.find((option) => option.value === channelCount)) { console.error(`无效的通道组设置: ${channelCount}`); return; } currentChannelDiv.value = channelCount; - + // 禁用所有通道 channels.forEach((channel) => { channel.enabled = false; @@ -257,7 +321,9 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( channels[i].enabled = true; } - const option = channelDivOptions.find(opt => opt.value === channelCount); + const option = channelDivOptions.find( + (opt) => opt.value === channelCount, + ); alert?.success(`已设置为${option?.label}`, 2000); }; @@ -294,7 +360,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( const getCaptureData = async () => { try { - const client = AuthManager.createAuthenticatedLogicAnalyzerClient(); + const client = AuthManager.createClient(LogicAnalyzerClient); // 获取捕获数据,使用当前设置的捕获长度 const base64Data = await client.getCaptureData(captureLength.value); @@ -308,7 +374,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( // 根据当前通道数量解析数据 const channelCount = currentChannelDiv.value; const timeStepNs = currentSamplePeriodNs.value; - + let sampleCount: number; let x: number[]; let y: number[][]; @@ -316,19 +382,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( if (channelCount === 1) { // 1通道:每个字节包含8个时间单位的数据 sampleCount = bytes.length * 8; - + // 创建时间轴 x = Array.from( { length: sampleCount }, (_, i) => (i * timeStepNs) / 1000, ); // 转换为微秒 - + // 创建通道数据数组 - y = Array.from( - { length: 1 }, - () => new Array(sampleCount), - ); - + y = Array.from({ length: 1 }, () => new Array(sampleCount)); + // 解析数据:每个字节的8个位对应8个时间单位 for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) { const byte = bytes[byteIndex]; @@ -340,19 +403,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( } else if (channelCount === 2) { // 2通道:每个字节包含4个时间单位的数据 sampleCount = bytes.length * 4; - + // 创建时间轴 x = Array.from( { length: sampleCount }, (_, i) => (i * timeStepNs) / 1000, ); // 转换为微秒 - + // 创建通道数据数组 - y = Array.from( - { length: 2 }, - () => new Array(sampleCount), - ); - + y = Array.from({ length: 2 }, () => new Array(sampleCount)); + // 解析数据:每个字节的8个位对应4个时间单位的2通道数据 // 位分布:[T3_CH1, T3_CH0, T2_CH1, T2_CH0, T1_CH1, T1_CH0, T0_CH1, T0_CH0] for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) { @@ -360,37 +420,34 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( for (let timeUnit = 0; timeUnit < 4; timeUnit++) { const timeIndex = byteIndex * 4 + timeUnit; const bitOffset = timeUnit * 2; - y[0][timeIndex] = (byte >> bitOffset) & 1; // CH0 + y[0][timeIndex] = (byte >> bitOffset) & 1; // CH0 y[1][timeIndex] = (byte >> (bitOffset + 1)) & 1; // CH1 } } } else if (channelCount === 4) { // 4通道:每个字节包含2个时间单位的数据 sampleCount = bytes.length * 2; - + // 创建时间轴 x = Array.from( { length: sampleCount }, (_, i) => (i * timeStepNs) / 1000, ); // 转换为微秒 - + // 创建通道数据数组 - y = Array.from( - { length: 4 }, - () => new Array(sampleCount), - ); - + y = Array.from({ length: 4 }, () => new Array(sampleCount)); + // 解析数据:每个字节的8个位对应2个时间单位的4通道数据 // 位分布:[T1_CH3, T1_CH2, T1_CH1, T1_CH0, T0_CH3, T0_CH2, T0_CH1, T0_CH0] for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) { const byte = bytes[byteIndex]; - + // 处理第一个时间单位(低4位) const timeIndex1 = byteIndex * 2; for (let channel = 0; channel < 4; channel++) { y[channel][timeIndex1] = (byte >> channel) & 1; } - + // 处理第二个时间单位(高4位) const timeIndex2 = byteIndex * 2 + 1; for (let channel = 0; channel < 4; channel++) { @@ -400,19 +457,16 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( } else if (channelCount === 8) { // 8通道:每个字节包含1个时间单位的8个通道数据 sampleCount = bytes.length; - + // 创建时间轴 x = Array.from( { length: sampleCount }, (_, i) => (i * timeStepNs) / 1000, ); // 转换为微秒 - + // 创建8个通道的数据 - y = Array.from( - { length: 8 }, - () => new Array(sampleCount), - ); - + y = Array.from({ length: 8 }, () => new Array(sampleCount)); + // 解析每个字节的8个位到对应通道 for (let i = 0; i < sampleCount; i++) { const byte = bytes[i]; @@ -424,30 +478,27 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( } else if (channelCount === 16) { // 16通道:每2个字节包含1个时间单位的16个通道数据 sampleCount = bytes.length / 2; - + // 创建时间轴 x = Array.from( { length: sampleCount }, (_, i) => (i * timeStepNs) / 1000, ); // 转换为微秒 - + // 创建16个通道的数据 - y = Array.from( - { length: 16 }, - () => new Array(sampleCount), - ); - + y = Array.from({ length: 16 }, () => new Array(sampleCount)); + // 解析数据:每2个字节为一个时间单位 for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) { const byteIndex = timeIndex * 2; - const byte1 = bytes[byteIndex]; // [7:0] + const byte1 = bytes[byteIndex]; // [7:0] const byte2 = bytes[byteIndex + 1]; // [15:8] - + // 处理低8位通道 [7:0] for (let channel = 0; channel < 8; channel++) { y[channel][timeIndex] = (byte1 >> channel) & 1; } - + // 处理高8位通道 [15:8] for (let channel = 0; channel < 8; channel++) { y[channel + 8][timeIndex] = (byte2 >> channel) & 1; @@ -456,42 +507,39 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( } else if (channelCount === 32) { // 32通道:每4个字节包含1个时间单位的32个通道数据 sampleCount = bytes.length / 4; - + // 创建时间轴 x = Array.from( { length: sampleCount }, (_, i) => (i * timeStepNs) / 1000, ); // 转换为微秒 - + // 创建32个通道的数据 - y = Array.from( - { length: 32 }, - () => new Array(sampleCount), - ); - + y = Array.from({ length: 32 }, () => new Array(sampleCount)); + // 解析数据:每4个字节为一个时间单位 for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) { const byteIndex = timeIndex * 4; - const byte1 = bytes[byteIndex]; // [7:0] + const byte1 = bytes[byteIndex]; // [7:0] const byte2 = bytes[byteIndex + 1]; // [15:8] const byte3 = bytes[byteIndex + 2]; // [23:16] const byte4 = bytes[byteIndex + 3]; // [31:24] - + // 处理 [7:0] for (let channel = 0; channel < 8; channel++) { y[channel][timeIndex] = (byte1 >> channel) & 1; } - + // 处理 [15:8] for (let channel = 0; channel < 8; channel++) { y[channel + 8][timeIndex] = (byte2 >> channel) & 1; } - + // 处理 [23:16] for (let channel = 0; channel < 8; channel++) { y[channel + 16][timeIndex] = (byte3 >> channel) & 1; } - + // 处理 [31:24] for (let channel = 0; channel < 8; channel++) { y[channel + 24][timeIndex] = (byte4 >> channel) & 1; @@ -525,11 +573,11 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( isCapturing.value = true; const release = await operationMutex.acquire(); try { - const client = AuthManager.createAuthenticatedLogicAnalyzerClient(); + const client = AuthManager.createClient(LogicAnalyzerClient); // 1. 先应用配置 alert?.info("正在应用配置...", 2000); - + // 准备配置数据 - 包含所有32个通道,未启用的通道设置为默认值 const allSignals = signalConfigs.map((signal, index) => { if (channels[index].enabled) { @@ -632,7 +680,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( const release = await operationMutex.acquire(); try { - const client = AuthManager.createAuthenticatedLogicAnalyzerClient(); + const client = AuthManager.createClient(LogicAnalyzerClient); // 执行强制捕获来停止当前捕获 const forceSuccess = await client.setCaptureMode(false, false); @@ -661,7 +709,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( const release = await operationMutex.acquire(); try { - const client = AuthManager.createAuthenticatedLogicAnalyzerClient(); + const client = AuthManager.createClient(LogicAnalyzerClient); // 执行强制捕获来停止当前捕获 const forceSuccess = await client.setCaptureMode(true, true); @@ -677,7 +725,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState( `强制捕获失败: ${error instanceof Error ? error.message : "未知错误"}`, 3000, ); - } finally{ + } finally { release(); } }; diff --git a/src/components/MarkdownRenderer.vue b/src/components/MarkdownRenderer.vue index 43255b3..a46c008 100644 --- a/src/components/MarkdownRenderer.vue +++ b/src/components/MarkdownRenderer.vue @@ -1,759 +1,797 @@ - - - - - + + + + + diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue index 89730c9..aa274e1 100644 --- a/src/components/Navbar.vue +++ b/src/components/Navbar.vue @@ -145,6 +145,7 @@ import { ChevronDownIcon, } from "lucide-vue-next"; import { AuthManager } from "@/utils/AuthManager"; +import { DataClient } from "@/APIClient"; const router = useRouter(); @@ -158,7 +159,7 @@ const loadUserInfo = async () => { try { const authenticated = await AuthManager.isAuthenticated(); if (authenticated) { - const client = AuthManager.createAuthenticatedDataClient(); + const client = AuthManager.createClient(DataClient); const userInfo = await client.getUserInfo(); userName.value = userInfo.name; isLoggedIn.value = true; diff --git a/src/components/Oscilloscope/OscilloscopeManager.ts b/src/components/Oscilloscope/OscilloscopeManager.ts index a200527..6d007f5 100644 --- a/src/components/Oscilloscope/OscilloscopeManager.ts +++ b/src/components/Oscilloscope/OscilloscopeManager.ts @@ -4,6 +4,7 @@ import { Mutex } from "async-mutex"; import { OscilloscopeFullConfig, OscilloscopeDataResponse, + OscilloscopeApiClient, } from "@/APIClient"; import { AuthManager } from "@/utils/AuthManager"; import { useAlertStore } from "@/components/Alert"; @@ -31,257 +32,269 @@ const DEFAULT_CONFIG: OscilloscopeFullConfig = new OscilloscopeFullConfig({ }); // 采样频率常量(后端返回) -const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(() => { - const oscData = shallowRef(); - const alert = useRequiredInjection(useAlertStore); +const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState( + () => { + const oscData = shallowRef(); + const alert = useRequiredInjection(useAlertStore); - // 互斥锁 - const operationMutex = new Mutex(); + // 互斥锁 + const operationMutex = new Mutex(); - // 状态 - const isApplying = ref(false); - const isCapturing = ref(false); + // 状态 + const isApplying = ref(false); + const isCapturing = ref(false); - // 配置 - const config = reactive(new OscilloscopeFullConfig({ ...DEFAULT_CONFIG })); + // 配置 + const config = reactive( + new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }), + ); - // 采样点数(由后端数据决定) - const sampleCount = ref(0); + // 采样点数(由后端数据决定) + const sampleCount = ref(0); - // 采样周期(ns),由adFrequency计算 - const samplePeriodNs = computed(() => - oscData.value?.adFrequency ? 1_000_000_000 / oscData.value.adFrequency : 200 - ); + // 采样周期(ns),由adFrequency计算 + const samplePeriodNs = computed(() => + oscData.value?.adFrequency + ? 1_000_000_000 / oscData.value.adFrequency + : 200, + ); - // 应用配置 - const applyConfiguration = async () => { - if (operationMutex.isLocked()) { - alert.warn("有其他操作正在进行中,请稍后再试", 3000); - return; - } - const release = await operationMutex.acquire(); - isApplying.value = true; - try { - const client = AuthManager.createAuthenticatedOscilloscopeApiClient(); - const success = await client.initialize({ ...config }); - if (success) { - alert.success("示波器配置已应用", 2000); - } else { - throw new Error("应用失败"); + // 应用配置 + const applyConfiguration = async () => { + if (operationMutex.isLocked()) { + alert.warn("有其他操作正在进行中,请稍后再试", 3000); + return; } - } catch (error) { - alert.error("应用配置失败", 3000); - } finally { - isApplying.value = false; - release(); - } - }; - - // 重置配置 - const resetConfiguration = () => { - Object.assign(config, { ...DEFAULT_CONFIG }); - alert.info("配置已重置", 2000); - }; - - const clearOscilloscopeData = () => { - oscData.value = undefined; - } - - // 获取数据 - const getOscilloscopeData = async () => { - try { - const client = AuthManager.createAuthenticatedOscilloscopeApiClient(); - const resp: OscilloscopeDataResponse = await client.getData(); - - // 解析波形数据 - const binaryString = atob(resp.waveformData); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); + const release = await operationMutex.acquire(); + isApplying.value = true; + try { + const client = AuthManager.createClient(OscilloscopeApiClient); + const success = await client.initialize({ ...config }); + if (success) { + alert.success("示波器配置已应用", 2000); + } else { + throw new Error("应用失败"); + } + } catch (error) { + alert.error("应用配置失败", 3000); + } finally { + isApplying.value = false; + release(); } - sampleCount.value = bytes.length; + }; - // 构建时间轴 + // 重置配置 + const resetConfiguration = () => { + Object.assign(config, { ...DEFAULT_CONFIG }); + alert.info("配置已重置", 2000); + }; + + const clearOscilloscopeData = () => { + oscData.value = undefined; + }; + + // 获取数据 + const getOscilloscopeData = async () => { + try { + const client = AuthManager.createClient(OscilloscopeApiClient); + const resp: OscilloscopeDataResponse = await client.getData(); + + // 解析波形数据 + const binaryString = atob(resp.waveformData); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + sampleCount.value = bytes.length; + + // 构建时间轴 + const x = Array.from( + { length: bytes.length }, + (_, i) => (i * samplePeriodNs.value) / 1000, // us + ); + const y = Array.from(bytes); + + oscData.value = { + x, + y, + xUnit: "us", + yUnit: "V", + adFrequency: resp.adFrequency, + adVpp: resp.adVpp, + adMax: resp.adMax, + adMin: resp.adMin, + }; + } catch (error) { + alert.error("获取示波器数据失败", 3000); + } + }; + + // 定时器引用 + let refreshIntervalId: number | undefined; + // 刷新间隔(毫秒),可根据需要调整 + const refreshIntervalMs = ref(1000); + + // 定时刷新函数 + const startAutoRefresh = () => { + if (refreshIntervalId !== undefined) return; + refreshIntervalId = window.setInterval(async () => { + await refreshRAM(); + await getOscilloscopeData(); + }, refreshIntervalMs.value); + }; + + const stopAutoRefresh = () => { + if (refreshIntervalId !== undefined) { + clearInterval(refreshIntervalId); + refreshIntervalId = undefined; + isCapturing.value = false; + } + }; + + // 启动捕获 + const startCapture = async () => { + if (operationMutex.isLocked()) { + alert.warn("有其他操作正在进行中,请稍后再试", 3000); + return; + } + isCapturing.value = true; + const release = await operationMutex.acquire(); + try { + const client = AuthManager.createClient(OscilloscopeApiClient); + const started = await client.startCapture(); + if (!started) throw new Error("无法启动捕获"); + alert.info("开始捕获...", 2000); + + // 启动定时刷新 + startAutoRefresh(); + } catch (error) { + alert.error("捕获失败", 3000); + isCapturing.value = false; + stopAutoRefresh(); + } finally { + release(); + } + }; + + // 停止捕获 + const stopCapture = async () => { + if (!isCapturing.value) { + alert.warn("当前没有正在进行的捕获操作", 2000); + return; + } + isCapturing.value = false; + stopAutoRefresh(); + const release = await operationMutex.acquire(); + try { + const client = AuthManager.createClient(OscilloscopeApiClient); + const stopped = await client.stopCapture(); + if (!stopped) throw new Error("无法停止捕获"); + alert.info("捕获已停止", 2000); + } catch (error) { + alert.error("停止捕获失败", 3000); + } finally { + release(); + } + }; + + // 更新触发参数 + const updateTrigger = async (level: number, risingEdge: boolean) => { + const client = AuthManager.createClient(OscilloscopeApiClient); + try { + const ok = await client.updateTrigger(level, risingEdge); + if (ok) { + config.triggerLevel = level; + config.triggerRisingEdge = risingEdge; + alert.success("触发参数已更新", 2000); + } else { + throw new Error(); + } + } catch { + alert.error("更新触发参数失败", 2000); + } + }; + + // 更新采样参数 + const updateSampling = async ( + horizontalShift: number, + decimationRate: number, + ) => { + const client = AuthManager.createClient(OscilloscopeApiClient); + try { + const ok = await client.updateSampling(horizontalShift, decimationRate); + if (ok) { + config.horizontalShift = horizontalShift; + config.decimationRate = decimationRate; + alert.success("采样参数已更新", 2000); + } else { + throw new Error(); + } + } catch { + alert.error("更新采样参数失败", 2000); + } + }; + + // 手动刷新RAM + const refreshRAM = async () => { + const client = AuthManager.createClient(OscilloscopeApiClient); + try { + const ok = await client.refreshRAM(); + if (ok) { + // alert.success("RAM已刷新", 2000); + } else { + throw new Error(); + } + } catch { + alert.error("刷新RAM失败", 2000); + } + }; + + // 生成测试数据 + const generateTestData = () => { + const freq = 5_000_000; + const duration = 0.001; // 1ms + const points = Math.floor(freq * duration); const x = Array.from( - { length: bytes.length }, - (_, i) => (i * samplePeriodNs.value) / 1000 // us + { length: points }, + (_, i) => (i * 1_000_000_000) / freq / 1000, + ); + const y = Array.from({ length: points }, (_, i) => + Math.floor(Math.sin(i * 0.01) * 127 + 128), ); - const y = Array.from(bytes); - oscData.value = { x, y, xUnit: "us", yUnit: "V", - adFrequency: resp.adFrequency, - adVpp: resp.adVpp, - adMax: resp.adMax, - adMin: resp.adMin, + adFrequency: freq, + adVpp: 2.0, + adMax: 255, + adMin: 0, }; - } catch (error) { - alert.error("获取示波器数据失败", 3000); - } - }; - - // 定时器引用 - let refreshIntervalId: number | undefined; - // 刷新间隔(毫秒),可根据需要调整 - const refreshIntervalMs = ref(1000); - - // 定时刷新函数 - const startAutoRefresh = () => { - if (refreshIntervalId !== undefined) return; - refreshIntervalId = window.setInterval(async () => { - await refreshRAM(); - await getOscilloscopeData(); - }, refreshIntervalMs.value); - }; - - const stopAutoRefresh = () => { - if (refreshIntervalId !== undefined) { - clearInterval(refreshIntervalId); - refreshIntervalId = undefined; - isCapturing.value = false; - } - }; - - // 启动捕获 - const startCapture = async () => { - if (operationMutex.isLocked()) { - alert.warn("有其他操作正在进行中,请稍后再试", 3000); - return; - } - isCapturing.value = true; - const release = await operationMutex.acquire(); - try { - const client = AuthManager.createAuthenticatedOscilloscopeApiClient(); - const started = await client.startCapture(); - if (!started) throw new Error("无法启动捕获"); - alert.info("开始捕获...", 2000); - - // 启动定时刷新 - startAutoRefresh(); - } catch (error) { - alert.error("捕获失败", 3000); - isCapturing.value = false; - stopAutoRefresh(); - } finally { - release(); - } - }; - - // 停止捕获 - const stopCapture = async () => { - if (!isCapturing.value) { - alert.warn("当前没有正在进行的捕获操作", 2000); - return; - } - isCapturing.value = false; - stopAutoRefresh(); - const release = await operationMutex.acquire(); - try { - const client = AuthManager.createAuthenticatedOscilloscopeApiClient(); - const stopped = await client.stopCapture(); - if (!stopped) throw new Error("无法停止捕获"); - alert.info("捕获已停止", 2000); - } catch (error) { - alert.error("停止捕获失败", 3000); - } finally { - release(); - } - }; - - // 更新触发参数 - const updateTrigger = async (level: number, risingEdge: boolean) => { - const client = AuthManager.createAuthenticatedOscilloscopeApiClient(); - try { - const ok = await client.updateTrigger(level, risingEdge); - if (ok) { - config.triggerLevel = level; - config.triggerRisingEdge = risingEdge; - alert.success("触发参数已更新", 2000); - } else { - throw new Error(); - } - } catch { - alert.error("更新触发参数失败", 2000); - } - }; - - // 更新采样参数 - const updateSampling = async (horizontalShift: number, decimationRate: number) => { - const client = AuthManager.createAuthenticatedOscilloscopeApiClient(); - try { - const ok = await client.updateSampling(horizontalShift, decimationRate); - if (ok) { - config.horizontalShift = horizontalShift; - config.decimationRate = decimationRate; - alert.success("采样参数已更新", 2000); - } else { - throw new Error(); - } - } catch { - alert.error("更新采样参数失败", 2000); - } - }; - - // 手动刷新RAM - const refreshRAM = async () => { - const client = AuthManager.createAuthenticatedOscilloscopeApiClient(); - try { - const ok = await client.refreshRAM(); - if (ok) { - // alert.success("RAM已刷新", 2000); - } else { - throw new Error(); - } - } catch { - alert.error("刷新RAM失败", 2000); - } - }; - - // 生成测试数据 - const generateTestData = () => { - const freq = 5_000_000; - const duration = 0.001; // 1ms - const points = Math.floor(freq * duration); - const x = Array.from({ length: points }, (_, i) => (i * 1_000_000_000 / freq) / 1000); - const y = Array.from({ length: points }, (_, i) => - Math.floor(Math.sin(i * 0.01) * 127 + 128) - ); - oscData.value = { - x, - y, - xUnit: "us", - yUnit: "V", - adFrequency: freq, - adVpp: 2.0, - adMax: 255, - adMin: 0, + alert.success("测试数据生成成功", 2000); }; - alert.success("测试数据生成成功", 2000); - }; - return { - oscData, - config, - isApplying, - isCapturing, - sampleCount, - samplePeriodNs, - refreshIntervalMs, + return { + oscData, + config, + isApplying, + isCapturing, + sampleCount, + samplePeriodNs, + refreshIntervalMs, - applyConfiguration, - resetConfiguration, - clearOscilloscopeData, - getOscilloscopeData, - startCapture, - stopCapture, - updateTrigger, - updateSampling, - refreshRAM, - generateTestData, - }; -}); + applyConfiguration, + resetConfiguration, + clearOscilloscopeData, + getOscilloscopeData, + startCapture, + stopCapture, + updateTrigger, + updateSampling, + refreshRAM, + generateTestData, + }; + }, +); -export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG }; \ No newline at end of file +export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG }; diff --git a/src/components/TutorialCarousel.vue b/src/components/TutorialCarousel.vue index 9bc01e0..e94d175 100644 --- a/src/components/TutorialCarousel.vue +++ b/src/components/TutorialCarousel.vue @@ -81,7 +81,7 @@ import { ref, onMounted, onUnmounted } from "vue"; import { useRouter } from "vue-router"; import { AuthManager } from "@/utils/AuthManager"; -import type { ExamInfo } from "@/APIClient"; +import { ExamClient, ResourceClient, type ExamInfo } from "@/APIClient"; // 接口定义 interface Tutorial { @@ -121,7 +121,7 @@ onMounted(async () => { console.log("正在从数据库加载实验数据..."); // 创建认证客户端 - const client = AuthManager.createAuthenticatedExamClient(); + const client = AuthManager.createClient(ExamClient); // 获取实验列表 const examList: ExamInfo[] = await client.getExamList(); @@ -142,7 +142,7 @@ onMounted(async () => { try { // 获取实验的封面资源(模板资源) - const resourceClient = AuthManager.createAuthenticatedResourceClient(); + const resourceClient = AuthManager.createClient(ResourceClient); const resourceList = await resourceClient.getResourceList( exam.id, "cover", diff --git a/src/components/UploadCard.vue b/src/components/UploadCard.vue index 7853981..fe5348d 100644 --- a/src/components/UploadCard.vue +++ b/src/components/UploadCard.vue @@ -95,6 +95,7 @@ import { import { ProgressStatus } from "@/utils/signalR/server.Hubs"; import { useRequiredInjection } from "@/utils/Common"; import { useAlertStore } from "./Alert"; +import { ResourceClient } from "@/APIClient"; interface Props { maxMemory?: number; @@ -138,8 +139,7 @@ const progressHubReceiver: IProgressReceiver = { }, }; onMounted(async () => { - progressHubConnection.value = - AuthManager.createAuthenticatedProgressHubConnection(); + progressHubConnection.value = AuthManager.createHubConnection("ProgressHub"); progressHubProxy.value = getHubProxyFactory("IProgressHub").createHubProxy( progressHubConnection.value, ); @@ -175,7 +175,7 @@ async function loadAvailableBitstreams() { } try { - const resourceClient = AuthManager.createAuthenticatedResourceClient(); + const resourceClient = AuthManager.createClient(ResourceClient); // 使用新的ResourceClient API获取比特流模板资源列表 const resources = await resourceClient.getResourceList( props.examId, @@ -199,7 +199,7 @@ async function downloadExampleBitstream(bitstream: { isDownloading.value = true; try { - const resourceClient = AuthManager.createAuthenticatedResourceClient(); + const resourceClient = AuthManager.createClient(ResourceClient); // 使用新的ResourceClient API获取资源文件 const response = await resourceClient.getResourceById(bitstream.id); diff --git a/src/components/equipments/DDSPropertyEditor.vue b/src/components/equipments/DDSPropertyEditor.vue index 937ac17..ca60fc2 100644 --- a/src/components/equipments/DDSPropertyEditor.vue +++ b/src/components/equipments/DDSPropertyEditor.vue @@ -212,6 +212,7 @@ import { useEquipments } from "@/stores/equipments"; import { useDialogStore } from "@/stores/dialog"; import { toInteger } from "lodash"; import { AuthManager } from "@/utils/AuthManager"; +import { DDSClient } from "@/APIClient"; // Component Attributes const props = defineProps<{ @@ -221,7 +222,7 @@ const props = defineProps<{ const emit = defineEmits(["update:modelValue"]); // Global varibles -const dds = AuthManager.createAuthenticatedDDSClient(); +const dds = AuthManager.createClient(DDSClient); const eqps = useEquipments(); const dialog = useDialogStore(); diff --git a/src/components/equipments/Switch.vue b/src/components/equipments/Switch.vue index a0018fe..05d792a 100644 --- a/src/components/equipments/Switch.vue +++ b/src/components/equipments/Switch.vue @@ -1,17 +1,30 @@ -// filepath: c:\_Project\FPGA_WebLab\FPGA_WebLab\src\components\equipments\Switch.vue @@ -194,16 +202,14 @@ defineExpose({ display: block; padding: 0; margin: 0; - line-height: 0; /* 移除行高导致的额外间距 */ - font-size: 0; /* 防止文本节点造成的间距 */ + line-height: 0; + font-size: 0; box-sizing: content-box; overflow: visible; } - rect { transition: all 100ms ease-in-out; } - .interactive { cursor: pointer; } diff --git a/src/stores/equipments.ts b/src/stores/equipments.ts index 09ad5d3..f19edec 100644 --- a/src/stores/equipments.ts +++ b/src/stores/equipments.ts @@ -17,7 +17,14 @@ import { getHubProxyFactory, getReceiverRegister, } from "@/utils/signalR/TypedSignalR.Client"; -import { ResourcePurpose, type ResourceInfo } from "@/APIClient"; +import { + JtagClient, + MatrixKeyClient, + PowerClient, + ResourceClient, + ResourcePurpose, + type ResourceInfo, +} from "@/APIClient"; import type { IDigitalTubesHub, IJtagHub, @@ -46,8 +53,7 @@ export const useEquipments = defineStore("equipments", () => { onMounted(async () => { // 每次挂载都重新创建连接 - jtagHubConnection.value = - AuthManager.createAuthenticatedJtagHubConnection(); + jtagHubConnection.value = AuthManager.createHubConnection("JtagHub"); jtagHubProxy.value = getHubProxyFactory("IJtagHub").createHubProxy( jtagHubConnection.value, ); @@ -101,7 +107,7 @@ export const useEquipments = defineStore("equipments", () => { // 自动开启电源 await powerSetOnOff(true); - const resourceClient = AuthManager.createAuthenticatedResourceClient(); + const resourceClient = AuthManager.createClient(ResourceClient); const resp = await resourceClient.addResource( "bitstream", ResourcePurpose.User, @@ -133,7 +139,7 @@ export const useEquipments = defineStore("equipments", () => { // 自动开启电源 await powerSetOnOff(true); - const jtagClient = AuthManager.createAuthenticatedJtagClient(); + const jtagClient = AuthManager.createClient(JtagClient); const resp = await jtagClient.downloadBitstream( boardAddr.value, boardPort.value, @@ -155,7 +161,7 @@ export const useEquipments = defineStore("equipments", () => { // 自动开启电源 await powerSetOnOff(true); - const jtagClient = AuthManager.createAuthenticatedJtagClient(); + const jtagClient = AuthManager.createClient(JtagClient); const resp = await jtagClient.getDeviceIDCode( boardAddr.value, boardPort.value, @@ -175,7 +181,7 @@ export const useEquipments = defineStore("equipments", () => { // 自动开启电源 await powerSetOnOff(true); - const jtagClient = AuthManager.createAuthenticatedJtagClient(); + const jtagClient = AuthManager.createClient(JtagClient); const resp = await jtagClient.setSpeed( boardAddr.value, boardPort.value, @@ -221,8 +227,7 @@ export const useEquipments = defineStore("equipments", () => { async function matrixKeypadSetKeyStates(keyStates: boolean[]) { const release = await matrixKeypadClientMutex.acquire(); try { - const matrixKeypadClient = - AuthManager.createAuthenticatedMatrixKeyClient(); + const matrixKeypadClient = AuthManager.createClient(MatrixKeyClient); const resp = await matrixKeypadClient.setMatrixKeyStatus( boardAddr.value, boardPort.value, @@ -240,8 +245,7 @@ export const useEquipments = defineStore("equipments", () => { async function matrixKeypadEnable(enable: boolean) { const release = await matrixKeypadClientMutex.acquire(); try { - const matrixKeypadClient = - AuthManager.createAuthenticatedMatrixKeyClient(); + const matrixKeypadClient = AuthManager.createClient(MatrixKeyClient); if (enable) { const resp = await matrixKeypadClient.enabelMatrixKey( boardAddr.value, @@ -276,7 +280,7 @@ export const useEquipments = defineStore("equipments", () => { async function powerSetOnOff(enable: boolean) { const release = await powerClientMutex.acquire(); try { - const powerClient = AuthManager.createAuthenticatedPowerClient(); + const powerClient = AuthManager.createClient(PowerClient); const resp = await powerClient.setPowerOnOff( boardAddr.value, boardPort.value, @@ -338,7 +342,7 @@ export const useEquipments = defineStore("equipments", () => { onMounted(async () => { // 每次挂载都重新创建连接 sevenSegmentDisplayHub.value = - AuthManager.createAuthenticatedDigitalTubesHubConnection(); + AuthManager.createHubConnection("DigitalTubesHub"); sevenSegmentDisplayHubProxy.value = getHubProxyFactory( "IDigitalTubesHub", ).createHubProxy(sevenSegmentDisplayHub.value); diff --git a/src/utils/AuthManager.ts b/src/utils/AuthManager.ts index 85eaf9a..3aec628 100644 --- a/src/utils/AuthManager.ts +++ b/src/utils/AuthManager.ts @@ -1,322 +1,105 @@ -import { - DataClient, - VideoStreamClient, - BsdlParserClient, - DDSClient, - JtagClient, - MatrixKeyClient, - PowerClient, - RemoteUpdateClient, - TutorialClient, - UDPClient, - LogicAnalyzerClient, - NetConfigClient, - OscilloscopeApiClient, - DebuggerClient, - ExamClient, - ResourceClient, - HdmiVideoStreamClient, -} from "@/APIClient"; -import router from "@/router"; +import { DataClient } from "@/APIClient"; import { HubConnectionBuilder } from "@microsoft/signalr"; import axios, { type AxiosInstance } from "axios"; -import { isNull } from "lodash"; - -// 支持的客户端类型联合类型 -type SupportedClient = - | DataClient - | VideoStreamClient - | BsdlParserClient - | DDSClient - | JtagClient - | MatrixKeyClient - | PowerClient - | RemoteUpdateClient - | TutorialClient - | LogicAnalyzerClient - | UDPClient - | NetConfigClient - | OscilloscopeApiClient - | DebuggerClient - | ExamClient - | ResourceClient - | HdmiVideoStreamClient; +// 简单到让人想哭的认证管理器 export class AuthManager { - // 存储token到localStorage - public static setToken(token: string): void { - localStorage.setItem("authToken", token); + private static readonly TOKEN_KEY = "authToken"; + + // 核心数据:就是个字符串 + static getToken(): string | null { + return localStorage.getItem(this.TOKEN_KEY); } - // 从localStorage获取token - public static getToken(): string | null { - return localStorage.getItem("authToken"); + static setToken(token: string): void { + localStorage.setItem(this.TOKEN_KEY, token); } - // 清除token - public static clearToken(): void { - localStorage.removeItem("authToken"); + static clearToken(): void { + localStorage.removeItem(this.TOKEN_KEY); } - // 检查是否已认证 - public static async isAuthenticated(): Promise { - return await AuthManager.verifyToken(); + // 核心功能:创建带认证的HTTP配置 + static getAuthHeaders(): Record { + const token = this.getToken(); + return token ? { Authorization: `Bearer ${token}` } : {}; } - // 通用的为HTTP请求添加Authorization header的方法 - public static addAuthHeader(client: SupportedClient): void { - const token = AuthManager.getToken(); - if (token) { - // 创建一个自定义的 http 对象,包装原有的 fetch 方法 - const customHttp = { - fetch: (url: RequestInfo, init?: RequestInit) => { - if (!init) init = {}; - if (!init.headers) init.headers = {}; - - // 添加Authorization header - if (typeof init.headers === "object" && init.headers !== null) { - (init.headers as any)["Authorization"] = `Bearer ${token}`; - } - - // 使用全局 fetch 或 window.fetch - return (window as any).fetch(url, init); - }, - }; - - // 重新构造客户端,传入自定义的 http 对象 - const ClientClass = client.constructor as new ( - baseUrl?: string, - http?: any, - ) => SupportedClient; - const newClient = new ClientClass(undefined, customHttp); - - // 将新客户端的属性复制到原客户端(这是一个 workaround) - // 更好的做法是返回新的客户端实例 - Object.setPrototypeOf(client, Object.getPrototypeOf(newClient)); - Object.assign(client, newClient); - } - } - - // 私有方法:创建带认证的HTTP客户端 - private static createAuthenticatedHttp() { - const token = AuthManager.getToken(); - if (!token) { - return null; - } - - return { - fetch: (url: RequestInfo, init?: RequestInit) => { - if (!init) init = {}; - if (!init.headers) init.headers = {}; - - if (typeof init.headers === "object" && init.headers !== null) { - (init.headers as any)["Authorization"] = `Bearer ${token}`; - } - - return (window as any).fetch(url, init); - }, - }; - } - - // 私有方法:创建带认证的Axios实例 - private static createAuthenticatedAxiosInstance(): AxiosInstance | null { - const token = AuthManager.getToken(); - if (!token) return null; - - const instance = axios.create(); - instance.interceptors.request.use((config) => { - config.headers = config.headers || {}; - (config.headers as any)["Authorization"] = `Bearer ${token}`; - return config; - }); - return instance; - } - - // 通用的创建已认证客户端的方法(使用泛型) - public static createAuthenticatedClient( - ClientClass: new (baseUrl?: string, instance?: AxiosInstance) => T, + // 一个方法搞定所有客户端,不要17个垃圾方法 + static createClient( + ClientClass: new (baseUrl?: string, config?: any) => T, + baseUrl?: string, ): T { - const axiosInstance = AuthManager.createAuthenticatedAxiosInstance(); - return axiosInstance - ? new ClientClass(undefined, axiosInstance) - : new ClientClass(); + const token = this.getToken(); + if (!token) { + return new ClientClass(baseUrl); + } + + // 对于axios客户端 + const axiosInstance = axios.create({ + headers: this.getAuthHeaders(), + }); + + return new ClientClass(baseUrl, axiosInstance); } - // 便捷方法:创建已配置认证的各种客户端 - public static createAuthenticatedDataClient(): DataClient { - return AuthManager.createAuthenticatedClient(DataClient); - } - - public static createAuthenticatedVideoStreamClient(): VideoStreamClient { - return AuthManager.createAuthenticatedClient(VideoStreamClient); - } - - public static createAuthenticatedBsdlParserClient(): BsdlParserClient { - return AuthManager.createAuthenticatedClient(BsdlParserClient); - } - - public static createAuthenticatedDDSClient(): DDSClient { - return AuthManager.createAuthenticatedClient(DDSClient); - } - - public static createAuthenticatedJtagClient(): JtagClient { - return AuthManager.createAuthenticatedClient(JtagClient); - } - - public static createAuthenticatedMatrixKeyClient(): MatrixKeyClient { - return AuthManager.createAuthenticatedClient(MatrixKeyClient); - } - - public static createAuthenticatedPowerClient(): PowerClient { - return AuthManager.createAuthenticatedClient(PowerClient); - } - - public static createAuthenticatedRemoteUpdateClient(): RemoteUpdateClient { - return AuthManager.createAuthenticatedClient(RemoteUpdateClient); - } - - public static createAuthenticatedTutorialClient(): TutorialClient { - return AuthManager.createAuthenticatedClient(TutorialClient); - } - - public static createAuthenticatedUDPClient(): UDPClient { - return AuthManager.createAuthenticatedClient(UDPClient); - } - - public static createAuthenticatedLogicAnalyzerClient(): LogicAnalyzerClient { - return AuthManager.createAuthenticatedClient(LogicAnalyzerClient); - } - - public static createAuthenticatedNetConfigClient(): NetConfigClient { - return AuthManager.createAuthenticatedClient(NetConfigClient); - } - - public static createAuthenticatedOscilloscopeApiClient(): OscilloscopeApiClient { - return AuthManager.createAuthenticatedClient(OscilloscopeApiClient); - } - - public static createAuthenticatedDebuggerClient(): DebuggerClient { - return AuthManager.createAuthenticatedClient(DebuggerClient); - } - - public static createAuthenticatedExamClient(): ExamClient { - return AuthManager.createAuthenticatedClient(ExamClient); - } - - public static createAuthenticatedResourceClient(): ResourceClient { - return AuthManager.createAuthenticatedClient(ResourceClient); - } - - public static createAuthenticatedHdmiVideoStreamClient(): HdmiVideoStreamClient { - return AuthManager.createAuthenticatedClient(HdmiVideoStreamClient); - } - - public static createAuthenticatedJtagHubConnection() { + // SignalR连接 - 简单明了 + static createHubConnection( + hubPath: "ProgressHub" | "JtagHub" | "DigitalTubesHub", + ) { return new HubConnectionBuilder() - .withUrl("http://127.0.0.1:5000/hubs/JtagHub", { + .withUrl(`http://127.0.0.1:5000/hubs/${hubPath}`, { accessTokenFactory: () => this.getToken() ?? "", }) .withAutomaticReconnect() .build(); } - public static createAuthenticatedProgressHubConnection() { - return new HubConnectionBuilder() - .withUrl("http://127.0.0.1:5000/hubs/ProgressHub", { - accessTokenFactory: () => this.getToken() ?? "", - }) - .withAutomaticReconnect() - .build(); - } - - public static createAuthenticatedDigitalTubesHubConnection() { - return new HubConnectionBuilder() - .withUrl("http://127.0.0.1:5000/hubs/DigitalTubesHub", { - accessTokenFactory: () => this.getToken() ?? "", - }) - .withAutomaticReconnect() - .build(); - } - - // 登录函数 - public static async login( - username: string, - password: string, - ): Promise { + // 认证逻辑 - 去除所有废话 + static async login(username: string, password: string): Promise { try { const client = new DataClient(); const token = await client.login(username, password); - if (token) { - AuthManager.setToken(token); + if (!token) return false; - // 验证token - const authClient = AuthManager.createAuthenticatedDataClient(); - await authClient.testAuth(); + this.setToken(token); - return true; - } - return false; - } catch (error) { - AuthManager.clearToken(); - throw error; - } - } - - // 登出函数 - public static logout(): void { - AuthManager.clearToken(); - } - - // 验证当前token是否有效 - public static async verifyToken(): Promise { - try { - const token = AuthManager.getToken(); - if (!token) { - return false; - } - - const client = AuthManager.createAuthenticatedDataClient(); - await client.testAuth(); + // 验证token - 如果失败直接抛异常 + await this.createClient(DataClient).testAuth(); return true; - } catch (error) { - AuthManager.clearToken(); - return false; + } catch { + this.clearToken(); + throw new Error("Login failed"); } } - // 验证管理员权限 - public static async verifyAdminAuth(): Promise { + static logout(): void { + this.clearToken(); + } + + // 简单的验证 - 不要搞复杂 + static async isAuthenticated(): Promise { + if (!this.getToken()) return false; + try { - const token = AuthManager.getToken(); - if (!token) { - return false; - } - - const client = AuthManager.createAuthenticatedDataClient(); - await client.testAdminAuth(); + await this.createClient(DataClient).testAuth(); return true; - } catch (error) { - // 只有在token完全无效的情况下才清除token - // 401错误表示token有效但权限不足,不应清除token - if (error && typeof error === "object" && "status" in error) { - // 如果是403 (Forbidden) 或401 (Unauthorized),说明token有效但权限不足 - if (error.status === 401 || error.status === 403) { - return false; - } - // 其他状态码可能表示token无效,清除token - AuthManager.clearToken(); - } else { - // 网络错误等,不清除token - console.error("管理员权限验证失败:", error); - } + } catch { + this.clearToken(); return false; } } - // 检查客户端是否已配置认证 - public static isClientAuthenticated(client: SupportedClient): boolean { - const token = AuthManager.getToken(); - return !!token; + static async isAdminAuthenticated(): Promise { + if (!this.getToken()) return false; + + try { + await this.createClient(DataClient).testAdminAuth(); + return true; + } catch { + this.clearToken(); + return false; + } } } diff --git a/src/utils/BoardManager.ts b/src/utils/BoardManager.ts index 755a853..bb2b91a 100644 --- a/src/utils/BoardManager.ts +++ b/src/utils/BoardManager.ts @@ -17,7 +17,7 @@ export interface BoardData extends Board { const [useProvideBoardManager, useBoardManager] = createInjectionState(() => { // 远程升级相关参数 const devPort = 1234; - const remoteUpdater = AuthManager.createAuthenticatedRemoteUpdateClient(); + const remoteUpdater = AuthManager.createClient(RemoteUpdateClient); // 统一的板卡数据 const boards = ref([]); @@ -35,13 +35,13 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => { async function getAllBoards(): Promise<{ success: boolean; error?: string }> { try { // 验证管理员权限 - const hasAdminAuth = await AuthManager.verifyAdminAuth(); + const hasAdminAuth = await AuthManager.isAdminAuthenticated(); if (!hasAdminAuth) { console.error("权限验证失败"); return { success: false, error: "权限不足" }; } - const client = AuthManager.createAuthenticatedDataClient(); + const client = AuthManager.createClient(DataClient); const result = await client.getAllBoards(); if (result) { @@ -77,7 +77,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => { ): Promise<{ success: boolean; error?: string; boardId?: string }> { try { // 验证管理员权限 - const hasAdminAuth = await AuthManager.verifyAdminAuth(); + const hasAdminAuth = await AuthManager.isAdminAuthenticated(); if (!hasAdminAuth) { console.error("权限验证失败"); return { success: false, error: "权限不足" }; @@ -89,11 +89,11 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => { return { success: false, error: "参数不完整" }; } - const client = AuthManager.createAuthenticatedDataClient(); + const client = AuthManager.createClient(DataClient); const boardId = await client.addBoard(name); if (boardId) { - console.log("新增板卡成功", { boardId, name}); + console.log("新增板卡成功", { boardId, name }); // 刷新板卡列表 await getAllBoards(); return { success: true }; @@ -119,7 +119,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => { ): Promise<{ success: boolean; error?: string }> { try { // 验证管理员权限 - const hasAdminAuth = await AuthManager.verifyAdminAuth(); + const hasAdminAuth = await AuthManager.isAdminAuthenticated(); if (!hasAdminAuth) { console.error("权限验证失败"); return { success: false, error: "权限不足" }; @@ -130,7 +130,7 @@ const [useProvideBoardManager, useBoardManager] = createInjectionState(() => { return { success: false, error: "板卡ID不能为空" }; } - const client = AuthManager.createAuthenticatedDataClient(); + const client = AuthManager.createClient(DataClient); const result = await client.deleteBoard(boardId); if (result > 0) { diff --git a/src/views/AuthView.vue b/src/views/AuthView.vue index 0c64764..12071e9 100644 --- a/src/views/AuthView.vue +++ b/src/views/AuthView.vue @@ -274,7 +274,7 @@ const handleSignUp = async () => { // 页面初始化时检查是否已有有效token const checkExistingToken = async () => { try { - const isValid = await AuthManager.verifyToken(); + const isValid = await AuthManager.isAuthenticated(); if (isValid) { // 如果token仍然有效,直接跳转到project页面 router.go(-1); diff --git a/src/views/Exam/ExamEditModal.vue b/src/views/Exam/ExamEditModal.vue index 2501c79..f15a95d 100644 --- a/src/views/Exam/ExamEditModal.vue +++ b/src/views/Exam/ExamEditModal.vue @@ -418,7 +418,12 @@ import { FileArchiveIcon, FileJsonIcon, } from "lucide-vue-next"; -import { ExamDto, type FileParameter } from "@/APIClient"; +import { + ExamClient, + ExamDto, + ResourceClient, + type FileParameter, +} from "@/APIClient"; import { useAlertStore } from "@/components/Alert"; import { AuthManager } from "@/utils/AuthManager"; import { useRequiredInjection } from "@/utils/Common"; @@ -618,7 +623,7 @@ const submitCreateExam = async () => { isUpdating.value = true; try { - const client = AuthManager.createAuthenticatedExamClient(); + const client = AuthManager.createClient(ExamClient); let exam: ExamInfo; if (mode.value === "create") { @@ -671,7 +676,7 @@ const submitCreateExam = async () => { // 上传实验资源 async function uploadExamResources(examId: string) { - const client = AuthManager.createAuthenticatedResourceClient(); + const client = AuthManager.createClient(ResourceClient); try { // 上传MD文档 @@ -750,7 +755,7 @@ function close() { } async function editExam(examId: string) { - const client = AuthManager.createAuthenticatedExamClient(); + const client = AuthManager.createClient(ExamClient); const examInfo = await client.getExam(examId); editExamInfo.value = { diff --git a/src/views/Exam/ExamInfoModal.vue b/src/views/Exam/ExamInfoModal.vue index 42cd2e8..2fd417a 100644 --- a/src/views/Exam/ExamInfoModal.vue +++ b/src/views/Exam/ExamInfoModal.vue @@ -250,7 +250,13 @@ - - + + + + + diff --git a/src/views/Project/Index.vue b/src/views/Project/Index.vue index f03b087..92fab74 100644 --- a/src/views/Project/Index.vue +++ b/src/views/Project/Index.vue @@ -8,7 +8,7 @@ @layout="handleVerticalSplitterResize" > - - @@ -80,11 +80,13 @@ - @@ -106,22 +108,48 @@ /> - @@ -131,7 +159,7 @@ + + From 97b86acfa84bc4aaa426b77af0dc3341a2018fc0 Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sun, 17 Aug 2025 12:29:46 +0800 Subject: [PATCH 07/10] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=8B=A8?= =?UTF-8?q?=E7=A0=81=E5=BC=80=E5=85=B3=E6=95=B0=E5=AD=97=E5=AD=AA=E7=94=9F?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=AD=A3=E5=B8=B8=E5=B7=A5=E4=BD=9C=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/Controllers/SwitchController.cs | 2 +- src/components/equipments/Switch.vue | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/server/src/Controllers/SwitchController.cs b/server/src/Controllers/SwitchController.cs index 76b98ef..b29498e 100644 --- a/server/src/Controllers/SwitchController.cs +++ b/server/src/Controllers/SwitchController.cs @@ -114,7 +114,7 @@ public class SwitchController : ControllerBase for (int i = 0; i < keyStatus.Length; i++) { - var result = await switchCtrl.SetSwitchOnOff(i, keyStatus[i]); + var result = await switchCtrl.SetSwitchOnOff(i + 1, keyStatus[i]); if (!result.IsSuccessful) { logger.Error(result.Error, $"SetSwitchOnOff({i}, {keyStatus[i]}) failed"); diff --git a/src/components/equipments/Switch.vue b/src/components/equipments/Switch.vue index d8108f4..f136adc 100644 --- a/src/components/equipments/Switch.vue +++ b/src/components/equipments/Switch.vue @@ -127,6 +127,10 @@ const switchCount = computed(() => { else return props.switchCount; }); +function getClient() { + return AuthManager.createClient(SwitchClient); +} + // 解析初始值 function parseInitialValues(): boolean[] { if (Array.isArray(props.initialValues)) { @@ -162,7 +166,7 @@ function updateStatus(newStates: boolean[], index?: number) { btnStatus.value = newStates.slice(0, switchCount.value); if (props.enableDigitalTwin) { try { - const client = AuthManager.createClient(SwitchClient); + const client = getClient(); if (!isUndefined(index)) client.setSwitchOnOff(index, newStates[index]); else client.setMultiSwitchsOnOff(btnStatus.value); } catch (error: any) {} @@ -186,6 +190,15 @@ function setBtnStatus(idx: number, isOn: boolean) { } // 监听 props 变化只同步一次 +watch( + () => props.enableDigitalTwin, + (newVal) => { + const client = getClient(); + client.setEnable(newVal); + }, + { immediate: true }, +); + watch( () => [switchCount.value, props.initialValues], () => { From 55edfd771ec9a2323326fdb66d3579daa8405f9c Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sun, 17 Aug 2025 13:33:11 +0800 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=9D=A1=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/Program.cs | 5 +- server/src/Controllers/JtagController.cs | 21 +- server/src/Hubs/ProgressHub.cs | 50 ++- server/src/MsgBus.cs | 30 +- server/src/Peripherals/JtagClient.cs | 62 ++-- server/src/Services/ProgressTracker.cs | 147 ++++++++ server/src/Services/ProgressTrackerService.cs | 294 ---------------- server/src/UdpClientPool.cs | 60 +--- src/components/UploadCard.vue | 317 ++++++++---------- src/stores/progress.ts | 83 +++++ .../signalR/TypedSignalR.Client/index.ts | 8 + .../TypedSignalR.Client/server.Hubs.ts | 10 + src/utils/signalR/server.Hubs.ts | 9 +- 13 files changed, 512 insertions(+), 584 deletions(-) create mode 100644 server/src/Services/ProgressTracker.cs delete mode 100644 server/src/Services/ProgressTrackerService.cs create mode 100644 src/stores/progress.ts diff --git a/server/Program.cs b/server/Program.cs index 211c2b3..682401e 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -180,8 +180,7 @@ try builder.Services.AddHostedService(provider => provider.GetRequiredService()); // 添加进度跟踪服务 - builder.Services.AddSingleton(); - builder.Services.AddHostedService(provider => provider.GetRequiredService()); + builder.Services.AddSingleton(); // Application Settings var app = builder.Build(); @@ -258,6 +257,8 @@ try // Setup Program MsgBus.Init(); + var progressTracker = app.Services.GetRequiredService(); + MsgBus.SetProgressTracker(progressTracker); // Generate API Client app.MapGet("GetAPIClientCode", async (HttpContext context) => diff --git a/server/src/Controllers/JtagController.cs b/server/src/Controllers/JtagController.cs index c8e071e..717f366 100644 --- a/server/src/Controllers/JtagController.cs +++ b/server/src/Controllers/JtagController.cs @@ -16,17 +16,12 @@ public class JtagController : ControllerBase { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - private readonly ProgressTrackerService _tracker; + private readonly ProgressTracker _tracker = MsgBus.ProgressTracker; private readonly UserManager _userManager = new(); private readonly ResourceManager _resourceManager = new(); private const string BITSTREAM_PATH = "bitstream/Jtag"; - public JtagController(ProgressTrackerService tracker) - { - _tracker = tracker; - } - /// /// 控制器首页信息 /// @@ -188,8 +183,8 @@ public class JtagController : ControllerBase logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes"); // 定义进度跟踪 - var (taskId, progress) = _tracker.CreateTask(cancelToken); - progress.Report(10); + var taskId = _tracker.CreateTask(1000); + _tracker.AdvanceProgress(taskId, 10); _ = Task.Run(async () => { @@ -210,7 +205,8 @@ public class JtagController : ControllerBase if (!retBuffer.IsSuccessful) { logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}"); - progress.Error($"User {username} failed to reverse bytes: {retBuffer.Error}"); + _tracker.FailProgress(taskId, + $"User {username} failed to reverse bytes: {retBuffer.Error}"); return; } revBuffer = retBuffer.Value; @@ -228,7 +224,7 @@ public class JtagController : ControllerBase var processedBytes = outputStream.ToArray(); logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}"); - progress.Report(20); + _tracker.AdvanceProgress(taskId, 20); // 下载比特流 var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); @@ -237,12 +233,13 @@ public class JtagController : ControllerBase if (ret.IsSuccessful) { logger.Info($"User {username} successfully downloaded bitstream '{resource.ResourceName}' to device {address}"); - progress.Finish(); + _tracker.CompleteProgress(taskId); } else { logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}"); - progress.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}"); + _tracker.FailProgress(taskId, + $"User {username} failed to download bitstream to device {address}: {ret.Error}"); } } }); diff --git a/server/src/Hubs/ProgressHub.cs b/server/src/Hubs/ProgressHub.cs index 068b919..3f153d1 100644 --- a/server/src/Hubs/ProgressHub.cs +++ b/server/src/Hubs/ProgressHub.cs @@ -1,17 +1,20 @@ using Microsoft.AspNetCore.Authorization; -using System.Security.Claims; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.Cors; using TypedSignalR.Client; using Tapper; using server.Services; +#pragma warning disable 1998 + namespace server.Hubs; [Hub] public interface IProgressHub { Task Join(string taskId); + Task Leave(string taskId); + Task GetProgress(string taskId); } [Receiver] @@ -23,8 +26,7 @@ public interface IProgressReceiver [TranspilationSource] public enum ProgressStatus { - Pending, - InProgress, + Running, Completed, Canceled, Failed @@ -33,10 +35,10 @@ public enum ProgressStatus [TranspilationSource] public class ProgressInfo { - public virtual string TaskId { get; } = string.Empty; - public virtual ProgressStatus Status { get; } - public virtual int ProgressPercent { get; } = 0; - public virtual string ErrorMessage { get; } = string.Empty; + public required string TaskId { get; set; } + public required ProgressStatus Status { get; set; } + public required int ProgressPercent { get; set; } + public required string ErrorMessage { get; set; } }; [Authorize] @@ -44,18 +46,32 @@ public class ProgressInfo public class ProgressHub : Hub, IProgressHub { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - - private readonly IHubContext _hubContext; - private readonly ProgressTrackerService _tracker; - - public ProgressHub(IHubContext hubContext, ProgressTrackerService tracker) - { - _hubContext = hubContext; - _tracker = tracker; - } + private readonly ProgressTracker _progressTracker = MsgBus.ProgressTracker; public async Task Join(string taskId) { - return await Task.Run(() => _tracker.BindTask(taskId, Context.ConnectionId)); + await Groups.AddToGroupAsync(Context.ConnectionId, taskId); + + // 发送当前状态(如果存在) + var task = _progressTracker.GetTask(taskId); + if (task != null) + { + await Clients.Caller.OnReceiveProgress(task.Value.ToProgressInfo()); + } + + logger.Info($"Client {Context.ConnectionId} joined task {taskId}"); + return true; + } + + public async Task Leave(string taskId) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, taskId); + logger.Info($"Client {Context.ConnectionId} left task {taskId}"); + return true; + } + + public async Task GetProgress(string taskId) + { + return _progressTracker.GetTask(taskId)?.ToProgressInfo(); } } diff --git a/server/src/MsgBus.cs b/server/src/MsgBus.cs index 5a93abc..18527ab 100644 --- a/server/src/MsgBus.cs +++ b/server/src/MsgBus.cs @@ -1,7 +1,8 @@ +using server.Services; /// /// 多线程通信总线 /// -public static class MsgBus +public sealed class MsgBus { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); @@ -11,12 +12,39 @@ public static class MsgBus /// public static UDPServer UDPServer { get { return udpServer; } } + // 添加静态ProgressTracker引用 + private static ProgressTracker? _progressTracker; + + /// + /// 设置全局ProgressTracker实例 + /// + public static void SetProgressTracker(ProgressTracker progressTracker) + { + _progressTracker = progressTracker; + } + + public static ProgressTracker ProgressTracker + { + get + { + if (_progressTracker == null) + { + throw new InvalidOperationException("ProgressTracker is not set."); + } + return _progressTracker; + } + } + private static bool isRunning = false; /// /// 获取通信总线运行状态 /// public static bool IsRunning { get { return isRunning; } } + private MsgBus() { } + + static MsgBus() { } + /// /// 通信总线初始化 /// diff --git a/server/src/Peripherals/JtagClient.cs b/server/src/Peripherals/JtagClient.cs index 11f43fd..0fcafac 100644 --- a/server/src/Peripherals/JtagClient.cs +++ b/server/src/Peripherals/JtagClient.cs @@ -380,6 +380,7 @@ public class JtagStatusReg public class Jtag { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private readonly ProgressTracker? _progressTracker; private const int CLOCK_FREQ = 50; // MHz @@ -392,18 +393,21 @@ public class Jtag public readonly string address; private IPEndPoint ep; + /// /// Jtag 构造函数 /// /// 目标 IP 地址 /// 目标 UDP 端口 /// 超时时间(毫秒) - public Jtag(string address, int port, int timeout = 2000) + /// 进度追踪器 + public Jtag(string address, int port, int timeout = 2000, ProgressTracker? progressTracker = null) { this.address = address; this.port = port; this.ep = new IPEndPoint(IPAddress.Parse(address), port); this.timeout = timeout; + this._progressTracker = progressTracker; } async ValueTask> ReadFIFO(uint devAddr) @@ -444,10 +448,10 @@ public class Jtag async ValueTask> WriteFIFO( UInt32 devAddr, UInt32 data, UInt32 result, - UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, ProgressReporter? progress = null) + UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, string progressId = "") { { - var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progress?.CreateChild(80)); + var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progressId); if (!ret.IsSuccessful) return new(ret.Error); if (!ret.Value) return new(new Exception("Write FIFO failed")); } @@ -458,17 +462,17 @@ public class Jtag { var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout); if (!ret.IsSuccessful) return new(ret.Error); - progress?.Finish(); + _progressTracker?.AdvanceProgress(progressId, 10); return ret.Value; } } async ValueTask> WriteFIFO( UInt32 devAddr, byte[] data, UInt32 result, - UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, ProgressReporter? progress = null) + UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, string progressId = "") { { - var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progress?.CreateChild(80)); + var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progressId); if (!ret.IsSuccessful) return new(ret.Error); if (!ret.Value) return new(new Exception("Write FIFO failed")); } @@ -479,7 +483,7 @@ public class Jtag { var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout); if (!ret.IsSuccessful) return new(ret.Error); - progress?.Finish(); + _progressTracker?.AdvanceProgress(progressId, 10); return ret.Value; } } @@ -564,7 +568,7 @@ public class Jtag } async ValueTask> LoadDRCareInput( - byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500, ProgressReporter? progress = null) + byte[] bytesArray, UInt32 timeout = 10_000, UInt32 cycle = 500, string progressId = "") { var bytesLen = ((uint)(bytesArray.Length * 8)); if (bytesLen > Math.Pow(2, 28)) return new(new Exception("Length is over 2^(28 - 3)")); @@ -579,14 +583,15 @@ public class Jtag else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed")); } - progress?.Report(10); + _progressTracker?.AdvanceProgress(progressId, 10); { var ret = await WriteFIFO( JtagAddr.WRITE_DATA, bytesArray, 0x01_00_00_00, JtagState.CMD_EXEC_FINISH, - progress: progress?.CreateChild(90) + 0, + progressId ); if (!ret.IsSuccessful) return new(ret.Error); @@ -709,58 +714,53 @@ public class Jtag /// 下载比特流到 JTAG 设备 /// /// 比特流数据 - /// 进度报告器 + /// 进度ID /// 指示下载是否成功的异步结果 public async ValueTask> DownloadBitstream( - byte[] bitstream, ProgressReporter? progress = null) + byte[] bitstream, string progressId = "") { // Clear Data MsgBus.UDPServer.ClearUDPData(this.address, 0); logger.Trace($"Clear up udp server {this.address,0} receive data"); - if (progress != null) - { - progress.ExpectedSteps = 25; - progress.Increase(); - } Result ret; ret = await CloseTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Close Test Failed")); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); ret = await RunTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); 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")); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); ret = await RunTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); 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")); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); logger.Trace("Jtag ready to write bitstream"); ret = await IdleDelay(100000); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed")); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); - ret = await LoadDRCareInput(bitstream, progress: progress?.CreateChild(50)); + ret = await LoadDRCareInput(bitstream, progressId: progressId); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Load Data Failed")); @@ -769,40 +769,40 @@ public class Jtag ret = await CloseTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Close Test Failed")); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); ret = await RunTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); 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")); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); logger.Trace("Jtag reset device"); ret = await IdleDelay(10000); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed")); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); var retCode = await ReadStatusReg(); if (!retCode.IsSuccessful) return new(retCode.Error); var jtagStatus = new JtagStatusReg(retCode.Value); if (!(jtagStatus.done && jtagStatus.wakeup_over && jtagStatus.init_complete)) return new(new Exception("Jtag download bitstream failed")); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); 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"); - progress?.Increase(); + _progressTracker?.AdvanceProgress(progressId, 10); // Finish - progress?.Finish(); + _progressTracker?.AdvanceProgress(progressId, 10); return true; } diff --git a/server/src/Services/ProgressTracker.cs b/server/src/Services/ProgressTracker.cs new file mode 100644 index 0000000..27f34dc --- /dev/null +++ b/server/src/Services/ProgressTracker.cs @@ -0,0 +1,147 @@ +using System.Collections.Concurrent; +using Microsoft.AspNetCore.SignalR; +using server.Hubs; + +namespace server.Services; + +public enum TaskState { Running, Completed, Failed, Cancelled } + +public readonly struct TaskProgress +{ + public string Id { get; } + public int Current { get; } + public int Total { get; } + public TaskState State { get; } + public long Timestamp { get; } + public string? Error { get; } + + public TaskProgress(string id, int current, int total, TaskState state, long timestamp, string? error = null) + { + Id = id; + Current = current; + Total = total; + State = state; + Timestamp = timestamp; + Error = error; + } + + public TaskProgress WithUpdate(int? current = null, TaskState? state = null, string? error = null) + { + return new TaskProgress( + Id, + current ?? Current, + Total, + state ?? State, + DateTimeOffset.UtcNow.ToUnixTimeSeconds(), + error ?? Error + ); + } + + public ProgressInfo ToProgressInfo() + { + return new ProgressInfo + { + TaskId = Id, + Status = State switch + { + TaskState.Running => ProgressStatus.Running, + TaskState.Completed => ProgressStatus.Completed, + TaskState.Failed => ProgressStatus.Failed, + TaskState.Cancelled => ProgressStatus.Canceled, + _ => ProgressStatus.Failed + }, + ProgressPercent = Total > 0 ? (Current * 100) / Total : 0, + ErrorMessage = Error ?? string.Empty + }; + } +} + +public sealed class ProgressTracker +{ + private readonly ConcurrentDictionary _tasks = new(); + private readonly Timer _cleaner; + private readonly IHubContext _hubContext; + + // 构造器支持可选的Hub注入 + public ProgressTracker(IHubContext hubContext) + { + _hubContext = hubContext; + _cleaner = new Timer(CleanExpiredTasks, null, + TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + } + + public void CleanExpiredTasks(object? obj) + { + var cutoff = DateTimeOffset.Now.AddMinutes(-3).ToUnixTimeSeconds(); + var expired = _tasks.Where(kvp => kvp.Value.Timestamp < cutoff).Select(kvp => kvp.Key).ToList(); + foreach (var id in expired) + { + _tasks.TryRemove(id, out _); + } + } + + public string CreateTask(int total) + { + var id = Guid.NewGuid().ToString(); + var task = new TaskProgress(id, 0, total, TaskState.Running, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); + _tasks[id] = task; + NotifyIfNeeded(task); + return id; + } + + // 核心更新方法,现在包含自动通知 + public bool UpdateTask(string id, Func updater) + { + if (!_tasks.TryGetValue(id, out var current)) + return false; + + var updated = updater(current); + if (_tasks.TryUpdate(id, updated, current)) + { + NotifyIfNeeded(updated); + return true; + } + return false; + } + + // 自动通知逻辑 - 简单直接 + private void NotifyIfNeeded(TaskProgress task) + { + _hubContext.Clients.Group(task.Id).OnReceiveProgress(task.ToProgressInfo()); + } + + public bool UpdateProgress(string id, int current) + { + return UpdateTask(id, p => p.WithUpdate( + current: Math.Min(current, p.Total))); + } + + public bool AdvanceProgress(string id, int steps) + { + return UpdateTask(id, p => p.WithUpdate( + current: Math.Min(p.Current + steps, p.Total))); + } + + public bool CancelProgress(string id) + { + return UpdateTask(id, p => p.WithUpdate(state: TaskState.Cancelled)); + } + + public bool CompleteProgress(string id) + { + return UpdateTask(id, p => p.WithUpdate( + current: p.Total, state: TaskState.Completed)); + } + + public bool FailProgress(string id, string? error) + { + return UpdateTask(id, p => p.WithUpdate( + state: TaskState.Failed, error: error)); + } + + public TaskProgress? GetTask(string id) + { + _tasks.TryGetValue(id, out var task); + return task.Id == null ? null : task; + } +} diff --git a/server/src/Services/ProgressTrackerService.cs b/server/src/Services/ProgressTrackerService.cs deleted file mode 100644 index 420dfc8..0000000 --- a/server/src/Services/ProgressTrackerService.cs +++ /dev/null @@ -1,294 +0,0 @@ -using Microsoft.AspNetCore.SignalR; -using System.Collections.Concurrent; -using DotNext; -using Common; -using server.Hubs; - -namespace server.Services; - -public class ProgressReporter : ProgressInfo, IProgress -{ - private int _progress = 0; - private int _stepProgress = 1; - private int _expectedSteps = 100; - private int _parentProportion = 100; - - public int Progress => _progress; - public int MaxProgress { get; set; } = 100; - public int StepProgress - { - get => _stepProgress; - set - { - _stepProgress = value; - _expectedSteps = MaxProgress / value; - } - } - public int ExpectedSteps - { - get => _expectedSteps; - set - { - _expectedSteps = value; - MaxProgress = Number.IntPow(10, Number.GetLength(value)); - _stepProgress = MaxProgress / value; - } - } - public Func? ReporterFunc { get; set; } = null; - public ProgressReporter? Parent { get; set; } - public ProgressReporter? Child { get; set; } - - private ProgressStatus _status = ProgressStatus.Pending; - private string _errorMessage = string.Empty; - - public override string TaskId { get; } = Guid.NewGuid().ToString(); - public override int ProgressPercent => _progress * 100 / MaxProgress; - public override ProgressStatus Status => _status; - public override string ErrorMessage => _errorMessage; - - public ProgressReporter(Func? reporter = null, int initProgress = 0, int maxProgress = 100, int step = 1) - { - _progress = initProgress; - MaxProgress = maxProgress; - StepProgress = step; - ReporterFunc = reporter; - } - - public ProgressReporter(int parentProportion, int expectedSteps = 100, Func? reporter = null) - { - this._parentProportion = parentProportion; - MaxProgress = Number.IntPow(10, Number.GetLength(expectedSteps)); - StepProgress = MaxProgress / expectedSteps; - ReporterFunc = reporter; - } - - private async void ForceReport(int value) - { - try - { - if (ReporterFunc != null) - await ReporterFunc(value); - - if (Parent != null) - Parent.Increase((value - _progress) / StepProgress * _parentProportion / (MaxProgress / StepProgress)); - - _progress = value; - } - catch (OperationCanceledException ex) - { - _errorMessage = ex.Message; - this._status = ProgressStatus.Canceled; - } - catch (Exception ex) - { - _errorMessage = ex.Message; - this._status = ProgressStatus.Failed; - } - } - - public void Report(int value) - { - if (this._status == ProgressStatus.Pending) - this._status = ProgressStatus.InProgress; - else if (this.Status != ProgressStatus.InProgress) - return; - - if (value > MaxProgress) return; - ForceReport(value); - } - - public void Increase(int? value = null) - { - if (this._status == ProgressStatus.Pending) - this._status = ProgressStatus.InProgress; - else if (this.Status != ProgressStatus.InProgress) - return; - - if (value.HasValue) - { - if (_progress + value.Value >= MaxProgress) return; - this.Report(_progress + value.Value); - } - else - { - if (_progress + StepProgress >= MaxProgress) return; - this.Report(_progress + StepProgress); - } - } - - public void Finish() - { - this._status = ProgressStatus.Completed; - this.ForceReport(MaxProgress); - } - - public void Cancel() - { - this._status = ProgressStatus.Canceled; - this._errorMessage = "User Cancelled"; - this.ForceReport(_progress); - } - - public void Error(string message) - { - this._status = ProgressStatus.Failed; - this._errorMessage = message; - this.ForceReport(_progress); - } - - public ProgressReporter CreateChild(int proportion, int expectedSteps = 100) - { - var child = new ProgressReporter(proportion, expectedSteps); - child.Parent = this; - this.Child = child; - return child; - } -} - -public class ProgressTrackerService : BackgroundService -{ - private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - private readonly ConcurrentDictionary _taskMap = new(); - private readonly IHubContext _hubContext; - - private class TaskProgressInfo - { - public ProgressReporter? Reporter { get; set; } - public string? ConnectionId { get; set; } - public required CancellationToken CancellationToken { get; set; } - public required CancellationTokenSource CancellationTokenSource { get; set; } - public required DateTime UpdatedAt { get; set; } - } - - public ProgressTrackerService(IHubContext hubContext) - { - _hubContext = hubContext; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - try - { - var now = DateTime.UtcNow; - foreach (var kvp in _taskMap) - { - var info = kvp.Value; - // 超过 1 分钟且任务已完成/失败/取消 - if ( - (now - info.UpdatedAt).TotalMinutes > 1 && - info.Reporter != null && - ( - info.Reporter.Status == ProgressStatus.Completed || - info.Reporter.Status == ProgressStatus.Failed || - info.Reporter.Status == ProgressStatus.Canceled - ) - ) - { - _taskMap.TryRemove(kvp.Key, out _); - logger.Info($"Cleaned up task {kvp.Key}"); - } - } - } - catch (Exception ex) - { - logger.Error(ex, "Error during ProgressTracker cleanup"); - } - await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); - } - } - - public (string, ProgressReporter) CreateTask(CancellationToken? cancellationToken = null) - { - CancellationTokenSource? cancellationTokenSource; - if (cancellationToken.HasValue) - { - cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken.Value); - } - else - { - cancellationTokenSource = new CancellationTokenSource(); - } - - var progressInfo = new TaskProgressInfo - { - ConnectionId = null, - UpdatedAt = DateTime.UtcNow, - CancellationToken = cancellationTokenSource.Token, - CancellationTokenSource = cancellationTokenSource, - }; - - var progress = new ProgressReporter(async value => - { - cancellationTokenSource.Token.ThrowIfCancellationRequested(); - - // 通过 SignalR 推送进度 - if (progressInfo.ConnectionId != null && progressInfo.Reporter != null) - await _hubContext.Clients.Client(progressInfo.ConnectionId).OnReceiveProgress(progressInfo.Reporter); - }); - - progressInfo.Reporter = progress; - - _taskMap.TryAdd(progressInfo.Reporter.TaskId, progressInfo); - - return (progressInfo.Reporter.TaskId, progress); - } - - public Optional GetReporter(string taskId) - { - if (_taskMap.TryGetValue(taskId, out var info)) - { - return info.Reporter; - } - return Optional.None; - } - - public Optional GetProgressStatus(string taskId) - { - if (_taskMap.TryGetValue(taskId, out var info)) - { - if (info.Reporter != null) - return info.Reporter.Status; - } - return Optional.None; - } - - public bool BindTask(string taskId, string connectionId) - { - if (_taskMap.TryGetValue(taskId, out var info) && info != null) - { - lock (info) - { - info.ConnectionId = connectionId; - } - return true; - } - return false; - } - - public bool CancelTask(string taskId) - { - try - { - if (_taskMap.TryGetValue(taskId, out var info) && info != null && info.Reporter != null) - { - lock (info) - { - info.CancellationTokenSource.Cancel(); - info.Reporter.Cancel(); - info.UpdatedAt = DateTime.UtcNow; - } - - return true; - } - - return false; - } - catch (Exception ex) - { - logger.Error(ex, $"Failed to cancel task {taskId}"); - return false; - } - } -} diff --git a/server/src/UdpClientPool.cs b/server/src/UdpClientPool.cs index 1797630..99a4ac1 100644 --- a/server/src/UdpClientPool.cs +++ b/server/src/UdpClientPool.cs @@ -8,11 +8,11 @@ using server.Services; /// /// UDP客户端发送池 /// -public class UDPClientPool +public sealed class UDPClientPool { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - private static IPAddress localhost = IPAddress.Parse("127.0.0.1"); + private static ProgressTracker _progressTracker = MsgBus.ProgressTracker; /// /// 发送字符串 @@ -183,40 +183,6 @@ public class UDPClientPool return await Task.Run(() => { return SendDataPack(endPoint, pkg); }); } - /// - /// 发送字符串到本地 - /// - /// 端口 - /// 字符串数组 - /// 是否成功 - public static bool SendStringLocalHost(int port, string[] stringArray) - { - return SendString(new IPEndPoint(localhost, port), stringArray); - } - - /// - /// 循环发送字符串到本地 - /// - /// 发送总次数 - /// 间隔时间 - /// 端口 - /// 字符串数组 - /// 是否成功 - public static bool CycleSendStringLocalHost(int times, int sleepMilliSeconds, int port, string[] stringArray) - { - var isSuccessful = true; - - while (times-- >= 0) - { - isSuccessful = SendStringLocalHost(port, stringArray); - if (!isSuccessful) break; - - Thread.Sleep(sleepMilliSeconds); - } - - return isSuccessful; - } - /// /// 读取设备地址数据 /// @@ -607,11 +573,11 @@ public class UDPClientPool /// 设备地址 /// 要写入的32位数据 /// 超时时间(毫秒) - /// 进度报告器 + /// 进度报告器 /// 写入结果,true表示写入成功 public static async ValueTask> WriteAddr( IPEndPoint endPoint, int taskID, UInt32 devAddr, - UInt32 data, int timeout = 1000, ProgressReporter? progress = null) + UInt32 data, int timeout = 1000, string progressId = "") { var ret = false; var opts = new SendAddrPackOptions() @@ -622,17 +588,18 @@ public class UDPClientPool Address = devAddr, IsWrite = true, }; - progress?.Report(20); + _progressTracker.AdvanceProgress(progressId, 10); // Write Register ret = await UDPClientPool.SendAddrPackAsync(endPoint, new SendAddrPackage(opts)); if (!ret) return new(new Exception("Send 1st address package failed!")); - progress?.Report(40); + _progressTracker.AdvanceProgress(progressId, 10); + // Send Data Package ret = await UDPClientPool.SendDataPackAsync(endPoint, new SendDataPackage(Common.Number.NumberToBytes(data, 4).Value)); if (!ret) return new(new Exception("Send data package failed!")); - progress?.Report(60); + _progressTracker.AdvanceProgress(progressId, 10); // Check Msg Bus if (!MsgBus.IsRunning) @@ -642,7 +609,7 @@ public class UDPClientPool var udpWriteAck = await MsgBus.UDPServer.WaitForAckAsync( endPoint.Address.ToString(), taskID, endPoint.Port, timeout); if (!udpWriteAck.IsSuccessful) return new(udpWriteAck.Error); - progress?.Finish(); + _progressTracker.AdvanceProgress(progressId, 10); return udpWriteAck.Value.IsSuccessful; } @@ -655,11 +622,11 @@ public class UDPClientPool /// 设备地址 /// 要写入的字节数组 /// 超时时间(毫秒) - /// 进度报告器 + /// 进度报告器 /// 写入结果,true表示写入成功 public static async ValueTask> WriteAddr( IPEndPoint endPoint, int taskID, UInt32 devAddr, - byte[] dataArray, int timeout = 1000, ProgressReporter? progress = null) + byte[] dataArray, int timeout = 1000, string progressId = "") { var ret = false; var opts = new SendAddrPackOptions() @@ -681,8 +648,6 @@ public class UDPClientPool var writeTimes = hasRest ? dataArray.Length / (max4BytesPerRead * (32 / 8)) + 1 : dataArray.Length / (max4BytesPerRead * (32 / 8)); - if (progress != null) - progress.ExpectedSteps = writeTimes; for (var i = 0; i < writeTimes; i++) { // Sperate Data Array @@ -712,10 +677,9 @@ public class UDPClientPool if (!udpWriteAck.Value.IsSuccessful) return false; - progress?.Increase(); + _progressTracker.AdvanceProgress(progressId, 10); } - progress?.Finish(); return true; } diff --git a/src/components/UploadCard.vue b/src/components/UploadCard.vue index aeeb32a..758e8e8 100644 --- a/src/components/UploadCard.vue +++ b/src/components/UploadCard.vue @@ -16,22 +16,32 @@ {{ bitstream.name }}
@@ -78,28 +92,19 @@ diff --git a/src/stores/progress.ts b/src/stores/progress.ts new file mode 100644 index 0000000..ef70dc7 --- /dev/null +++ b/src/stores/progress.ts @@ -0,0 +1,83 @@ +import type { HubConnection } from "@microsoft/signalr"; +import type { + IProgressHub, + IProgressReceiver, +} from "@/utils/signalR/TypedSignalR.Client/server.Hubs"; +import { + getHubProxyFactory, + getReceiverRegister, +} from "@/utils/signalR/TypedSignalR.Client"; +import { ProgressStatus, type ProgressInfo } from "@/utils/signalR/server.Hubs"; +import { onMounted, onUnmounted, ref, shallowRef } from "vue"; +import { defineStore } from "pinia"; +import { AuthManager } from "@/utils/AuthManager"; +import { forEach, isUndefined } from "lodash"; + +export type ProgressCallback = (msg: ProgressInfo) => void; + +export const useProgressStore = defineStore("progress", () => { + // taskId -> name -> callback + const progressCallbackFuncs = shallowRef< + Map> + >(new Map()); + + const progressHubConnection = shallowRef(); + const progressHubProxy = shallowRef(); + const progressHubReceiver: IProgressReceiver = { + onReceiveProgress: async (msg) => { + const taskMap = progressCallbackFuncs.value.get(msg.taskId); + if (taskMap) { + for (const func of taskMap.values()) { + func(msg); + } + } + }, + }; + + onMounted(async () => { + progressHubConnection.value = + AuthManager.createHubConnection("ProgressHub"); + progressHubProxy.value = getHubProxyFactory("IProgressHub").createHubProxy( + progressHubConnection.value, + ); + getReceiverRegister("IProgressReceiver").register( + progressHubConnection.value, + progressHubReceiver, + ); + progressHubConnection.value.start(); + }); + + onUnmounted(() => { + if (progressHubConnection.value) { + progressHubConnection.value.stop(); + progressHubConnection.value = undefined; + progressHubProxy.value = undefined; + } + }); + + function register(progressId: string, name: string, func: ProgressCallback) { + progressHubProxy.value?.join(progressId); + let taskMap = progressCallbackFuncs.value.get(progressId); + if (!taskMap) { + taskMap = new Map(); + progressCallbackFuncs.value?.set(progressId, taskMap); + } + taskMap.set(name, func); + } + + function unregister(taskId: string, name: string) { + progressHubProxy.value?.leave(taskId); + const taskMap = progressCallbackFuncs.value.get(taskId); + if (taskMap) { + taskMap.delete(name); + if (taskMap.size === 0) { + progressCallbackFuncs.value?.delete(taskId); + } + } + } + + return { + register, + unregister, + }; +}); diff --git a/src/utils/signalR/TypedSignalR.Client/index.ts b/src/utils/signalR/TypedSignalR.Client/index.ts index cbe2a39..55e0eac 100644 --- a/src/utils/signalR/TypedSignalR.Client/index.ts +++ b/src/utils/signalR/TypedSignalR.Client/index.ts @@ -161,6 +161,14 @@ class IProgressHub_HubProxy implements IProgressHub { public readonly join = async (taskId: string): Promise => { return await this.connection.invoke("Join", taskId); } + + public readonly leave = async (taskId: string): Promise => { + return await this.connection.invoke("Leave", taskId); + } + + public readonly getProgress = async (taskId: string): Promise => { + return await this.connection.invoke("GetProgress", taskId); + } } diff --git a/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts b/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts index c4966dc..3b17ecd 100644 --- a/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts +++ b/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts @@ -48,6 +48,16 @@ export type IProgressHub = { * @returns Transpiled from System.Threading.Tasks.Task */ join(taskId: string): Promise; + /** + * @param taskId Transpiled from string + * @returns Transpiled from System.Threading.Tasks.Task + */ + leave(taskId: string): Promise; + /** + * @param taskId Transpiled from string + * @returns Transpiled from System.Threading.Tasks.Task + */ + getProgress(taskId: string): Promise; } export type IDigitalTubesReceiver = { diff --git a/src/utils/signalR/server.Hubs.ts b/src/utils/signalR/server.Hubs.ts index 4b6b4b6..9726bda 100644 --- a/src/utils/signalR/server.Hubs.ts +++ b/src/utils/signalR/server.Hubs.ts @@ -12,11 +12,10 @@ export type DigitalTubeTaskStatus = { /** Transpiled from server.Hubs.ProgressStatus */ export enum ProgressStatus { - Pending = 0, - InProgress = 1, - Completed = 2, - Canceled = 3, - Failed = 4, + Running = 0, + Completed = 1, + Canceled = 2, + Failed = 3, } /** Transpiled from server.Hubs.ProgressInfo */ From caa26c729e2643512f07309f1427aabe2e189f7c Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sun, 17 Aug 2025 13:46:14 +0800 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20=E5=89=8D=E7=AB=AF=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=8B=A8=E7=A0=81=E5=BC=80=E5=85=B3=E7=AC=AC=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E6=97=A0=E6=B3=95=E5=BC=80=E5=85=B3=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=9B=E5=90=8E=E7=AB=AF=E4=BF=AE=E5=A4=8D=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=9D=A1=E5=81=9C=E6=AD=A2=E5=9C=A83%=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/Controllers/JtagController.cs | 2 +- server/src/Peripherals/JtagClient.cs | 41 ++++++++++++------------ server/src/UdpClientPool.cs | 2 +- src/components/equipments/Switch.vue | 3 +- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/server/src/Controllers/JtagController.cs b/server/src/Controllers/JtagController.cs index 717f366..bf4670c 100644 --- a/server/src/Controllers/JtagController.cs +++ b/server/src/Controllers/JtagController.cs @@ -228,7 +228,7 @@ public class JtagController : ControllerBase // 下载比特流 var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port); - var ret = await jtagCtrl.DownloadBitstream(processedBytes); + var ret = await jtagCtrl.DownloadBitstream(processedBytes, taskId); if (ret.IsSuccessful) { diff --git a/server/src/Peripherals/JtagClient.cs b/server/src/Peripherals/JtagClient.cs index 0fcafac..90ac444 100644 --- a/server/src/Peripherals/JtagClient.cs +++ b/server/src/Peripherals/JtagClient.cs @@ -380,7 +380,7 @@ public class JtagStatusReg public class Jtag { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - private readonly ProgressTracker? _progressTracker; + private readonly ProgressTracker _progressTracker = MsgBus.ProgressTracker; private const int CLOCK_FREQ = 50; // MHz @@ -400,14 +400,12 @@ public class Jtag /// 目标 IP 地址 /// 目标 UDP 端口 /// 超时时间(毫秒) - /// 进度追踪器 - public Jtag(string address, int port, int timeout = 2000, ProgressTracker? progressTracker = null) + 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; - this._progressTracker = progressTracker; } async ValueTask> ReadFIFO(uint devAddr) @@ -472,7 +470,8 @@ public class Jtag UInt32 resultMask = 0xFF_FF_FF_FF, UInt32 delayMilliseconds = 0, string progressId = "") { { - var ret = await UDPClientPool.WriteAddr(this.ep, 0, devAddr, data, this.timeout, progressId); + var ret = await UDPClientPool.WriteAddr( + this.ep, 0, devAddr, data, this.timeout, progressId); if (!ret.IsSuccessful) return new(ret.Error); if (!ret.Value) return new(new Exception("Write FIFO failed")); } @@ -483,7 +482,7 @@ public class Jtag { var ret = await UDPClientPool.ReadAddrWithWait(this.ep, 0, JtagAddr.STATE, result, resultMask, 0, this.timeout); if (!ret.IsSuccessful) return new(ret.Error); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); return ret.Value; } } @@ -583,7 +582,7 @@ public class Jtag else if (!ret.Value) return new(new Exception("Write CMD_JTAG_LOAD_DR_CAREI Failed")); } - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); { var ret = await WriteFIFO( @@ -724,41 +723,43 @@ public class Jtag logger.Trace($"Clear up udp server {this.address,0} receive data"); + _progressTracker.AdvanceProgress(progressId, 10); + Result ret; ret = await CloseTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Close Test Failed")); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); ret = await RunTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); 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")); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); ret = await RunTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); 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")); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); logger.Trace("Jtag ready to write bitstream"); ret = await IdleDelay(100000); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed")); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); ret = await LoadDRCareInput(bitstream, progressId: progressId); if (!ret.IsSuccessful) return new(ret.Error); @@ -769,40 +770,40 @@ public class Jtag ret = await CloseTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Close Test Failed")); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); ret = await RunTest(); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag Run Test Failed")); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); 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")); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); logger.Trace("Jtag reset device"); ret = await IdleDelay(10000); if (!ret.IsSuccessful) return new(ret.Error); else if (!ret.Value) return new(new Exception("Jtag IDLE Delay Failed")); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); var retCode = await ReadStatusReg(); if (!retCode.IsSuccessful) return new(retCode.Error); var jtagStatus = new JtagStatusReg(retCode.Value); if (!(jtagStatus.done && jtagStatus.wakeup_over && jtagStatus.init_complete)) return new(new Exception("Jtag download bitstream failed")); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); 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"); - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); // Finish - _progressTracker?.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 10); return true; } diff --git a/server/src/UdpClientPool.cs b/server/src/UdpClientPool.cs index 99a4ac1..12113ff 100644 --- a/server/src/UdpClientPool.cs +++ b/server/src/UdpClientPool.cs @@ -677,7 +677,7 @@ public sealed class UDPClientPool if (!udpWriteAck.Value.IsSuccessful) return false; - _progressTracker.AdvanceProgress(progressId, 10); + _progressTracker.AdvanceProgress(progressId, 5); } return true; diff --git a/src/components/equipments/Switch.vue b/src/components/equipments/Switch.vue index f136adc..8f0a021 100644 --- a/src/components/equipments/Switch.vue +++ b/src/components/equipments/Switch.vue @@ -167,7 +167,8 @@ function updateStatus(newStates: boolean[], index?: number) { if (props.enableDigitalTwin) { try { const client = getClient(); - if (!isUndefined(index)) client.setSwitchOnOff(index, newStates[index]); + if (!isUndefined(index)) + client.setSwitchOnOff(index + 1, newStates[index]); else client.setMultiSwitchsOnOff(btnStatus.value); } catch (error: any) {} } From 8e69c968913da4830e5913d036bb8b23969948b7 Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sun, 17 Aug 2025 14:23:35 +0800 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=9D=A1=E7=9A=84=E6=AD=A5=E5=B9=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/Controllers/JtagController.cs | 2 +- server/src/Hubs/ProgressHub.cs | 2 +- server/src/Services/ProgressTracker.cs | 2 +- server/src/UdpClientPool.cs | 2 +- src/utils/signalR/server.Hubs.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/Controllers/JtagController.cs b/server/src/Controllers/JtagController.cs index bf4670c..b1a86b8 100644 --- a/server/src/Controllers/JtagController.cs +++ b/server/src/Controllers/JtagController.cs @@ -183,7 +183,7 @@ public class JtagController : ControllerBase logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes"); // 定义进度跟踪 - var taskId = _tracker.CreateTask(1000); + var taskId = _tracker.CreateTask(10000); _tracker.AdvanceProgress(taskId, 10); _ = Task.Run(async () => diff --git a/server/src/Hubs/ProgressHub.cs b/server/src/Hubs/ProgressHub.cs index 3f153d1..15dfdae 100644 --- a/server/src/Hubs/ProgressHub.cs +++ b/server/src/Hubs/ProgressHub.cs @@ -37,7 +37,7 @@ public class ProgressInfo { public required string TaskId { get; set; } public required ProgressStatus Status { get; set; } - public required int ProgressPercent { get; set; } + public required double ProgressPercent { get; set; } public required string ErrorMessage { get; set; } }; diff --git a/server/src/Services/ProgressTracker.cs b/server/src/Services/ProgressTracker.cs index 27f34dc..eac1fee 100644 --- a/server/src/Services/ProgressTracker.cs +++ b/server/src/Services/ProgressTracker.cs @@ -50,7 +50,7 @@ public readonly struct TaskProgress TaskState.Cancelled => ProgressStatus.Canceled, _ => ProgressStatus.Failed }, - ProgressPercent = Total > 0 ? (Current * 100) / Total : 0, + ProgressPercent = Total > 0 ? ((double)Current * 100) / (double)Total : 0, ErrorMessage = Error ?? string.Empty }; } diff --git a/server/src/UdpClientPool.cs b/server/src/UdpClientPool.cs index 12113ff..9660e87 100644 --- a/server/src/UdpClientPool.cs +++ b/server/src/UdpClientPool.cs @@ -677,7 +677,7 @@ public sealed class UDPClientPool if (!udpWriteAck.Value.IsSuccessful) return false; - _progressTracker.AdvanceProgress(progressId, 5); + _progressTracker.AdvanceProgress(progressId, 1); } return true; diff --git a/src/utils/signalR/server.Hubs.ts b/src/utils/signalR/server.Hubs.ts index 9726bda..647c84f 100644 --- a/src/utils/signalR/server.Hubs.ts +++ b/src/utils/signalR/server.Hubs.ts @@ -24,7 +24,7 @@ export type ProgressInfo = { taskId: string; /** Transpiled from server.Hubs.ProgressStatus */ status: ProgressStatus; - /** Transpiled from int */ + /** Transpiled from double */ progressPercent: number; /** Transpiled from string */ errorMessage: string;