feat: 完成debugger前后端交互
This commit is contained in:
parent
23d4459406
commit
3da0f284f3
|
@ -58,15 +58,20 @@ public class DebuggerController : ControllerBase
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[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
|
try
|
||||||
{
|
{
|
||||||
var debugger = GetDebugger();
|
var debugger = GetDebugger();
|
||||||
if (debugger == null)
|
if (debugger == null)
|
||||||
return BadRequest("用户未绑定有效的实验板");
|
return BadRequest("用户未绑定有效的实验板");
|
||||||
|
|
||||||
var result = await debugger.SetMode(mode);
|
var result = await debugger.SetMode((byte)channelNum, mode);
|
||||||
if (!result.IsSuccessful)
|
if (!result.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"设置捕获模式失败: {result.Error}");
|
logger.Error($"设置捕获模式失败: {result.Error}");
|
||||||
|
|
|
@ -43,12 +43,12 @@ class DebuggerCmd
|
||||||
/// 启动触发器命令
|
/// 启动触发器命令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const UInt32 Start = 0xFFFF_FFFF;
|
public const UInt32 Start = 0xFFFF_FFFF;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 刷新命令
|
/// 刷新命令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const UInt32 Fresh = 0x0000_0000;
|
public const UInt32 Fresh = 0x0000_0000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 清除信号标志命令
|
/// 清除信号标志命令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -119,12 +119,18 @@ public class DebuggerClient
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置信号捕获模式
|
/// 设置信号捕获模式
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="channelNum">要设置的通道</param>
|
||||||
/// <param name="mode">要设置的捕获模式</param>
|
/// <param name="mode">要设置的捕获模式</param>
|
||||||
/// <returns>操作结果,成功返回true,失败返回错误信息</returns>
|
/// <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);
|
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)
|
if (!ret.IsSuccessful)
|
||||||
{
|
{
|
||||||
logger.Error($"Failed to set mode: {ret.Error}");
|
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> {
|
setMode(channelNum: number | undefined, mode: CaptureMode | undefined): Promise<boolean> {
|
||||||
let url_ = this.baseUrl + "/api/Debugger/SetMode";
|
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(/[?&]$/, "");
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
const content_ = JSON.stringify(mode);
|
|
||||||
|
|
||||||
let options_: RequestInit = {
|
let options_: RequestInit = {
|
||||||
body: content_,
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json"
|
"Accept": "application/json"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -5508,6 +5514,15 @@ export interface IArgumentException extends ISystemException {
|
||||||
paramName?: string | undefined;
|
paramName?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 信号捕获模式枚举 */
|
||||||
|
export enum CaptureMode {
|
||||||
|
None = 0,
|
||||||
|
Logic0 = 1,
|
||||||
|
Logic1 = 2,
|
||||||
|
Rise = 3,
|
||||||
|
Fall = 4,
|
||||||
|
}
|
||||||
|
|
||||||
/** 逻辑分析仪运行状态枚举 */
|
/** 逻辑分析仪运行状态枚举 */
|
||||||
export enum CaptureStatus {
|
export enum CaptureStatus {
|
||||||
None = 0,
|
None = 0,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
LogicAnalyzerClient,
|
LogicAnalyzerClient,
|
||||||
NetConfigClient,
|
NetConfigClient,
|
||||||
OscilloscopeApiClient,
|
OscilloscopeApiClient,
|
||||||
|
DebuggerClient,
|
||||||
} from "@/APIClient";
|
} from "@/APIClient";
|
||||||
|
|
||||||
// 支持的客户端类型联合类型
|
// 支持的客户端类型联合类型
|
||||||
|
@ -28,7 +29,8 @@ type SupportedClient =
|
||||||
| LogicAnalyzerClient
|
| LogicAnalyzerClient
|
||||||
| UDPClient
|
| UDPClient
|
||||||
| NetConfigClient
|
| NetConfigClient
|
||||||
| OscilloscopeApiClient;
|
| OscilloscopeApiClient
|
||||||
|
| DebuggerClient;
|
||||||
|
|
||||||
export class AuthManager {
|
export class AuthManager {
|
||||||
// 存储token到localStorage
|
// 存储token到localStorage
|
||||||
|
@ -168,6 +170,10 @@ export class AuthManager {
|
||||||
public static createAuthenticatedOscilloscopeApiClient(): OscilloscopeApiClient {
|
public static createAuthenticatedOscilloscopeApiClient(): OscilloscopeApiClient {
|
||||||
return AuthManager.createAuthenticatedClient(OscilloscopeApiClient);
|
return AuthManager.createAuthenticatedClient(OscilloscopeApiClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static createAuthenticatedDebuggerClient(): DebuggerClient {
|
||||||
|
return AuthManager.createAuthenticatedClient(DebuggerClient);
|
||||||
|
}
|
||||||
|
|
||||||
// 登录函数
|
// 登录函数
|
||||||
public static async login(
|
public static async login(
|
||||||
|
|
|
@ -1,11 +1,321 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="card m-5 bg-base-200 shadow-2xl">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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