add: 为逻辑分析仪添加了深度、预存储深度、通道组设置

This commit is contained in:
alivender 2025-07-31 13:14:23 +08:00
parent 3257a68407
commit 6b701658d1
8 changed files with 1012 additions and 718 deletions

View File

@ -45,7 +45,18 @@ public class LogicAnalyzerController : ControllerBase
/// 全局触发模式
/// </summary>
public GlobalCaptureMode GlobalMode { get; set; }
/// <summary>
/// 捕获深度
/// </summary>
public int CaptureLength { get; set; } = 2048 * 32;
/// <summary>
/// 预采样深度
/// </summary>
public int PreCaptureLength { get; set; } = 2048;
/// <summary>
/// 有效通道
/// </summary>
public AnalyzerChannelDiv ChannelDiv { get; set; } = AnalyzerChannelDiv.EIGHT;
/// <summary>
/// 信号触发配置列表
/// </summary>
@ -77,7 +88,7 @@ public class LogicAnalyzerController : ControllerBase
return null;
var board = boardRet.Value.Value;
return new Analyzer(board.IpAddr, board.Port, 2);
return new Analyzer(board.IpAddr, board.Port, 0);
}
catch (Exception ex)
{
@ -208,8 +219,8 @@ public class LogicAnalyzerController : ControllerBase
{
try
{
if (signalIndex < 0 || signalIndex > 7)
return BadRequest("信号索引必须在0-7之间");
if (signalIndex < 0 || signalIndex > 31)
return BadRequest("信号索引必须在0-31之间");
var analyzer = GetAnalyzer();
if (analyzer == null)
@ -231,6 +242,48 @@ public class LogicAnalyzerController : ControllerBase
}
}
/// <summary>
/// 设置深度、预采样深度、有效通道
/// </summary>
/// <param name="capture_length">深度</param>
/// <param name="pre_capture_length">预采样深度</param>
/// <param name="channel_div">有效通道(0-[1],1-[2],2-[4],3-[8],4-[16],5-[32])</param>
/// <returns>操作结果</returns>
[HttpPost("SetCaptureParams")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetCaptureParams(int capture_length, int pre_capture_length, AnalyzerChannelDiv channel_div)
{
try
{
if (capture_length < 0 || capture_length > 2048*32)
return BadRequest("采样深度设置错误");
if (pre_capture_length < 0 || pre_capture_length >= capture_length)
return BadRequest("预采样深度必须小于捕获深度");
var analyzer = GetAnalyzer();
if (analyzer == null)
return BadRequest("用户未绑定有效的实验板");
var result = await analyzer.SetCaptureParams(capture_length, pre_capture_length, channel_div);
if (!result.IsSuccessful)
{
logger.Error($"设置深度、预采样深度、有效通道失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置深度、预采样深度、有效通道失败");
}
return Ok(result.Value);
}
catch (Exception ex)
{
logger.Error(ex, "设置深度、预采样深度、有效通道失败时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 批量配置捕获参数
/// </summary>
@ -264,8 +317,8 @@ public class LogicAnalyzerController : ControllerBase
// 设置信号触发模式
foreach (var signalConfig in config.SignalConfigs)
{
if (signalConfig.SignalIndex < 0 || signalConfig.SignalIndex > 7)
return BadRequest($"信号索引{signalConfig.SignalIndex}超出范围0-7");
if (signalConfig.SignalIndex < 0 || signalConfig.SignalIndex > 31)
return BadRequest($"信号索引{signalConfig.SignalIndex}超出范围0-31");
var signalResult = await analyzer.SetSignalTrigMode(
signalConfig.SignalIndex, signalConfig.Operator, signalConfig.Value);
@ -276,6 +329,14 @@ public class LogicAnalyzerController : ControllerBase
$"设置信号{signalConfig.SignalIndex}触发模式失败");
}
}
// 设置深度、预采样深度、有效通道
var paramsResult = await analyzer.SetCaptureParams(
config.CaptureLength, config.PreCaptureLength, config.ChannelDiv);
if (!paramsResult.IsSuccessful)
{
logger.Error($"设置深度、预采样深度、有效通道失败: {paramsResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "设置深度、预采样深度、有效通道失败");
}
return Ok(true);
}
@ -330,7 +391,7 @@ public class LogicAnalyzerController : ControllerBase
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetCaptureData()
public async Task<IActionResult> GetCaptureData(int capture_length = 2048 * 32)
{
try
{
@ -338,7 +399,7 @@ public class LogicAnalyzerController : ControllerBase
if (analyzer == null)
return BadRequest("用户未绑定有效的实验板");
var result = await analyzer.ReadCaptureData();
var result = await analyzer.ReadCaptureData(capture_length);
if (!result.IsSuccessful)
{
logger.Error($"读取捕获数据失败: {result.Error}");

View File

@ -46,17 +46,26 @@ static class AnalyzerAddr
/// 111 SOME NUMBER <br/>
/// </summary>
public static readonly UInt32[] SIGNAL_TRIG_MODE = {
BASE + 0x0000_0010,
BASE + 0x0000_0011,
BASE + 0x0000_0012,
BASE + 0x0000_0013,
BASE + 0x0000_0014,
BASE + 0x0000_0015,
BASE + 0x0000_0016,
BASE + 0x0000_0017,
BASE + 0x0000_0010, BASE + 0x0000_0011,
BASE + 0x0000_0012, BASE + 0x0000_0013,
BASE + 0x0000_0014, BASE + 0x0000_0015,
BASE + 0x0000_0016, BASE + 0x0000_0017,
BASE + 0x0000_0018, BASE + 0x0000_0019,
BASE + 0x0000_001A, BASE + 0x0000_001B,
BASE + 0x0000_001C, BASE + 0x0000_001D,
BASE + 0x0000_001E, BASE + 0x0000_001F,
BASE + 0x0000_0020, BASE + 0x0000_0021,
BASE + 0x0000_0022, BASE + 0x0000_0023,
BASE + 0x0000_0024, BASE + 0x0000_0025,
BASE + 0x0000_0026, BASE + 0x0000_0027,
BASE + 0x0000_0028, BASE + 0x0000_0029,
BASE + 0x0000_002A, BASE + 0x0000_002B,
BASE + 0x0000_002C, BASE + 0x0000_002D,
BASE + 0x0000_002E, BASE + 0x0000_002F
};
public const UInt32 LOAD_NUM_ADDR = BASE + 0x0000_0002;
public const UInt32 PRE_LOAD_NUM_ADDR = BASE + 0x0000_0003;
public const UInt32 CAHNNEL_DIV_ADDR = BASE + 0x0000_0004;
public const UInt32 DMA1_START_WRITE_ADDR = DMA1_BASE + 0x0000_0012;
public const UInt32 DMA1_END_WRITE_ADDR = DMA1_BASE + 0x0000_0013;
public const UInt32 DMA1_CAPTURE_CTRL_ADDR = DMA1_BASE + 0x0000_0014;
@ -198,6 +207,37 @@ public enum SignalValue : byte
SomeNumber = 0b111 // SOME NUMBER
}
/// <summary>
/// 逻辑分析仪有效通道数
/// </summary>
public enum AnalyzerChannelDiv
{
/// <summary>
/// 1路
/// </summary>
ONE = 0x0000_0000,
/// <summary>
/// 2路
/// </summary>
TWO = 0x0000_0001,
/// <summary>
/// 4路
/// </summary>
FOUR = 0x0000_0002,
/// <summary>
/// 8路
/// </summary>
EIGHT = 0x0000_0003,
/// <summary>
/// 16路
/// </summary>
XVI = 0x0000_0004,
/// <summary>
/// 32路
/// </summary>
XXXII = 0x0000_0005
}
/// <summary>
/// FPGA逻辑分析仪客户端用于控制FPGA上的逻辑分析仪模块进行信号捕获和分析
/// </summary>
@ -239,58 +279,6 @@ public class Analyzer
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetCaptureMode(bool captureOn, bool force)
{
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.LOAD_NUM_ADDR, AnalyzerAddr.CAPTURE_DATA_LENGTH - 1, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set LOAD_NUM_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to LOAD_NUM_ADDR returned false");
return new(new Exception("Failed to set LOAD_NUM_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.PRE_LOAD_NUM_ADDR, AnalyzerAddr.CAPTURE_DATA_PRELOAD - 1, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set PRE_LOAD_NUM_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to PRE_LOAD_NUM_ADDR returned false");
return new(new Exception("Failed to set PRE_LOAD_NUM_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA1_START_WRITE_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set DMA1_START_WRITE_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DMA1_START_WRITE_ADDR returned false");
return new(new Exception("Failed to set DMA1_START_WRITE_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA1_END_WRITE_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR + AnalyzerAddr.CAPTURE_DATA_LENGTH - 1, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set DMA1_END_WRITE_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DMA1_END_WRITE_ADDR returned false");
return new(new Exception("Failed to set DMA1_END_WRITE_ADDR"));
}
}
// 构造寄存器值
UInt32 value = 0;
if (captureOn) value |= 1 << 0;
@ -380,7 +368,7 @@ public class Analyzer
return new(new ArgumentException($"Signal index must be 0~{AnalyzerAddr.SIGNAL_TRIG_MODE.Length}"));
// 计算模式值: [2:0] 信号值, [5:3] 操作符
UInt32 mode = ((UInt32)op << 3) | (UInt32) val;
UInt32 mode = ((UInt32)op << 3) | (UInt32)val;
var addr = AnalyzerAddr.SIGNAL_TRIG_MODE[signalIndex];
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, addr, mode, this.timeout);
@ -397,17 +385,96 @@ public class Analyzer
return true;
}
/// <summary>
/// 设置逻辑分析仪的深度、预采样深度、有效通道
/// </summary>
/// <param name="capture_length">深度</param>
/// <param name="pre_capture_length">预采样深度</param>
/// <param name="channel_div">有效通道(0-[1],1-[2],2-[4],3-[8],4-[16],5-[32])</param>
/// <returns>操作结果成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> SetCaptureParams(int capture_length, int pre_capture_length, AnalyzerChannelDiv channel_div)
{
if (capture_length == 0) capture_length = 1;
if (pre_capture_length == 0) pre_capture_length = 1;
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.LOAD_NUM_ADDR, (UInt32)(capture_length - 1), this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set LOAD_NUM_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to LOAD_NUM_ADDR returned false");
return new(new Exception("Failed to set LOAD_NUM_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.PRE_LOAD_NUM_ADDR, (UInt32)(pre_capture_length - 1), this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set PRE_LOAD_NUM_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to PRE_LOAD_NUM_ADDR returned false");
return new(new Exception("Failed to set PRE_LOAD_NUM_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA1_START_WRITE_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set DMA1_START_WRITE_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DMA1_START_WRITE_ADDR returned false");
return new(new Exception("Failed to set DMA1_START_WRITE_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.DMA1_END_WRITE_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR + (UInt32)(capture_length - 1), this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set DMA1_END_WRITE_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to DMA1_END_WRITE_ADDR returned false");
return new(new Exception("Failed to set DMA1_END_WRITE_ADDR"));
}
}
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, AnalyzerAddr.CAHNNEL_DIV_ADDR, (UInt32)channel_div, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set CAHNNEL_DIV_ADDR: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error("WriteAddr to CAHNNEL_DIV_ADDR returned false");
return new(new Exception("Failed to set CAHNNEL_DIV_ADDR"));
}
}
return true;
}
/// <summary>
/// 读取捕获的波形数据
/// </summary>
/// <returns>操作结果成功返回byte[],否则返回异常信息</returns>
public async ValueTask<Result<byte[]>> ReadCaptureData()
public async ValueTask<Result<byte[]>> ReadCaptureData(int capture_length = 2048 * 32)
{
var ret = await UDPClientPool.ReadAddr4BytesAsync(
this.ep,
this.taskID,
AnalyzerAddr.STORE_OFFSET_ADDR,
AnalyzerAddr.CAPTURE_DATA_LENGTH,
capture_length,
this.timeout
);
if (!ret.IsSuccessful)
@ -416,7 +483,7 @@ public class Analyzer
return new(ret.Error);
}
var data = ret.Value;
if (data == null || data.Length != AnalyzerAddr.CAPTURE_DATA_LENGTH * 4)
if (data == null || data.Length != capture_length * 4)
{
logger.Error($"Capture data length mismatch: {data?.Length}");
return new(new Exception("Capture data length mismatch"));

View File

@ -3678,6 +3678,92 @@ export class LogicAnalyzerClient {
return Promise.resolve<boolean>(null as any);
}
/**
*
* @param capture_length (optional)
* @param pre_capture_length (optional)
* @param channel_div (optional) (0-[1],1-[2],2-[4],3-[8],4-[16],5-[32])
* @return
*/
setCaptureParams(capture_length: number | undefined, pre_capture_length: number | undefined, channel_div: AnalyzerChannelDiv | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/LogicAnalyzer/SetCaptureParams?";
if (capture_length === null)
throw new Error("The parameter 'capture_length' cannot be null.");
else if (capture_length !== undefined)
url_ += "capture_length=" + encodeURIComponent("" + capture_length) + "&";
if (pre_capture_length === null)
throw new Error("The parameter 'pre_capture_length' cannot be null.");
else if (pre_capture_length !== undefined)
url_ += "pre_capture_length=" + encodeURIComponent("" + pre_capture_length) + "&";
if (channel_div === null)
throw new Error("The parameter 'channel_div' cannot be null.");
else if (channel_div !== undefined)
url_ += "channel_div=" + encodeURIComponent("" + channel_div) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processSetCaptureParams(_response);
});
}
protected processSetCaptureParams(response: AxiosResponse): Promise<boolean> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
let result200: any = null;
let resultData200 = _responseText;
result200 = resultData200 !== undefined ? resultData200 : <any>null;
return Promise.resolve<boolean>(result200);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ProblemDetails.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<boolean>(null as any);
}
/**
*
* @param config
@ -3827,10 +3913,15 @@ export class LogicAnalyzerClient {
/**
*
* @param capture_length (optional)
* @return Base64编码
*/
getCaptureData( cancelToken?: CancelToken): Promise<string> {
let url_ = this.baseUrl + "/api/LogicAnalyzer/GetCaptureData";
getCaptureData(capture_length: number | undefined, cancelToken?: CancelToken): Promise<string> {
let url_ = this.baseUrl + "/api/LogicAnalyzer/GetCaptureData?";
if (capture_length === null)
throw new Error("The parameter 'capture_length' cannot be null.");
else if (capture_length !== undefined)
url_ += "capture_length=" + encodeURIComponent("" + capture_length) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
@ -7237,10 +7328,26 @@ export enum SignalValue {
SomeNumber = 7,
}
/** 逻辑分析仪有效通道数 */
export enum AnalyzerChannelDiv {
ONE = 0,
TWO = 1,
FOUR = 2,
EIGHT = 3,
XVI = 4,
XXXII = 5,
}
/** 捕获配置 */
export class CaptureConfig implements ICaptureConfig {
/** 全局触发模式 */
globalMode!: GlobalCaptureMode;
/** 捕获深度 */
captureLength!: number;
/** 预采样深度 */
preCaptureLength!: number;
/** 有效通道 */
channelDiv!: AnalyzerChannelDiv;
/** 信号触发配置列表 */
signalConfigs!: SignalTriggerConfig[];
@ -7259,6 +7366,9 @@ export class CaptureConfig implements ICaptureConfig {
init(_data?: any) {
if (_data) {
this.globalMode = _data["globalMode"];
this.captureLength = _data["captureLength"];
this.preCaptureLength = _data["preCaptureLength"];
this.channelDiv = _data["channelDiv"];
if (Array.isArray(_data["signalConfigs"])) {
this.signalConfigs = [] as any;
for (let item of _data["signalConfigs"])
@ -7277,6 +7387,9 @@ export class CaptureConfig implements ICaptureConfig {
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["globalMode"] = this.globalMode;
data["captureLength"] = this.captureLength;
data["preCaptureLength"] = this.preCaptureLength;
data["channelDiv"] = this.channelDiv;
if (Array.isArray(this.signalConfigs)) {
data["signalConfigs"] = [];
for (let item of this.signalConfigs)
@ -7290,6 +7403,12 @@ export class CaptureConfig implements ICaptureConfig {
export interface ICaptureConfig {
/** 全局触发模式 */
globalMode: GlobalCaptureMode;
/** 捕获深度 */
captureLength: number;
/** 预采样深度 */
preCaptureLength: number;
/** 有效通道 */
channelDiv: AnalyzerChannelDiv;
/** 信号触发配置列表 */
signalConfigs: SignalTriggerConfig[];
}

View File

@ -9,6 +9,7 @@ import {
SignalOperator,
SignalTriggerConfig,
SignalValue,
AnalyzerChannelDiv,
} from "@/APIClient";
import { AuthManager } from "@/utils/AuthManager";
import { useAlertStore } from "@/components/Alert";
@ -65,6 +66,39 @@ const signalValues = [
{ value: SignalValue.SomeNumber, label: "#" },
];
// 通道组选项
const channelDivOptions = [
{ value: 1, label: "1通道", description: "启用1个通道 (CH0)" },
{ value: 2, label: "2通道", description: "启用2个通道 (CH0-CH1)" },
{ value: 4, label: "4通道", description: "启用4个通道 (CH0-CH3)" },
{ value: 8, label: "8通道", description: "启用8个通道 (CH0-CH7)" },
{ value: 16, label: "16通道", description: "启用16个通道 (CH0-CH15)" },
{ value: 32, label: "32通道", description: "启用32个通道 (CH0-CH31)" },
];
// 捕获深度选项
const captureLengthOptions = [
{ value: 256, label: "256" },
{ value: 512, label: "512" },
{ value: 1024, label: "1K" },
{ value: 2048, label: "2K" },
{ value: 4096, label: "4K" },
{ value: 8192, label: "8K" },
{ value: 16384, label: "16K" },
{ value: 32768, label: "32K" },
];
// 预捕获深度选项
const preCaptureLengthOptions = [
{ value: 0, label: "0" },
{ value: 16, label: "16" },
{ value: 32, label: "32" },
{ value: 64, label: "64" },
{ value: 128, label: "128" },
{ value: 256, label: "256" },
{ value: 512, label: "512" },
];
// 默认颜色数组
const defaultColors = [
"#FF5733",
@ -78,7 +112,7 @@ const defaultColors = [
];
// 添加逻辑分析仪频率常量
const LOGIC_ANALYZER_FREQUENCY = 5_000_000; // 5MHz
const LOGIC_ANALYZER_FREQUENCY = 125_000_000; // 125MHz
const SAMPLE_PERIOD_NS = 1_000_000_000 / LOGIC_ANALYZER_FREQUENCY; // 采样周期,单位:纳秒
const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
@ -91,22 +125,25 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
// 触发设置相关状态
const currentGlobalMode = ref<GlobalCaptureMode>(GlobalCaptureMode.AND);
const currentChannelDiv = ref<number>(8); // 默认启用8个通道
const captureLength = ref<number>(1024); // 捕获深度默认1024
const preCaptureLength = ref<number>(0); // 预捕获深度默认0
const isApplying = ref(false);
const isCapturing = ref(false); // 添加捕获状态标识
// 通道配置
const channels = reactive<Channel[]>(
Array.from({ length: 8 }, (_, index) => ({
enabled: false,
Array.from({ length: 32 }, (_, index) => ({
enabled: index < 8, // 默认启用前8个通道
label: `CH${index}`,
color: defaultColors[index],
color: defaultColors[index % defaultColors.length], // 使用模运算避免数组越界
})),
);
// 8个信号通道的配置
// 32个信号通道的配置
const signalConfigs = reactive<SignalTriggerConfig[]>(
Array.from(
{ length: 8 },
{ length: 32 },
(_, index) =>
new SignalTriggerConfig({
signalIndex: index,
@ -131,101 +168,52 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
channels.filter((channel) => channel.enabled),
);
const enableAllChannels = () => {
channels.forEach((channel) => {
channel.enabled = true;
});
// 转换通道数字到枚举值
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;
}
};
const disableAllChannels = () => {
// 设置通道组
const setChannelDiv = (channelCount: number) => {
// 验证通道数量是否有效
if (!channelDivOptions.find(option => option.value === channelCount)) {
console.error(`无效的通道组设置: ${channelCount}`);
return;
}
currentChannelDiv.value = channelCount;
// 禁用所有通道
channels.forEach((channel) => {
channel.enabled = false;
});
// 启用指定数量的通道从CH0开始
for (let i = 0; i < channelCount && i < channels.length; i++) {
channels[i].enabled = true;
}
const option = channelDivOptions.find(opt => opt.value === channelCount);
alert?.success(`已设置为${option?.label}`, 2000);
};
const setGlobalMode = async (mode: GlobalCaptureMode) => {
// 检查是否有其他操作正在进行
if (operationMutex.isLocked()) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
return;
}
const release = await operationMutex.acquire();
try {
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
const success = await client.setGlobalTrigMode(mode);
if (success) {
currentGlobalMode.value = mode;
alert?.success(
`全局触发模式已设置为 ${globalModes.find((m) => m.value === mode)?.label}`,
3000,
);
} else {
throw new Error("设置失败");
}
} catch (error) {
console.error("设置全局触发模式失败:", error);
alert?.error("设置全局触发模式失败", 3000);
} finally {
release();
}
};
const applyConfiguration = async () => {
// 检查是否有其他操作正在进行
if (operationMutex.isLocked()) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
return;
}
const release = await operationMutex.acquire();
isApplying.value = true;
try {
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
// 准备配置数据 - 只包含启用的通道
const enabledSignals = signalConfigs.filter(
(signal, index) => channels[index].enabled,
);
const config = new CaptureConfig({
globalMode: currentGlobalMode.value,
signalConfigs: enabledSignals,
});
// 发送配置
const success = await client.configureCapture(config);
if (success) {
const enabledChannelCount = channels.filter(
(ch) => ch.enabled,
).length;
alert?.success(
`配置已成功应用,启用了 ${enabledChannelCount} 个通道和触发条件`,
3000,
);
} else {
throw new Error("应用配置失败");
}
} catch (error) {
console.error("应用配置失败:", error);
alert?.error("应用配置失败,请检查设备连接", 3000);
} finally {
isApplying.value = false;
release();
}
const setGlobalMode = (mode: GlobalCaptureMode) => {
currentGlobalMode.value = mode;
const modeOption = globalModes.find((m) => m.value === mode);
alert?.info(`全局触发模式已设置为 ${modeOption?.label}`, 2000);
};
const resetConfiguration = () => {
currentGlobalMode.value = GlobalCaptureMode.AND;
channels.forEach((channel, index) => {
channel.enabled = false;
channel.label = `CH${index}`;
channel.color = defaultColors[index];
});
currentChannelDiv.value = 8; // 重置为默认的8通道
setChannelDiv(8); // 重置为默认的8通道
signalConfigs.forEach((signal) => {
signal.operator = SignalOperator.Equal;
@ -243,51 +231,223 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
const getCaptureData = async () => {
try {
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
// 3. 获取捕获数据
const base64Data = await client.getCaptureData();
// 获取捕获数据,使用当前设置的捕获长度
const base64Data = await client.getCaptureData(captureLength.value);
// 4. 将base64数据转换为bytes
// 将base64数据转换为bytes
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// 5. 解析数据为8个通道的数字信号
const sampleCount = bytes.length;
const timeStepNs = SAMPLE_PERIOD_NS; // 每个采样点间隔200ns (1/5MHz)
// 根据当前通道数量解析数据
const channelCount = currentChannelDiv.value;
const timeStepNs = SAMPLE_PERIOD_NS;
let sampleCount: number;
let x: number[];
let y: number[][];
// 创建时间轴(转换为合适的单位)
const x = Array.from(
{ length: sampleCount },
(_, i) => (i * timeStepNs) / 1000,
); // 转换为微秒
// 创建8个通道的数据
const y: number[][] = Array.from(
{ length: 8 },
() => new Array(sampleCount),
);
// 解析每个字节的8个位到对应通道
for (let i = 0; i < sampleCount; i++) {
const byte = bytes[i];
for (let channel = 0; channel < 8; channel++) {
// bit0对应ch0, bit1对应ch1, ..., bit7对应ch7
y[channel][i] = (byte >> channel) & 1;
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),
);
// 解析数据每个字节的8个位对应8个时间单位
for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
const byte = bytes[byteIndex];
for (let bitIndex = 0; bitIndex < 8; bitIndex++) {
const timeIndex = byteIndex * 8 + bitIndex;
y[0][timeIndex] = (byte >> bitIndex) & 1;
}
}
} 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),
);
// 解析数据每个字节的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++) {
const byte = bytes[byteIndex];
for (let timeUnit = 0; timeUnit < 4; timeUnit++) {
const timeIndex = byteIndex * 4 + timeUnit;
const bitOffset = timeUnit * 2;
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),
);
// 解析数据每个字节的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++) {
y[channel][timeIndex2] = (byte >> (channel + 4)) & 1;
}
}
} 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),
);
// 解析每个字节的8个位到对应通道
for (let i = 0; i < sampleCount; i++) {
const byte = bytes[i];
for (let channel = 0; channel < 8; channel++) {
// bit0对应ch0, bit1对应ch1, ..., bit7对应ch7
y[channel][i] = (byte >> channel) & 1;
}
}
} 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),
);
// 解析数据每2个字节为一个时间单位
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
const byteIndex = timeIndex * 2;
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;
}
}
} 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),
);
// 解析数据每4个字节为一个时间单位
for (let timeIndex = 0; timeIndex < sampleCount; timeIndex++) {
const byteIndex = timeIndex * 4;
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;
}
}
} else {
throw new Error(`不支持的通道数量: ${channelCount}`);
}
// 6. 设置逻辑数据
// 设置逻辑数据
const logicData: LogicDataType = {
x,
y,
xUnit: "us", // 改为微秒单位
xUnit: "us", // 微秒单位
};
setLogicData(logicData);
} catch (error) {
console.error("获取捕获数据失败:", error);
alert?.error("获取捕获数据失败", 3000);
}
};
@ -303,7 +463,45 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
try {
const client = AuthManager.createAuthenticatedLogicAnalyzerClient();
// 1. 设置捕获模式为开始捕获
// 1. 先应用配置
alert?.info("正在应用配置...", 2000);
// 准备配置数据 - 包含所有32个通道未启用的通道设置为默认值
const allSignals = signalConfigs.map((signal, index) => {
if (channels[index].enabled) {
// 启用的通道使用用户配置的触发条件
return signal;
} else {
// 未启用的通道设置为默认触发条件
return new SignalTriggerConfig({
signalIndex: index,
operator: SignalOperator.Equal,
value: SignalValue.NotCare,
});
}
});
const config = new CaptureConfig({
globalMode: currentGlobalMode.value,
channelDiv: getChannelDivEnum(currentChannelDiv.value),
captureLength: captureLength.value,
preCaptureLength: preCaptureLength.value,
signalConfigs: allSignals,
});
// 发送配置
const configSuccess = await client.configureCapture(config);
if (!configSuccess) {
throw new Error("配置应用失败");
}
const enabledChannelCount = channels.filter((ch) => ch.enabled).length;
alert?.success(
`配置已应用,启用了 ${enabledChannelCount} 个通道,捕获深度: ${captureLength.value}`,
2000,
);
// 2. 设置捕获模式为开始捕获
const captureStarted = await client.setCaptureMode(true, false);
if (!captureStarted) {
throw new Error("无法启动捕获");
@ -311,7 +509,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
alert?.info("开始捕获信号...", 2000);
// 2. 轮询捕获状态
// 3. 轮询捕获状态
let captureCompleted = false;
while (isCapturing.value) {
const status = await client.getCaptureStatus();
@ -390,8 +588,11 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
};
const forceCapture = async () => {
// 设置捕获状态为false这会使轮询停止
isCapturing.value = false;
// 检查是否有其他操作正在进行
if (operationMutex.isLocked()) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
return;
}
const release = await operationMutex.acquire();
try {
@ -404,7 +605,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
}
await getCaptureData();
alert.success(`捕获完成!`, 3000);
alert.success(`强制捕获完成!`, 3000);
} catch (error) {
console.error("强制捕获失败:", error);
alert.error(
@ -487,7 +688,7 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
});
// 设置逻辑数据
enableAllChannels();
setChannelDiv(8);
setLogicData({ x, y, xUnit: "us" }); // 改为微秒单位
alert?.success("测试数据生成成功", 2000);
@ -499,6 +700,9 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
// 触发设置状态
currentGlobalMode,
currentChannelDiv, // 导出当前通道组状态
captureLength, // 导出捕获深度
preCaptureLength, // 导出预捕获深度
isApplying,
isCapturing, // 导出捕获状态
isOperationInProgress, // 导出操作进行状态
@ -512,12 +716,13 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
globalModes,
operators,
signalValues,
channelDivOptions, // 导出通道组选项
captureLengthOptions, // 导出捕获深度选项
preCaptureLengthOptions, // 导出预捕获深度选项
// 触发设置方法
enableAllChannels,
disableAllChannels,
setChannelDiv, // 导出设置通道组方法
setGlobalMode,
applyConfiguration,
resetConfiguration,
setLogicData,
startCapture,

View File

@ -21,60 +21,7 @@
<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 from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 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 focus:ring-blue-300 active:scale-95"
@click="analyzer.generateTestData"
>
<span class="flex items-center gap-2">
<RefreshCcw
class="w-5 h-5 group-hover:rotate-180 transition-transform duration-300"
/>
生成测试数据
</span>
<div
class="absolute inset-0 bg-white opacity-0 group-hover:opacity-20 rounded-lg transition-opacity duration-200"
></div>
</button> -->
<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':
!analyzer.isCapturing.value,
'from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 focus:ring-red-300':
analyzer.isCapturing.value,
}"
@click="
analyzer.isCapturing.value
? analyzer.stopCapture()
: analyzer.startCapture()
"
>
<span class="flex items-center gap-2">
<template v-if="analyzer.isCapturing.value">
<Square class="w-5 h-5" />
停止捕获
</template>
<template v-else>
<Play class="w-5 h-5" />
开始捕获
</template>
</span>
</button>
<!-- 强制捕获按钮 - 只在正在捕获时显示 -->
<button
v-if="analyzer.isCapturing.value"
class="group relative px-8 py-3 bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 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 focus:ring-orange-300 active:scale-95"
@click="analyzer.forceCapture()"
>
<span class="flex items-center gap-2">
<Square class="w-5 h-5" />
强制捕获
</span>
</button>
</div>
</div>
</template>
@ -82,7 +29,6 @@
<script setup lang="ts">
import { computed, shallowRef } from "vue";
import VChart from "vue-echarts";
import { RefreshCcw, Play, Square } from "lucide-vue-next";
// Echarts
import { use } from "echarts/core";

View File

@ -1,345 +1,175 @@
<template>
<div class="space-y-6">
<!-- 通道状态概览 -->
<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">8</div>
<div class="stat-desc">逻辑分析仪通道</div>
</div>
<div class="stat">
<div class="stat-title">启用通道</div>
<div class="stat-value text-success">{{ enabledChannelCount }}</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="form-control">
<!-- 全局触发模式选择 -->
<div class="flex flex-row justify-between my-4 mx-2">
<div class="flex flex-row gap-4">
<label class="label">
<span class="label-text text-sm">全局触发逻辑</span>
</label>
<select
v-model="currentGlobalMode"
@change="setGlobalMode(currentGlobalMode)"
class="select select-sm select-bordered w-full"
>
<option
v-for="mode in globalModes"
:key="mode.value"
:value="mode.value"
<!-- 全局触发模式选择和通道组配置 -->
<div class="flex flex-col lg:flex-row justify-between gap-4 my-4 mx-2">
<!-- 左侧全局触发模式和通道组选择 -->
<div class="flex flex-col lg:flex-row gap-4">
<div class="flex flex-row gap-2 items-center">
<label class="label">
<span class="label-text text-sm">全局触发逻辑</span>
</label>
<select
v-model="currentGlobalMode"
@change="setGlobalMode(currentGlobalMode)"
class="select select-sm select-bordered"
>
{{ mode.label }} - {{ mode.description }}
</option>
</select>
<option
v-for="mode in globalModes"
:key="mode.value"
:value="mode.value"
>
{{ mode.label }} - {{ mode.description }}
</option>
</select>
</div>
<div class="flex flex-row gap-2 items-center">
<label class="label">
<span class="label-text text-sm">通道组</span>
</label>
<select
v-model="currentChannelDiv"
@change="setChannelDiv(currentChannelDiv)"
class="select select-sm select-bordered"
>
<option
v-for="option in channelDivOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
<div class="flex flex-row gap-2 items-center">
<label class="label">
<span class="label-text text-sm">捕获深度</span>
</label>
<select
v-model="captureLength"
class="select select-sm select-bordered"
>
<option
v-for="option in captureLengthOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
<div class="flex flex-row gap-2 items-center">
<label class="label">
<span class="label-text text-sm">预捕获深度</span>
</label>
<select
v-model="preCaptureLength"
class="select select-sm select-bordered"
>
<option
v-for="option in preCaptureLengthOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
</div>
</div>
<div class="flex flex-row gap-4">
<button @click="toggleAllChannels" class="btn btn-primary btn-sm">
{{ enabledChannelCount > 0 ? "全部禁用" : "全部启用" }}
</button>
<button
@click="applyConfiguration"
:disabled="isApplying"
class="btn btn-primary btn-sm"
>
<span
v-if="isApplying"
class="loading loading-spinner loading-sm"
></span>
应用配置
</button>
<!-- 右侧操作按钮 -->
<div class="flex flex-row gap-2">
<button @click="resetConfiguration" class="btn btn-outline btn-sm">
重置
重置配置
</button>
</div>
</div>
<!-- 通道列表 -->
<div class="space-y-2">
<!-- 表头 - 小屏幕单列时显示 -->
<!-- 表头 -->
<div
class="flex items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium lg:hidden"
class="flex items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium"
>
<span class="w-16">通道</span>
<span class="w-20">启用/触发</span>
<span class="w-32">标签</span>
<span class="w-16">颜色</span>
<span class="w-32">触发操作</span>
<span class="w-32">触发值</span>
</div>
<!-- 通道配置网格 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- 左列 (CH0-CH3) -->
<div class="space-y-2">
<!-- 左列表头 - 大屏幕时显示 -->
<div
class="hidden lg:flex items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium"
>
<span class="w-16">通道</span>
<span class="w-20">启用</span>
<span class="w-32">标签</span>
<span class="w-16">颜色</span>
<span class="w-32">触发操作</span>
<span class="w-32">触发值</span>
<!-- 通道配置网格 - 根据当前通道组动态显示 -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
<div
v-for="(channel, index) in channels.filter(ch => ch.enabled)"
:key="index"
class="flex items-center gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
>
<!-- 通道编号和颜色指示 -->
<div class="flex items-center gap-2 w-16">
<span class="font-mono font-medium">CH{{ channels.indexOf(channel) }}</span>
<div
class="w-3 h-3 rounded-full border-2 border-white shadow-sm"
:style="{ backgroundColor: channel.color }"
></div>
</div>
<!-- 左列通道 (0-3) -->
<div
v-for="(channel, index) in channels.slice(0, 4)"
:key="index"
class="flex items-center gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
>
<!-- 通道编号和颜色指示 -->
<div class="flex items-center gap-2 w-16">
<span class="font-mono font-medium">CH{{ index }}</span>
<div
class="w-3 h-3 rounded-full border-2 border-white shadow-sm"
:style="{ backgroundColor: channel.color }"
></div>
</div>
<!-- 通道启用开关同时控制触发 -->
<div class="form-control w-20">
<input
type="checkbox"
v-model="channel.enabled"
class="toggle toggle-sm toggle-primary"
/>
</div>
<!-- 通道标签 -->
<div class="form-control w-32">
<input
type="text"
v-model="channel.label"
:placeholder="`通道 ${index}`"
class="input input-sm input-bordered w-full"
:disabled="!channel.enabled"
/>
</div>
<!-- 颜色选择 -->
<div class="form-control w-16">
<input
type="color"
v-model="channel.color"
class="w-8 h-8 rounded border-2 border-base-300 cursor-pointer"
:disabled="!channel.enabled"
/>
</div>
<!-- 触发操作符选择 -->
<select
v-model="signalConfigs[index].operator"
class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
>
<option
v-for="op in operators"
:key="op.value"
:value="op.value"
>
{{ op.label }}
</option>
</select>
<!-- 触发信号值选择 -->
<select
v-model="signalConfigs[index].value"
class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
>
<option
v-for="val in signalValues"
:key="val.value"
:value="val.value"
>
{{ val.label }}
</option>
</select>
<!-- 通道标签 -->
<div class="form-control w-32">
<input
type="text"
v-model="channel.label"
:placeholder="`通道 ${channels.indexOf(channel)}`"
class="input input-sm input-bordered w-full"
/>
</div>
<!-- 颜色选择 -->
<div class="form-control w-16">
<input
type="color"
v-model="channel.color"
class="w-8 h-8 rounded border-2 border-base-300 cursor-pointer"
/>
</div>
<!-- 触发操作符选择 -->
<select
v-model="signalConfigs[channels.indexOf(channel)].operator"
class="select select-sm select-bordered w-32"
>
<option
v-for="op in operators"
:key="op.value"
:value="op.value"
>
{{ op.label }}
</option>
</select>
<!-- 触发信号值选择 -->
<select
v-model="signalConfigs[channels.indexOf(channel)].value"
class="select select-sm select-bordered w-32"
>
<option
v-for="val in signalValues"
:key="val.value"
:value="val.value"
>
{{ val.label }}
</option>
</select>
</div>
</div>
<!-- 右列 (CH4-CH7) - 仅在大屏幕显示 -->
<div class="hidden lg:block space-y-2">
<!-- 右列表头 -->
<div
class="flex items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium"
>
<span class="w-16">通道</span>
<span class="w-20">启用/触发</span>
<span class="w-32">标签</span>
<span class="w-16">颜色</span>
<span class="w-32">触发操作</span>
<span class="w-32">触发值</span>
</div>
<!-- 右列通道 (4-7) -->
<div
v-for="(channel, index) in channels.slice(4, 8)"
:key="index + 4"
class="flex items-center gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
>
<!-- 通道编号和颜色指示 -->
<div class="flex items-center gap-2 w-16">
<span class="font-mono font-medium">CH{{ index + 4 }}</span>
<div
class="w-3 h-3 rounded-full border-2 border-white shadow-sm"
:style="{ backgroundColor: channel.color }"
></div>
</div>
<!-- 通道启用开关同时控制触发 -->
<div class="form-control w-20">
<input
type="checkbox"
v-model="channel.enabled"
class="toggle toggle-sm toggle-primary"
/>
</div>
<!-- 通道标签 -->
<div class="form-control w-32">
<input
type="text"
v-model="channel.label"
:placeholder="`通道 ${index + 4}`"
class="input input-sm input-bordered w-full"
:disabled="!channel.enabled"
/>
</div>
<!-- 颜色选择 -->
<div class="form-control w-16">
<input
type="color"
v-model="channel.color"
class="w-8 h-8 rounded border-2 border-base-300 cursor-pointer"
:disabled="!channel.enabled"
/>
</div>
<!-- 触发操作符选择 -->
<select
v-model="signalConfigs[index + 4].operator"
class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
>
<option
v-for="op in operators"
:key="op.value"
:value="op.value"
>
{{ op.label }}
</option>
</select>
<!-- 触发信号值选择 -->
<select
v-model="signalConfigs[index + 4].value"
class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
>
<option
v-for="val in signalValues"
:key="val.value"
:value="val.value"
>
{{ val.label }}
</option>
</select>
</div>
</div>
<!-- 小屏幕时继续显示 CH4-CH7 -->
<div class="lg:hidden space-y-2">
<div
v-for="(channel, index) in channels.slice(4, 8)"
:key="index + 4"
class="flex items-center gap-2 p-3 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
>
<!-- 通道编号和颜色指示 -->
<div class="flex items-center gap-2 w-16">
<span class="font-mono font-medium">CH{{ index + 4 }}</span>
<div
class="w-3 h-3 rounded-full border-2 border-white shadow-sm"
:style="{ backgroundColor: channel.color }"
></div>
</div>
<!-- 通道启用开关同时控制触发 -->
<div class="form-control w-20">
<input
type="checkbox"
v-model="channel.enabled"
class="toggle toggle-sm toggle-primary"
/>
</div>
<!-- 通道标签 -->
<div class="form-control w-32">
<input
type="text"
v-model="channel.label"
:placeholder="`通道 ${index + 4}`"
class="input input-sm input-bordered w-full"
:disabled="!channel.enabled"
/>
</div>
<!-- 颜色选择 -->
<div class="form-control w-16">
<input
type="color"
v-model="channel.color"
class="w-8 h-8 rounded border-2 border-base-300 cursor-pointer"
:disabled="!channel.enabled"
/>
</div>
<!-- 触发操作符选择 -->
<select
v-model="signalConfigs[index + 4].operator"
class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
>
<option
v-for="op in operators"
:key="op.value"
:value="op.value"
>
{{ op.label }}
</option>
</select>
<!-- 触发信号值选择 -->
<select
v-model="signalConfigs[index + 4].value"
class="select select-sm select-bordered w-32"
:disabled="!channel.enabled"
>
<option
v-for="val in signalValues"
:key="val.value"
:value="val.value"
>
{{ val.label }}
</option>
</select>
</div>
</div>
<!-- 当没有启用通道时的提示 -->
<div v-if="enabledChannelCount === 0" class="text-center py-8 text-base-content/60">
<p class="text-lg font-medium">未启用任何通道</p>
<p class="text-sm">请选择通道组来配置逻辑分析仪</p>
</div>
</div>
</div>
@ -352,6 +182,9 @@ import { useLogicAnalyzerState } from "./LogicAnalyzerManager";
const {
currentGlobalMode,
currentChannelDiv,
captureLength,
preCaptureLength,
isApplying,
channels,
signalConfigs,
@ -359,18 +192,11 @@ const {
globalModes,
operators,
signalValues,
enableAllChannels,
disableAllChannels,
channelDivOptions,
captureLengthOptions,
preCaptureLengthOptions,
setChannelDiv,
setGlobalMode,
applyConfiguration,
resetConfiguration,
} = useRequiredInjection(useLogicAnalyzerState);
const toggleAllChannels = () => {
if (enabledChannelCount.value > 0) {
disableAllChannels();
} else {
enableAllChannels();
}
};
</script>

View File

@ -1,135 +1,135 @@
<template>
<div class="flex flex-col bg-base-100 justify-center items-center">
<!-- Title -->
<h1 class="font-bold text-2xl">上传比特流文件</h1>
<!-- Input File -->
<fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">选择或拖拽上传文件</legend>
<input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" />
<label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
</fieldset>
<!-- Upload Button -->
<div class="card-actions w-full">
<button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading">
<div v-if="isUploading">
<span class="loading loading-spinner"></span>
下载中...
</div>
<div v-else>
{{ buttonText }}
</div>
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, useTemplateRef, onMounted } from "vue";
import { useDialogStore } from "@/stores/dialog";
import { isNull, isUndefined } from "lodash";
interface Props {
uploadEvent?: (file: File) => Promise<boolean>;
downloadEvent?: () => Promise<boolean>;
maxMemory?: number;
}
const props = withDefaults(defineProps<Props>(), {
maxMemory: 4,
});
const emits = defineEmits<{
finishedUpload: [file: File];
}>();
const dialog = useDialogStore();
const isUploading = ref(false);
const buttonText = computed(() => {
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
});
const fileInput = useTemplateRef("fileInput");
const bitstream = defineModel("bitstreamFile", {
type: File,
default: undefined,
});
onMounted(() => {
if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
let fileList = new DataTransfer();
fileList.items.add(bitstream.value);
fileInput.value.files = fileList.files;
}
});
function handleFileChange(event: Event): void {
const target = event.target as HTMLInputElement;
const file = target.files?.[0]; //
if (!file) {
return;
}
bitstream.value = file;
}
function checkFile(file: File): boolean {
const maxBytes = props.maxMemory! * 1024 * 1024; // MB
if (file.size > maxBytes) {
dialog.error(`文件大小超过最大限制: ${props.maxMemory}MB`);
return false;
}
return true;
}
async function handleClick(event: Event): Promise<void> {
if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
dialog.error(`未选择文件`);
return;
}
if (!checkFile(bitstream.value)) return;
if (isUndefined(props.uploadEvent)) {
dialog.error("无法上传");
return;
}
isUploading.value = true;
try {
const ret = await props.uploadEvent(bitstream.value);
if (isUndefined(props.downloadEvent)) {
if (ret) {
dialog.info("上传成功");
emits("finishedUpload", bitstream.value);
} else dialog.error("上传失败");
return;
}
if (!ret) {
isUploading.value = false;
return;
}
} catch (e) {
dialog.error("上传失败");
console.error(e);
return;
}
// Download
try {
const ret = await props.downloadEvent();
if (ret) dialog.info("下载成功");
else dialog.error("下载失败");
} catch (e) {
dialog.error("下载失败");
console.error(e);
}
isUploading.value = false;
}
</script>
<style scoped lang="postcss">
@import "../assets/main.css";
</style>
<template>
<div class="flex flex-col bg-base-100 justify-center items-center">
<!-- Title -->
<h1 class="font-bold text-2xl">上传比特流文件</h1>
<!-- Input File -->
<fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">选择或拖拽上传文件</legend>
<input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" />
<label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
</fieldset>
<!-- Upload Button -->
<div class="card-actions w-full">
<button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading">
<div v-if="isUploading">
<span class="loading loading-spinner"></span>
下载中...
</div>
<div v-else>
{{ buttonText }}
</div>
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, useTemplateRef, onMounted } from "vue";
import { useDialogStore } from "@/stores/dialog";
import { isNull, isUndefined } from "lodash";
interface Props {
uploadEvent?: (file: File) => Promise<boolean>;
downloadEvent?: () => Promise<boolean>;
maxMemory?: number;
}
const props = withDefaults(defineProps<Props>(), {
maxMemory: 4,
});
const emits = defineEmits<{
finishedUpload: [file: File];
}>();
const dialog = useDialogStore();
const isUploading = ref(false);
const buttonText = computed(() => {
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
});
const fileInput = useTemplateRef("fileInput");
const bitstream = defineModel("bitstreamFile", {
type: File,
default: undefined,
});
onMounted(() => {
if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
let fileList = new DataTransfer();
fileList.items.add(bitstream.value);
fileInput.value.files = fileList.files;
}
});
function handleFileChange(event: Event): void {
const target = event.target as HTMLInputElement;
const file = target.files?.[0]; //
if (!file) {
return;
}
bitstream.value = file;
}
function checkFile(file: File): boolean {
const maxBytes = props.maxMemory! * 1024 * 1024; // MB
if (file.size > maxBytes) {
dialog.error(`文件大小超过最大限制: ${props.maxMemory}MB`);
return false;
}
return true;
}
async function handleClick(event: Event): Promise<void> {
if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
dialog.error(`未选择文件`);
return;
}
if (!checkFile(bitstream.value)) return;
if (isUndefined(props.uploadEvent)) {
dialog.error("无法上传");
return;
}
isUploading.value = true;
try {
const ret = await props.uploadEvent(bitstream.value);
if (isUndefined(props.downloadEvent)) {
if (ret) {
dialog.info("上传成功");
emits("finishedUpload", bitstream.value);
} else dialog.error("上传失败");
return;
}
if (!ret) {
isUploading.value = false;
return;
}
} catch (e) {
dialog.error("上传失败");
console.error(e);
return;
}
// Download
try {
const ret = await props.downloadEvent();
if (ret) dialog.info("下载成功");
else dialog.error("下载失败");
} catch (e) {
dialog.error("下载失败");
console.error(e);
}
isUploading.value = false;
}
</script>
<style scoped lang="postcss">
@import "../assets/main.css";
</style>

View File

@ -9,8 +9,42 @@
逻辑信号分析
</div>
<div class="flex items-center gap-2">
<!-- 空闲状态只显示开始捕获按钮 -->
<button
v-if="!analyzer.isCapturing.value"
@click="analyzer.startCapture"
:disabled="analyzer.isApplying.value"
class="btn btn-sm btn-primary"
>
开始捕获
</button>
<!-- 捕获状态显示停止捕获和强制捕获按钮 -->
<button
v-if="analyzer.isCapturing.value"
@click="analyzer.stopCapture"
class="btn btn-sm btn-warning"
>
<span class="loading loading-spinner loading-sm"></span>
停止捕获
</button>
<button
v-if="analyzer.isCapturing.value"
@click="analyzer.forceCapture"
class="btn btn-sm btn-secondary"
>
强制捕获
</button>
<!-- 其他按钮保持不变 -->
<button
@click="analyzer.generateTestData"
class="btn btn-sm btn-info"
>
测试数据
</button>
<button class="btn btn-sm btn-error" @click="handleDeleteData">
清空
清空数据
</button>
</div>
</h2>
@ -21,9 +55,45 @@
<!-- 触发设置 -->
<div class="card bg-base-200 shadow-xl mx-5">
<div class="card-body">
<h2 class="card-title">
<Settings class="w-5 h-5" />
触发设置
<h2 class="card-title flex justify-between items-center">
<div class="flex gap-8">
<div class="flex items-center gap-2">
<Settings class="w-5 h-5" />
触发设置
</div>
<!-- 配置摘要 -->
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>{{ analyzer.enabledChannelCount.value }}/32 通道</span>
<span>捕获: {{ analyzer.captureLength.value }}</span>
<span>预捕获: {{ analyzer.preCaptureLength.value }}</span>
<span>{{ analyzer.globalModes.find(m => m.value === analyzer.currentGlobalMode.value)?.label || '未知' }}</span>
</div>
</div>
<div class="flex items-center gap-4">
<!-- 状态指示 -->
<div class="flex items-center gap-2 text-sm">
<span
v-if="analyzer.isCapturing.value"
class="flex items-center gap-1 text-warning"
>
<span class="loading loading-spinner loading-xs"></span>
捕获中
</span>
<span
v-else-if="analyzer.isApplying.value"
class="flex items-center gap-1 text-info"
>
<span class="loading loading-spinner loading-xs"></span>
配置中
</span>
<span
v-else
class="text-success"
>
就绪
</span>
</div>
</div>
</h2>
<TriggerSettings />
</div>