feat: 修改后端apiclient生成逻辑

fix: 修复debugger获取flag失败的问题
refactor: 重新编写debugger前后端逻辑
This commit is contained in:
2025-07-30 15:31:11 +08:00
parent 6dfd275091
commit 3257a68407
11 changed files with 4194 additions and 1733 deletions

View File

@@ -8,6 +8,13 @@
调试器波形捕获
</div>
<div class="flex items-center gap-2">
<button
class="btn btn-sm btn-primary"
@click="startCapture(true)"
:disabled="!captureData"
>
重新捕获
</button>
<button
class="btn btn-sm btn-error"
@click="handleDeleteData"
@@ -18,6 +25,12 @@
</div>
</h2>
<WaveformDisplay :data="captureData">
<div class="text-center">
<h3 class="text-xl font-semibold text-slate-600 mb-2">
暂无逻辑分析数据
</h3>
<p class="text-sm text-slate-500">点击下方按钮开始捕获</p>
</div>
<button
class="group relative px-8 py-3 bg-gradient-to-r text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 ease-in-out focus:outline-none focus:ring-4 active:scale-95"
:class="{
@@ -45,51 +58,94 @@
<!-- Debugger 通道配置 -->
<div class="card m-5 bg-base-200 shadow-2xl">
<div class="card-body">
<h2 class="card-title mb-4">调试器通道配置</h2>
<div class="overflow-x-auto flex flex-col gap-10">
<!-- 通道状态概览 -->
<div class="flex justify-between">
<h2 class="card-title mb-4">调试器通道配置</h2>
<div class="flex items-center gap-2">
<button
class="btn btn-sm btn-primary"
@click="addChannel"
:disabled="!configInited"
>
添加通道
</button>
<button
class="btn btn-sm btn-secondary"
@click="showConfigDialog = true"
>
配置
</button>
</div>
</div>
<!-- 配置未初始化时 -->
<div
v-if="!configInited"
class="flex flex-col items-center justify-center py-10"
>
<div class="text-lg text-slate-500 mb-4">请先进行调试器基本配置</div>
<button class="btn btn-primary" @click="showConfigDialog = true">
配置调试器
</button>
</div>
<div v-if="configInited" class="overflow-x-auto flex flex-col gap-10">
<!-- 状态概览 -->
<div
class="stats stats-horizontal bg-base-100 shadow flex justify-between"
>
<div class="stat">
<div class="stat-title">总通道</div>
<div class="stat-value text-primary">16</div>
<div class="stat-desc">逻辑分析仪通道</div>
</div>
<div class="stat">
<div class="stat-title">启用通道</div>
<div class="stat-value text-success">
{{ channels.filter((ch) => ch.visible).length }}
<div class="stat-title">启用端口</div>
<div class="stat-value text-primary">
{{ config.totalPortNum }}
</div>
<div class="stat-desc">当前激活通道</div>
<div class="stat-desc">每端口最大32线</div>
</div>
<div class="stat">
<div class="stat-title">采样率</div>
<div class="stat-value text-info">5MHz</div>
<div class="stat-desc">最大采样频率</div>
<div class="stat-title">最大线宽数</div>
<div class="stat-value text-info">
{{ config.totalPortNum * 32 }}
</div>
<div class="stat-desc">启用端口数 × 32</div>
</div>
<div class="stat">
<div class="stat-title">已用线宽数</div>
<div class="stat-value text-success">
{{ channels.reduce((sum, ch) => sum + ch.width, 0) }}
</div>
<div class="stat-desc">所有通道线宽总和</div>
</div>
<div class="stat">
<div class="stat-title">采样深度</div>
<div class="stat-value text-warning">
{{ config.captureDepth }}
</div>
<div class="stat-desc">每通道采样点数</div>
</div>
<div class="stat">
<div class="stat-title">时钟频率</div>
<div class="stat-value text-accent">{{ config.clkFreq }} MHz</div>
<div class="stat-desc">采样时钟</div>
</div>
</div>
<!-- 表格 -->
<!-- 通道表格 -->
<div class="space-y-2">
<!-- 表头 -->
<div
class="grid grid-cols-6 justify-items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium"
class="grid grid-cols-7 justify-items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium"
>
<span>名称</span>
<span>显示</span>
<span>颜色</span>
<span>触发模式</span>
<span>数据位</span>
<span>数据位(起始:宽度)</span>
<span>父端口编号</span>
<span>操作</span>
</div>
<!-- 通道列表 -->
<div
v-for="(ch, idx) in channels"
:key="idx"
class="grid grid-cols-6 justify-items-center gap-2 p-2 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
class="grid grid-cols-7 place-items-center gap-4 p-4 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
>
<input
v-model="ch.name"
@@ -118,11 +174,17 @@
{{ mode.label }}
</option>
</select>
<input
v-model="ch.widthStr"
class="input input-bordered w-full"
placeholder="如0:8"
@change="parseWidthStr(idx)"
/>
<input
type="number"
min="1"
max="32"
v-model.number="ch.width"
min="0"
:max="config.totalPortNum - 1"
v-model.number="ch.parentPort"
class="input input-bordered w-full"
/>
<button class="btn btn-error" @click="removeChannel(idx)">
@@ -139,19 +201,78 @@
</div>
</div>
</div>
<!-- 配置Dialog -->
<dialog v-if="showConfigDialog" class="modal modal-open">
<form
method="dialog"
class="modal-box max-w-fit"
@submit.prevent="onConfigSubmit"
>
<h3 class="font-bold text-lg mb-4">调试器基本配置</h3>
<div class="flex flex-col gap-4 w-80">
<BaseInputField
v-model="config.clkFreq"
label="时钟频率 (MHz)"
type="number"
min="1"
max="200"
:error="
config.clkFreq < 1 || config.clkFreq > 500 ? '范围1~200' : ''
"
required
/>
<BaseInputField
v-model="config.totalPortNum"
label="启用端口数"
type="number"
min="1"
max="16"
:error="
config.totalPortNum < 1 || config.totalPortNum > 16
? '范围1~16'
: ''
"
required
/>
<BaseInputField
v-model="config.captureDepth"
label="采样深度"
type="number"
min="1"
max="1048576"
:error="
config.captureDepth < 1 || config.captureDepth > 1048576
? '范围1~1048576'
: ''
"
required
/>
</div>
<div class="modal-action mt-6">
<button class="btn btn-primary" type="submit">确定</button>
<button class="btn" type="button" @click="showConfigDialog = false">
取消
</button>
</div>
</form>
</dialog>
</div>
</template>
<script setup lang="ts">
import { CaptureMode } from "@/APIClient";
import { CaptureMode, ChannelConfig, DebuggerConfig } from "@/APIClient";
import { useAlertStore } from "@/components/Alert";
import BaseInputField from "@/components/InputField/BaseInputField.vue";
import type { LogicDataType } from "@/components/WaveformDisplay";
import WaveformDisplay from "@/components/WaveformDisplay/WaveformDisplay.vue";
import { AuthManager } from "@/utils/AuthManager";
import { useRequiredInjection } from "@/utils/Common";
import { useLocalStorage } from "@vueuse/core";
import axios, { type CancelTokenSource } from "axios";
import { isNull } from "lodash";
import { Play, Square, Zap } from "lucide-vue-next";
import { ref } from "vue";
import { ref, reactive, computed } from "vue";
interface DebugChannel {
name: string;
@@ -159,6 +280,15 @@ interface DebugChannel {
color: string;
trigger: CaptureMode;
width: number;
widthStr: string; // "start:宽度"
start: number;
parentPort: number;
}
interface DebuggerSettings {
clkFreq: number;
totalPortNum: number;
captureDepth: number;
}
const triggerModes = [
@@ -169,153 +299,234 @@ const triggerModes = [
{ value: CaptureMode.Fall, label: "↓ (下降沿)" },
];
// 基本配置
const config = reactive<DebuggerSettings>({
clkFreq: 50,
totalPortNum: 1,
captureDepth: 1024,
});
const configInited = ref(false);
const showConfigDialog = ref(false);
function onConfigSubmit() {
configInited.value = true;
showConfigDialog.value = false;
// 清空通道
channels.value = [];
}
// 通道配置
const channels = useLocalStorage<DebugChannel[]>("debugger-channels", []);
const captureData = ref<LogicDataType>();
const alert = useRequiredInjection(useAlertStore);
const isCapturing = ref(false);
const readCancelTokenSource = ref<CancelTokenSource | null>(null);
async function startCapture() {
if (channels.value.length === 0) {
alert.error("请至少添加一个通道");
// 解析widthStr为start/width
function parseWidthStr(idx: number) {
const ch = channels.value[idx];
const match = /^(\d+)\s*:\s*(\d+)$/.exec(ch.widthStr);
if (isNull(match)) {
alert.error("格式错误,应为 起始位:宽度,如 0:7");
ch.widthStr = `${ch.start}:${ch.width}`;
return;
}
isCapturing.value = true;
const client = AuthManager.createAuthenticatedDebuggerClient();
const min = Math.min(parseInt(match[1]), parseInt(match[2]));
const max = Math.max(parseInt(match[1]), parseInt(match[2]));
for (let i = 0; i < channels.value.length; i++) {
const channel = channels.value[i];
if (!channel.visible) continue;
if (!channel.name) {
alert.error(`通道 ${i + 1} 名称不能为空`);
isCapturing.value = false;
return;
}
if (channel.width < 1 || channel.width > 32) {
alert.error(`通道 ${i + 1} 数据位数必须在1到32之间`);
isCapturing.value = false;
return;
}
try {
let ret = await client.setMode(i, channel.trigger);
if (!ret) {
alert.error(`设置通道 ${i + 1} 触发模式失败`);
isCapturing.value = false;
return;
}
} catch (error: any) {
alert.error(`设置通道 ${i + 1} 触发模式失败: ${error.message}`);
isCapturing.value = false;
return;
}
}
try {
let ret = await client.startTrigger();
if (!ret) {
alert.error("开始捕获失败,请检查连接");
isCapturing.value = false;
return;
}
while ((await client.readFlag()) !== 1 && isCapturing.value) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
if (!isCapturing.value) {
alert.info("捕获已停止");
return;
}
const base64Data = await client.readData(0);
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// 数据分割
// 1. 统计每个通道的位宽
const enabledChannels = channels.value.filter((ch) => ch.visible);
const widths = enabledChannels.map((ch) => ch.width);
const totalBitsPerSample = widths.reduce((a, b) => a + b, 0);
// 2. 计算总采样点数
const totalBits = bytes.length * 8;
const sampleCount = Math.floor(totalBits / totalBitsPerSample);
// 3. 逐采样点解析
let bitOffset = 0;
const channelValues: number[][] = enabledChannels.map(() => []);
for (let sampleIdx = 0; sampleIdx < sampleCount; sampleIdx++) {
for (let chIdx = 0; chIdx < enabledChannels.length; chIdx++) {
const width = widths[chIdx];
let value = 0;
for (let w = 0; w < width; w++) {
const absBit = bitOffset + w;
const byteIdx = Math.floor(absBit / 8);
const bitInByte = absBit % 8;
const bit = (bytes[byteIdx] >> (7 - bitInByte)) & 1;
value = (value << 1) | bit;
}
channelValues[chIdx].push(value);
bitOffset += width;
}
}
// 4. 构造LogicDataType
const x: number[] = [];
const xUnit = "us"; // 5MHz -> 0.2us/point, 但WaveformDisplay支持ns/ms/us/s选us
for (let i = 0; i < sampleCount; i++) {
x.push(i * 0.2); // 0.2us per sample
}
const y = enabledChannels.map((ch, idx) => ({
enabled: true,
type: ch.width === 1 ? ("logic" as const) : ("number" as const),
name: ch.name,
color: ch.color,
value: channelValues[idx],
base: ch.width === 1 ? ("bin" as const) : ("hex" as const),
}));
captureData.value = {
x: x,
y: y,
xUnit: "us",
};
} catch (error: any) {
alert.error(`开始捕获失败: ${error.message}`);
isCapturing.value = false;
return;
}
}
function stopCapture() {
isCapturing.value = false;
}
function handleDeleteData() {
captureData.value = undefined;
ch.start = min;
ch.width = max - min + 1;
}
function addChannel() {
if (channels.value.length >= 16) {
alert.error("最多只能添加16个通道");
if (!configInited.value) {
alert.error("请先配置调试器基本参数");
return;
}
if (channels.value.length >= config.totalPortNum * 32) {
alert.error("通道数已达最大线宽数");
return;
}
channels.value.push({
name: `CH${channels.value.length + 1}`,
visible: true,
color: "#00bcd4",
trigger: CaptureMode.None,
width: 1,
widthStr: "0:1",
start: 0,
parentPort: 0,
});
}
function removeChannel(idx: number) {
channels.value.splice(idx, 1);
}
function handleDeleteData() {
captureData.value = undefined;
}
function stopCapture() {
isCapturing.value = false;
if (readCancelTokenSource.value) {
readCancelTokenSource.value.cancel("用户手动停止捕获");
readCancelTokenSource.value = null;
}
}
async function startCapture(isRestart = false) {
if (!configInited.value) {
alert.error("请先配置调试器基本参数");
return;
}
if (channels.value.length === 0) {
alert.error("请至少添加一个通道");
return;
}
// 校验通道参数
if (!isRestart) {
let usedWires = 0;
for (let i = 0; i < channels.value.length; i++) {
const ch = channels.value[i];
if (!ch.visible) continue;
if (!ch.name) {
alert.error(`通道 ${i + 1} 名称不能为空`);
return;
}
if (ch.width < 1 || ch.width > 32) {
alert.error(`通道 ${i + 1} 数据位宽必须在1到32之间`);
return;
}
if (ch.start < 0 || ch.start + ch.width > 32) {
alert.error(`通道 ${i + 1} 起始位+宽度不能超过32`);
return;
}
if (ch.parentPort < 0 || ch.parentPort >= config.totalPortNum) {
alert.error(`通道 ${i + 1} 父端口编号超出范围`);
return;
}
usedWires += ch.width;
}
if (usedWires > config.totalPortNum * 32) {
alert.error("所有通道线宽总和不能超过最大线宽数");
return;
}
}
isCapturing.value = true;
const client = AuthManager.createAuthenticatedDebuggerClient();
// 构造API配置
const channelConfigs = channels.value
.filter((ch) => ch.visible)
.map(
(ch) =>
new ChannelConfig({
name: ch.name,
color: ch.color,
wireWidth: ch.width,
wireStartIndex: ch.start,
parentPort: ch.parentPort,
mode: ch.trigger,
}),
);
const apiConfig = new DebuggerConfig({
clkFreq: config.clkFreq,
totalPortNum: config.totalPortNum,
captureDepth: config.captureDepth,
triggerNum: 0,
channelConfigs: channelConfigs,
});
try {
// 设置通道模式
if (!isRestart) {
let ret = await client.setChannelsMode(apiConfig);
if (!ret) {
alert.error("设置通道模式失败");
isCapturing.value = false;
return;
}
// 启动捕获
ret = await client.startTrigger();
if (!ret) {
alert.error("开始捕获失败,请检查连接");
isCapturing.value = false;
return;
}
} else {
let ret = await client.restartTrigger();
if (!ret) {
alert.error("重新开始捕获失败,请检查连接");
isCapturing.value = false;
return;
}
}
// 读取数据
readCancelTokenSource.value = axios.CancelToken.source();
const readDataPromise = client
.readData(apiConfig, readCancelTokenSource.value.token)
.then((data) => {
const enabledChannels = channelConfigs;
const sampleCount = config.captureDepth;
// 解析数据
const y = data.map((cd, idx) => {
const ch = enabledChannels[idx];
const bin = atob(cd.data);
// UInt32数组
const arr = [];
for (let i = 0; i < bin.length; i += 4) {
arr.push(
(bin.charCodeAt(i) << 24) |
(bin.charCodeAt(i + 1) << 16) |
(bin.charCodeAt(i + 2) << 8) |
bin.charCodeAt(i + 3),
);
}
// 截取采样深度
return {
enabled: true,
type: ch.wireWidth === 1 ? ("logic" as const) : ("number" as const),
name: ch.name,
color: ch.color,
value: arr.slice(0, sampleCount),
base: ch.wireWidth === 1 ? ("bin" as const) : ("hex" as const),
};
});
const x: number[] = [];
for (let i = 0; i < sampleCount; i++) {
x.push(i * (1 / config.clkFreq)); // us
}
captureData.value = {
x,
y,
xUnit: "us",
};
})
.catch((error) => {
if (axios.isCancel(error)) {
alert.info("捕获已取消");
} else {
alert.error(`读取数据失败: ${error.message}`);
}
})
.finally(() => {
isCapturing.value = false;
readCancelTokenSource.value = null;
});
} catch (error: any) {
alert.error(`开始捕获失败: ${error.message}`);
isCapturing.value = false;
return;
}
}
</script>