feat: 前端七段数码管添加数字孪生功能
This commit is contained in:
parent
3644c75304
commit
0a1e0982c2
|
@ -29,7 +29,7 @@ DebuggerCmd.md
|
|||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sw?
|
||||
|
||||
prompt.md
|
||||
*.tsbuildinfo
|
||||
|
||||
# Generated Files
|
||||
|
|
13
TODO.md
13
TODO.md
|
@ -1,13 +0,0 @@
|
|||
# TODO
|
||||
|
||||
1. 后端HTTP视频流
|
||||
|
||||
640*480, RGB565
|
||||
0x0000_0000 + 25800
|
||||
|
||||
|
||||
2. 信号发生器界面导入.dat文件
|
||||
3. 示波器后端交互、前端界面
|
||||
4. 逻辑分析仪后端交互、前端界面
|
||||
5. 前端重构
|
||||
6. 数据库 —— 用户登录、板卡资源分配、板卡IP地址分配
|
|
@ -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<DiagramData> {
|
|||
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<DiagramData> {
|
|||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ export const previewSizes: Record<string, number> = {
|
|||
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: "以太网接口" },
|
||||
|
|
|
@ -1,65 +1,114 @@
|
|||
<template>
|
||||
<div class="seven-segment-display" :style="{
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
position: 'relative',
|
||||
}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 120 220" class="display">
|
||||
<div
|
||||
class="seven-segment-display"
|
||||
:style="{
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
position: 'relative',
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="width"
|
||||
:height="height"
|
||||
viewBox="0 0 120 220"
|
||||
class="display"
|
||||
>
|
||||
<!-- 数码管基座 -->
|
||||
<rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
|
||||
<rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
|
||||
<!-- 7段 + 小数点,每个段由多边形表示,重新设计点位置使其更接近实际数码管 -->
|
||||
<!-- a段 (顶部横线) -->
|
||||
<polygon :points="'30,20 90,20 98,28 82,36 38,36 22,28'"
|
||||
<polygon
|
||||
:points="'30,20 90,20 98,28 82,36 38,36 22,28'"
|
||||
:fill="isSegmentActive('a') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- b段 (右上竖线) -->
|
||||
<polygon :points="'100,30 108,38 108,82 100,90 92,82 92,38'"
|
||||
<polygon
|
||||
:points="'100,30 108,38 108,82 100,90 92,82 92,38'"
|
||||
:fill="isSegmentActive('b') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- c段 (右下竖线) -->
|
||||
<polygon :points="'100,90 108,98 108,142 100,150 92,142 92,98'"
|
||||
<polygon
|
||||
:points="'100,90 108,98 108,142 100,150 92,142 92,98'"
|
||||
:fill="isSegmentActive('c') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- d段 (底部横线) -->
|
||||
<polygon :points="'30,160 90,160 98,152 82,144 38,144 22,152'"
|
||||
<polygon
|
||||
:points="'30,160 90,160 98,152 82,144 38,144 22,152'"
|
||||
:fill="isSegmentActive('d') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- e段 (左下竖线) -->
|
||||
<polygon :points="'20,90 28,98 28,142 20,150 12,142 12,98'"
|
||||
<polygon
|
||||
:points="'20,90 28,98 28,142 20,150 12,142 12,98'"
|
||||
:fill="isSegmentActive('e') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- f段 (左上竖线) -->
|
||||
<polygon :points="'20,30 28,38 28,82 20,90 12,82 12,38'"
|
||||
<polygon
|
||||
:points="'20,30 28,38 28,82 20,90 12,82 12,38'"
|
||||
:fill="isSegmentActive('f') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
|
||||
<!-- g段 (中间横线) -->
|
||||
<polygon :points="'30,90 38,82 82,82 90,90 82,98 38,98'"
|
||||
<polygon
|
||||
:points="'30,90 38,82 82,82 90,90 82,98 38,98'"
|
||||
:fill="isSegmentActive('g') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }" class="segment" />
|
||||
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
<!-- dp段 (小数点) -->
|
||||
<circle cx="108" cy="154" r="6" :fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }" class="segment" />
|
||||
<circle
|
||||
cx="108"
|
||||
cy="154"
|
||||
r="6"
|
||||
:fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
|
||||
class="segment"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- 引脚 -->
|
||||
<div v-for="pin in pins" :key="pin.pinId" :style="{
|
||||
position: 'absolute',
|
||||
left: `${pin.x * props.size}px`,
|
||||
top: `${pin.y * props.size}px`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${pin.x * props.size}`"
|
||||
:data-pin-y="`${pin.y * props.size}`">
|
||||
<Pin :ref="(el) => {
|
||||
if (el) pinRefs[pin.pinId] = el;
|
||||
}
|
||||
" :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId" @pin-click="$emit('pin-click', $event)" />
|
||||
<div
|
||||
v-for="pin in pins"
|
||||
:key="pin.pinId"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${pin.x * props.size}px`,
|
||||
top: `${pin.y * props.size}px`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}"
|
||||
:data-pin-wrapper="`${pin.pinId}`"
|
||||
:data-pin-x="`${pin.x * props.size}`"
|
||||
:data-pin-y="`${pin.y * props.size}`"
|
||||
>
|
||||
<Pin
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el) pinRefs[pin.pinId] = el;
|
||||
}
|
||||
"
|
||||
:label="pin.pinId"
|
||||
:constraint="pin.constraint"
|
||||
:pinId="pin.pinId"
|
||||
@pin-click="$emit('pin-click', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -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,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -418,7 +467,8 @@ defineExpose({
|
|||
|
||||
/* 数码管发光效果 */
|
||||
.segment[style*="opacity: 1"] {
|
||||
filter: drop-shadow(0 0 4px v-bind(segmentColor)) drop-shadow(0 0 2px v-bind(segmentColor));
|
||||
filter: drop-shadow(0 0 4px v-bind(segmentColor))
|
||||
drop-shadow(0 0 2px v-bind(segmentColor));
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -0,0 +1,413 @@
|
|||
<template>
|
||||
<div
|
||||
class="seven-segment-display"
|
||||
:style="{
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
position: 'relative',
|
||||
}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="width"
|
||||
:height="height"
|
||||
viewBox="0 0 120 220"
|
||||
class="display"
|
||||
>
|
||||
<!-- 数码管基座 -->
|
||||
<rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
|
||||
<rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
|
||||
|
||||
<!-- 7段显示 -->
|
||||
<polygon
|
||||
v-for="(segment, id) in segmentPaths"
|
||||
:key="id"
|
||||
:points="segment.points"
|
||||
:fill="isSegmentActive(id) ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive(id) ? 1 : 0.15 }"
|
||||
:class="{ segment: true, active: isSegmentActive(id) }"
|
||||
/>
|
||||
|
||||
<!-- 小数点 -->
|
||||
<circle
|
||||
cx="108"
|
||||
cy="154"
|
||||
r="6"
|
||||
:fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
|
||||
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
|
||||
:class="{ segment: true, active: isSegmentActive('dp') }"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- 引脚(仅在非数字孪生模式下显示) -->
|
||||
<div
|
||||
v-if="!props.enableDigitalTwin"
|
||||
v-for="pin in props.pins"
|
||||
:key="pin.pinId"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${pin.x * props.size}px`,
|
||||
top: `${pin.y * props.size}px`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}"
|
||||
:data-pin-wrapper="`${pin.pinId}`"
|
||||
:data-pin-x="`${pin.x * props.size}`"
|
||||
:data-pin-y="`${pin.y * props.size}`"
|
||||
>
|
||||
<Pin
|
||||
:ref="
|
||||
(el) => {
|
||||
if (el) pinRefs[pin.pinId] = el;
|
||||
}
|
||||
"
|
||||
:label="pin.pinId"
|
||||
:constraint="pin.constraint"
|
||||
:pinId="pin.pinId"
|
||||
@pin-click="$emit('pin-click', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||||
import { useConstraintsStore } from "../../stores/constraints";
|
||||
import Pin from "./Pin.vue";
|
||||
import { useEquipments } from "@/stores/equipments";
|
||||
|
||||
// ============================================================================
|
||||
// Linus式极简数据结构:一个byte解决一切
|
||||
// ============================================================================
|
||||
|
||||
interface Props {
|
||||
size?: number;
|
||||
color?: string;
|
||||
enableDigitalTwin?: boolean;
|
||||
digitalTwinNum?: number;
|
||||
afterglowDuration?: number;
|
||||
cathodeType?: "common" | "anode";
|
||||
pins?: Array<{
|
||||
pinId: string;
|
||||
constraint: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 1,
|
||||
color: "red",
|
||||
enableDigitalTwin: false,
|
||||
digitalTwinNum: 0,
|
||||
afterglowDuration: 500,
|
||||
cathodeType: "common",
|
||||
pins: () => [
|
||||
{ pinId: "a", constraint: "", x: 10, y: 170 },
|
||||
{ pinId: "b", constraint: "", x: 24, y: 170 },
|
||||
{ pinId: "c", constraint: "", x: 38, y: 170 },
|
||||
{ pinId: "d", constraint: "", x: 52, y: 170 },
|
||||
{ pinId: "e", constraint: "", x: 66, y: 170 },
|
||||
{ pinId: "f", constraint: "", x: 80, y: 170 },
|
||||
{ pinId: "g", constraint: "", x: 94, y: 170 },
|
||||
{ pinId: "dp", constraint: "", x: 108, y: 170 },
|
||||
{ pinId: "COM", constraint: "", x: 60, y: 10 },
|
||||
],
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 核心状态:简单到极致
|
||||
// ============================================================================
|
||||
|
||||
// 当前显示状态 - 8bit对应8个段
|
||||
const displayByte = ref<number>(0);
|
||||
|
||||
// 余晖状态
|
||||
const afterglowByte = ref<number>(0);
|
||||
const afterglowTimer = ref<number | null>(null);
|
||||
|
||||
// 约束系统状态(兼容模式)
|
||||
const constraintStates = ref<Record<string, boolean>>({
|
||||
a: false,
|
||||
b: false,
|
||||
c: false,
|
||||
d: false,
|
||||
e: false,
|
||||
f: false,
|
||||
g: false,
|
||||
dp: false,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Bit操作:硬件工程师的好品味
|
||||
// ============================================================================
|
||||
|
||||
// 段到bit位的映射 (标准7段数码管编码)
|
||||
const SEGMENT_BITS = {
|
||||
a: 0, // bit 0
|
||||
b: 1, // bit 1
|
||||
c: 2, // bit 2
|
||||
d: 3, // bit 3
|
||||
e: 4, // bit 4
|
||||
f: 5, // bit 5
|
||||
g: 6, // bit 6
|
||||
dp: 7, // bit 7
|
||||
} as const;
|
||||
|
||||
function isBitSet(byte: number, bit: number): boolean {
|
||||
return (byte & (1 << bit)) !== 0;
|
||||
}
|
||||
|
||||
function isSegmentActive(segmentId: keyof typeof SEGMENT_BITS): boolean {
|
||||
if (props.enableDigitalTwin) {
|
||||
// 数字孪生模式:余晖优先,然后是当前byte
|
||||
const bit = SEGMENT_BITS[segmentId];
|
||||
return (
|
||||
isBitSet(afterglowByte.value, bit) || isBitSet(displayByte.value, bit)
|
||||
);
|
||||
} else {
|
||||
// 约束模式:使用传统逻辑
|
||||
return constraintStates.value[segmentId] || false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SignalR数字孪生集成
|
||||
// ============================================================================
|
||||
|
||||
const {
|
||||
sevenSegmentDisplaySetOnOff,
|
||||
sevenSegmentDisplayData,
|
||||
sevenSegmentDisplaySetFrequency,
|
||||
} = useEquipments();
|
||||
|
||||
async function initDigitalTwin() {
|
||||
if (!props.enableDigitalTwin || !props.digitalTwinNum) return;
|
||||
|
||||
try {
|
||||
sevenSegmentDisplaySetOnOff(props.enableDigitalTwin);
|
||||
|
||||
console.log(
|
||||
`Digital twin initialized for address: ${props.digitalTwinNum}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("Failed to initialize digital twin:", error);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [sevenSegmentDisplayData],
|
||||
() => {
|
||||
if (
|
||||
!sevenSegmentDisplayData ||
|
||||
props.digitalTwinNum < 0 ||
|
||||
props.digitalTwinNum > 31
|
||||
)
|
||||
return;
|
||||
|
||||
handleDigitalTwinData(sevenSegmentDisplayData[props.digitalTwinNum]);
|
||||
},
|
||||
);
|
||||
|
||||
function handleDigitalTwinData(data: any) {
|
||||
let newByte = 0;
|
||||
|
||||
if (typeof data === "number") {
|
||||
// 直接是byte数据
|
||||
newByte = data & 0xff; // 确保只取低8位
|
||||
} else if (data && typeof data.value === "number") {
|
||||
// 包装在对象中的byte数据
|
||||
newByte = data.value & 0xff;
|
||||
} else if (data && data.segments) {
|
||||
// 段状态对象格式
|
||||
Object.keys(SEGMENT_BITS).forEach((segment) => {
|
||||
if (data.segments[segment]) {
|
||||
newByte |= 1 << SEGMENT_BITS[segment as keyof typeof SEGMENT_BITS];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateDisplayByte(newByte);
|
||||
}
|
||||
|
||||
function updateDisplayByte(newByte: number) {
|
||||
const oldByte = displayByte.value;
|
||||
displayByte.value = newByte;
|
||||
|
||||
// 启动余晖效果
|
||||
if (oldByte !== 0 && newByte !== oldByte) {
|
||||
startAfterglow(oldByte);
|
||||
}
|
||||
}
|
||||
|
||||
function startAfterglow(byte: number) {
|
||||
afterglowByte.value = byte;
|
||||
|
||||
if (afterglowTimer.value) {
|
||||
clearTimeout(afterglowTimer.value);
|
||||
}
|
||||
|
||||
afterglowTimer.value = setTimeout(() => {
|
||||
afterglowByte.value = 0;
|
||||
afterglowTimer.value = null;
|
||||
}, props.afterglowDuration);
|
||||
}
|
||||
|
||||
function cleanupDigitalTwin() {
|
||||
sevenSegmentDisplaySetOnOff(false);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 约束系统兼容(传统模式)
|
||||
// ============================================================================
|
||||
|
||||
const { getConstraintState, onConstraintStateChange } = useConstraintsStore();
|
||||
let constraintUnsubscribe: (() => void) | null = null;
|
||||
|
||||
function updateConstraintStates() {
|
||||
if (props.enableDigitalTwin) return; // 数字孪生模式下忽略约束
|
||||
|
||||
// 获取COM状态
|
||||
const comPin = props.pins.find((p) => p.pinId === "COM");
|
||||
const comActive = isComActive(comPin);
|
||||
|
||||
if (!comActive) {
|
||||
// COM不活跃,所有段关闭
|
||||
Object.keys(constraintStates.value).forEach((key) => {
|
||||
constraintStates.value[key] = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新各段状态
|
||||
props.pins.forEach((pin) => {
|
||||
if (Object.hasOwnProperty.call(SEGMENT_BITS, pin.pinId)) {
|
||||
constraintStates.value[pin.pinId] = isPinActive(pin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isComActive(comPin: any): boolean {
|
||||
if (!comPin?.constraint) return true;
|
||||
const state = getConstraintState(comPin.constraint);
|
||||
return props.cathodeType === "common" ? state === "low" : state === "low";
|
||||
}
|
||||
|
||||
function isPinActive(pin: any): boolean {
|
||||
if (!pin.constraint) return false;
|
||||
const state = getConstraintState(pin.constraint);
|
||||
return props.cathodeType === "common" ? state === "high" : state === "low";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 渲染数据
|
||||
// ============================================================================
|
||||
|
||||
const segmentPaths = {
|
||||
a: { points: "30,20 90,20 98,28 82,36 38,36 22,28" },
|
||||
b: { points: "100,30 108,38 108,82 100,90 92,82 92,38" },
|
||||
c: { points: "100,90 108,98 108,142 100,150 92,142 92,98" },
|
||||
d: { points: "30,160 90,160 98,152 82,144 38,144 22,152" },
|
||||
e: { points: "20,90 28,98 28,142 20,150 12,142 12,98" },
|
||||
f: { points: "20,30 28,38 28,82 20,90 12,82 12,38" },
|
||||
g: { points: "30,90 38,82 82,82 90,90 82,98 38,98" },
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// 计算属性
|
||||
// ============================================================================
|
||||
|
||||
const width = computed(() => 120 * props.size);
|
||||
const height = computed(() => 220 * props.size);
|
||||
const segmentColor = computed(() => props.color);
|
||||
const inactiveColor = computed(() => "#FFFFFF");
|
||||
const pinRefs = ref<Record<string, any>>({});
|
||||
|
||||
// ============================================================================
|
||||
// 生命周期
|
||||
// ============================================================================
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.enableDigitalTwin) {
|
||||
await initDigitalTwin();
|
||||
} else {
|
||||
constraintUnsubscribe = onConstraintStateChange(updateConstraintStates);
|
||||
updateConstraintStates();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupDigitalTwin();
|
||||
|
||||
if (constraintUnsubscribe) {
|
||||
constraintUnsubscribe();
|
||||
}
|
||||
|
||||
if (afterglowTimer.value) {
|
||||
clearTimeout(afterglowTimer.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听模式切换
|
||||
watch(
|
||||
() => [props.enableDigitalTwin, props.digitalTwinNum],
|
||||
async () => {
|
||||
// 清理旧模式
|
||||
cleanupDigitalTwin();
|
||||
if (constraintUnsubscribe) {
|
||||
constraintUnsubscribe();
|
||||
constraintUnsubscribe = null;
|
||||
}
|
||||
|
||||
// 初始化新模式
|
||||
if (props.enableDigitalTwin) {
|
||||
await initDigitalTwin();
|
||||
} else {
|
||||
constraintUnsubscribe = onConstraintStateChange(updateConstraintStates);
|
||||
updateConstraintStates();
|
||||
}
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.seven-segment-display {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.segment {
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
fill 0.2s;
|
||||
}
|
||||
|
||||
.segment.active {
|
||||
filter: drop-shadow(0 0 4px v-bind(segmentColor))
|
||||
drop-shadow(0 0 2px v-bind(segmentColor));
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
export function getDefaultProps() {
|
||||
return {
|
||||
size: 1,
|
||||
color: "red",
|
||||
enableDigitalTwin: false,
|
||||
digitalTwinNum: 0,
|
||||
afterglowDuration: 500,
|
||||
cathodeType: "common",
|
||||
pins: [
|
||||
{ pinId: "a", constraint: "", x: 10, y: 170 },
|
||||
{ pinId: "b", constraint: "", x: 24, y: 170 },
|
||||
{ pinId: "c", constraint: "", x: 38, y: 170 },
|
||||
{ pinId: "d", constraint: "", x: 52, y: 170 },
|
||||
{ pinId: "e", constraint: "", x: 66, y: 170 },
|
||||
{ pinId: "f", constraint: "", x: 80, y: 170 },
|
||||
{ pinId: "g", constraint: "", x: 94, y: 170 },
|
||||
{ pinId: "dp", constraint: "", x: 108, y: 170 },
|
||||
{ pinId: "COM", constraint: "", x: 60, y: 10 },
|
||||
],
|
||||
};
|
||||
}
|
||||
</script>
|
|
@ -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<File>();
|
||||
const jtagBoundaryScanFreq = ref(100);
|
||||
const jtagUserBitstreams = ref<ResourceInfo[]>([]);
|
||||
|
@ -62,46 +69,6 @@ export const useEquipments = defineStore("equipments", () => {
|
|||
}
|
||||
});
|
||||
|
||||
// Matrix Key
|
||||
const matrixKeyStates = reactive(new Array<boolean>(16).fill(false));
|
||||
const matrixKeypadClientMutex = withTimeout(
|
||||
new Mutex(),
|
||||
1000,
|
||||
new Error("Matrixkeyclient Mutex Timeout!"),
|
||||
);
|
||||
|
||||
// Power
|
||||
const powerClientMutex = withTimeout(
|
||||
new Mutex(),
|
||||
1000,
|
||||
new Error("Matrixkeyclient Mutex Timeout!"),
|
||||
);
|
||||
|
||||
// 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<boolean>(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<Uint8Array>();
|
||||
const sevenSegmentDisplayHub = ref<HubConnection>();
|
||||
const sevenSegmentDisplayHubProxy = ref<IDigitalTubesHub>();
|
||||
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<boolean> => {
|
||||
return await this.connection.invoke("SetFrequency", frequency);
|
||||
}
|
||||
|
||||
public readonly getStatus = async (): Promise<DigitalTubeTaskStatus> => {
|
||||
return await this.connection.invoke("GetStatus");
|
||||
}
|
||||
}
|
||||
|
||||
class IJtagHub_HubProxyFactory implements HubProxyFactory<IJtagHub> {
|
||||
|
|
|
@ -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<bool>
|
||||
*/
|
||||
setFrequency(frequency: number): Promise<boolean>;
|
||||
/**
|
||||
* @returns Transpiled from System.Threading.Tasks.Task<server.Hubs.DigitalTubeTaskStatus>
|
||||
*/
|
||||
getStatus(): Promise<DigitalTubeTaskStatus>;
|
||||
}
|
||||
|
||||
export type IJtagHub = {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue