feat: 完成debugger前后端交互
This commit is contained in:
parent
23d4459406
commit
3da0f284f3
|
@ -58,15 +58,20 @@ public class DebuggerController : ControllerBase
|
|||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<IActionResult> SetMode([FromBody] CaptureMode mode)
|
||||
public async Task<IActionResult> SetMode(int channelNum, CaptureMode mode)
|
||||
{
|
||||
if (channelNum > 0x0F)
|
||||
{
|
||||
return BadRequest($"最多只能建立16个通道");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var debugger = GetDebugger();
|
||||
if (debugger == null)
|
||||
return BadRequest("用户未绑定有效的实验板");
|
||||
|
||||
var result = await debugger.SetMode(mode);
|
||||
var result = await debugger.SetMode((byte)channelNum, mode);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error($"设置捕获模式失败: {result.Error}");
|
||||
|
|
|
@ -43,12 +43,12 @@ class DebuggerCmd
|
|||
/// 启动触发器命令
|
||||
/// </summary>
|
||||
public const UInt32 Start = 0xFFFF_FFFF;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 刷新命令
|
||||
/// </summary>
|
||||
public const UInt32 Fresh = 0x0000_0000;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 清除信号标志命令
|
||||
/// </summary>
|
||||
|
@ -119,12 +119,18 @@ public class DebuggerClient
|
|||
/// <summary>
|
||||
/// 设置信号捕获模式
|
||||
/// </summary>
|
||||
/// <param name="channelNum">要设置的通道</param>
|
||||
/// <param name="mode">要设置的捕获模式</param>
|
||||
/// <returns>操作结果,成功返回true,失败返回错误信息</returns>
|
||||
public async ValueTask<Result<bool>> SetMode(CaptureMode mode)
|
||||
public async ValueTask<Result<bool>> SetMode(byte channelNum, CaptureMode mode)
|
||||
{
|
||||
if (channelNum > 0x0F)
|
||||
{
|
||||
return new(new ArgumentException($"Channel Num can't be over 16, but receive num: {channelNum}"));
|
||||
}
|
||||
|
||||
UInt32 data = ((UInt32)mode);
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Mode, data, this.timeout);
|
||||
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Mode + channelNum, data, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Failed to set mode: {ret.Error}");
|
||||
|
|
|
@ -1649,18 +1649,24 @@ export class DebuggerClient {
|
|||
|
||||
/**
|
||||
* 设置捕获模式
|
||||
* @param channelNum (optional)
|
||||
* @param mode (optional)
|
||||
*/
|
||||
setMode(mode: string): Promise<boolean> {
|
||||
let url_ = this.baseUrl + "/api/Debugger/SetMode";
|
||||
setMode(channelNum: number | undefined, mode: CaptureMode | undefined): Promise<boolean> {
|
||||
let url_ = this.baseUrl + "/api/Debugger/SetMode?";
|
||||
if (channelNum === null)
|
||||
throw new Error("The parameter 'channelNum' cannot be null.");
|
||||
else if (channelNum !== undefined)
|
||||
url_ += "channelNum=" + encodeURIComponent("" + channelNum) + "&";
|
||||
if (mode === null)
|
||||
throw new Error("The parameter 'mode' cannot be null.");
|
||||
else if (mode !== undefined)
|
||||
url_ += "mode=" + encodeURIComponent("" + mode) + "&";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
const content_ = JSON.stringify(mode);
|
||||
|
||||
let options_: RequestInit = {
|
||||
body: content_,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
};
|
||||
|
@ -5508,6 +5514,15 @@ export interface IArgumentException extends ISystemException {
|
|||
paramName?: string | undefined;
|
||||
}
|
||||
|
||||
/** 信号捕获模式枚举 */
|
||||
export enum CaptureMode {
|
||||
None = 0,
|
||||
Logic0 = 1,
|
||||
Logic1 = 2,
|
||||
Rise = 3,
|
||||
Fall = 4,
|
||||
}
|
||||
|
||||
/** 逻辑分析仪运行状态枚举 */
|
||||
export enum CaptureStatus {
|
||||
None = 0,
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
LogicAnalyzerClient,
|
||||
NetConfigClient,
|
||||
OscilloscopeApiClient,
|
||||
DebuggerClient,
|
||||
} from "@/APIClient";
|
||||
|
||||
// 支持的客户端类型联合类型
|
||||
|
@ -28,7 +29,8 @@ type SupportedClient =
|
|||
| LogicAnalyzerClient
|
||||
| UDPClient
|
||||
| NetConfigClient
|
||||
| OscilloscopeApiClient;
|
||||
| OscilloscopeApiClient
|
||||
| DebuggerClient;
|
||||
|
||||
export class AuthManager {
|
||||
// 存储token到localStorage
|
||||
|
@ -168,6 +170,10 @@ export class AuthManager {
|
|||
public static createAuthenticatedOscilloscopeApiClient(): OscilloscopeApiClient {
|
||||
return AuthManager.createAuthenticatedClient(OscilloscopeApiClient);
|
||||
}
|
||||
|
||||
public static createAuthenticatedDebuggerClient(): DebuggerClient {
|
||||
return AuthManager.createAuthenticatedClient(DebuggerClient);
|
||||
}
|
||||
|
||||
// 登录函数
|
||||
public static async login(
|
||||
|
|
|
@ -1,11 +1,321 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="card m-5 bg-base-200 shadow-2xl">
|
||||
<WaveformDisplay />
|
||||
<div class="card-body">
|
||||
<h2 class="card-title flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<Zap class="w-5 h-5" />
|
||||
调试器波形捕获
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-error"
|
||||
@click="handleDeleteData"
|
||||
:disabled="!captureData"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</h2>
|
||||
<WaveformDisplay :data="captureData">
|
||||
<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="{
|
||||
'from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 focus:ring-blue-300':
|
||||
!isCapturing,
|
||||
'from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 focus:ring-red-300':
|
||||
isCapturing,
|
||||
}"
|
||||
@click="isCapturing ? stopCapture() : startCapture()"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<template v-if="isCapturing">
|
||||
<Square class="w-5 h-5" />
|
||||
停止捕获
|
||||
</template>
|
||||
<template v-else>
|
||||
<Play class="w-5 h-5" />
|
||||
开始捕获
|
||||
</template>
|
||||
</span>
|
||||
</button>
|
||||
</WaveformDisplay>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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="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>
|
||||
<div class="stat-desc">当前激活通道</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">采样率</div>
|
||||
<div class="stat-value text-info">5MHz</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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<input
|
||||
v-model="ch.name"
|
||||
class="input input-bordered w-full"
|
||||
:placeholder="`通道${idx + 1}`"
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="ch.visible"
|
||||
class="toggle toggle-primary"
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
v-model="ch.color"
|
||||
class="w-8 h-8 rounded border-2 border-base-300 cursor-pointer"
|
||||
/>
|
||||
<select
|
||||
v-model="ch.trigger"
|
||||
class="select select-bordered w-full"
|
||||
>
|
||||
<option
|
||||
v-for="mode in triggerModes"
|
||||
:key="mode.value"
|
||||
:value="mode.value"
|
||||
>
|
||||
{{ mode.label }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="32"
|
||||
v-model.number="ch.width"
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
<button class="btn btn-error" @click="removeChannel(idx)">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
<!-- 添加通道按钮 -->
|
||||
<div class="flex justify-center mt-2">
|
||||
<button class="btn btn-primary w-100" @click="addChannel">
|
||||
添加通道
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import WaveformDisplay from '@/components/WaveformDisplay/WaveformDisplay.vue';
|
||||
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
import { CaptureMode } from "@/APIClient";
|
||||
import { useAlertStore } from "@/components/Alert";
|
||||
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 { Play, Square, Zap } from "lucide-vue-next";
|
||||
import { ref } from "vue";
|
||||
|
||||
interface DebugChannel {
|
||||
name: string;
|
||||
visible: boolean;
|
||||
color: string;
|
||||
trigger: CaptureMode;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const triggerModes = [
|
||||
{ value: CaptureMode.None, label: "x (无关)" },
|
||||
{ value: CaptureMode.Logic0, label: "0 (低电平)" },
|
||||
{ value: CaptureMode.Logic1, label: "1 (高电平)" },
|
||||
{ value: CaptureMode.Rise, label: "↑ (上升沿)" },
|
||||
{ value: CaptureMode.Fall, label: "↓ (下降沿)" },
|
||||
];
|
||||
|
||||
const channels = useLocalStorage<DebugChannel[]>("debugger-channels", []);
|
||||
const captureData = ref<LogicDataType>();
|
||||
const alert = useRequiredInjection(useAlertStore);
|
||||
|
||||
const isCapturing = ref(false);
|
||||
|
||||
async function startCapture() {
|
||||
if (channels.value.length === 0) {
|
||||
alert.error("请至少添加一个通道");
|
||||
return;
|
||||
}
|
||||
|
||||
isCapturing.value = true;
|
||||
const client = AuthManager.createAuthenticatedDebuggerClient();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function addChannel() {
|
||||
if (channels.value.length >= 16) {
|
||||
alert.error("最多只能添加16个通道");
|
||||
return;
|
||||
}
|
||||
|
||||
channels.value.push({
|
||||
name: `CH${channels.value.length + 1}`,
|
||||
visible: true,
|
||||
color: "#00bcd4",
|
||||
trigger: CaptureMode.None,
|
||||
width: 1,
|
||||
});
|
||||
}
|
||||
|
||||
function removeChannel(idx: number) {
|
||||
channels.value.splice(idx, 1);
|
||||
}
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue