Compare commits
3 Commits
dpp
...
04b136117d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04b136117d | ||
|
|
5c87204ef6 | ||
| 35647d21bb |
@@ -145,6 +145,8 @@ try
|
|||||||
// 添加 HTTP 视频流服务
|
// 添加 HTTP 视频流服务
|
||||||
builder.Services.AddSingleton<HttpVideoStreamService>();
|
builder.Services.AddSingleton<HttpVideoStreamService>();
|
||||||
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpVideoStreamService>());
|
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpVideoStreamService>());
|
||||||
|
builder.Services.AddSingleton<HttpHdmiVideoStreamService>();
|
||||||
|
builder.Services.AddHostedService(provider => provider.GetRequiredService<HttpHdmiVideoStreamService>());
|
||||||
|
|
||||||
// Application Settings
|
// Application Settings
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
63
server/src/Controllers/HdmiVideoStreamController.cs
Normal file
63
server/src/Controllers/HdmiVideoStreamController.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Cors;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using server.Services;
|
||||||
|
using Database;
|
||||||
|
|
||||||
|
namespace server.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[EnableCors("Users")]
|
||||||
|
public class HdmiVideoStreamController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly HttpHdmiVideoStreamService _videoStreamService;
|
||||||
|
private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||||
|
|
||||||
|
public HdmiVideoStreamController(HttpHdmiVideoStreamService videoStreamService)
|
||||||
|
{
|
||||||
|
_videoStreamService = videoStreamService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员获取所有板子的 endpoints
|
||||||
|
[HttpGet("AllEndpoints")]
|
||||||
|
[Authorize("Admin")]
|
||||||
|
public ActionResult<List<HdmiVideoStreamEndpoint>> GetAllEndpoints()
|
||||||
|
{
|
||||||
|
var endpoints = _videoStreamService.GetAllVideoEndpoints();
|
||||||
|
if (endpoints == null)
|
||||||
|
return NotFound("No boards found.");
|
||||||
|
return Ok(endpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户获取自己板子的 endpoint
|
||||||
|
[HttpGet("MyEndpoint")]
|
||||||
|
[Authorize]
|
||||||
|
public ActionResult<HdmiVideoStreamEndpoint> GetMyEndpoint()
|
||||||
|
{
|
||||||
|
var userName = User.FindFirstValue(ClaimTypes.Name);
|
||||||
|
if (string.IsNullOrEmpty(userName))
|
||||||
|
return Unauthorized("User name not found in claims.");
|
||||||
|
|
||||||
|
var db = new AppDataConnection();
|
||||||
|
if (db == null)
|
||||||
|
return NotFound("Database connection failed.");
|
||||||
|
|
||||||
|
var userRet = db.GetUserByName(userName);
|
||||||
|
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
|
||||||
|
return NotFound("User not found.");
|
||||||
|
|
||||||
|
var user = userRet.Value.Value;
|
||||||
|
var boardId = user.BoardID;
|
||||||
|
if (boardId == Guid.Empty)
|
||||||
|
return NotFound("No board bound to this user.");
|
||||||
|
|
||||||
|
var boardRet = db.GetBoardByID(boardId);
|
||||||
|
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
|
||||||
|
return NotFound("Board not found.");
|
||||||
|
|
||||||
|
var endpoint = _videoStreamService.GetVideoEndpoint(boardId.ToString());
|
||||||
|
return Ok(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -259,7 +259,8 @@ public class LogicAnalyzerController : ControllerBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (capture_length < 0 || capture_length > 2048*32)
|
//DDR深度为 32'h01000000 - 32'h0FFFFFFF
|
||||||
|
if (capture_length < 0 || capture_length > 0x10000000 - 0x01000000)
|
||||||
return BadRequest("采样深度设置错误");
|
return BadRequest("采样深度设置错误");
|
||||||
if (pre_capture_length < 0 || pre_capture_length >= capture_length)
|
if (pre_capture_length < 0 || pre_capture_length >= capture_length)
|
||||||
return BadRequest("预采样深度必须小于捕获深度");
|
return BadRequest("预采样深度必须小于捕获深度");
|
||||||
|
|||||||
@@ -32,14 +32,16 @@ class HdmiIn
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">HDMI输入设备IP地址</param>
|
/// <param name="address">HDMI输入设备IP地址</param>
|
||||||
/// <param name="port">HDMI输入设备端口</param>
|
/// <param name="port">HDMI输入设备端口</param>
|
||||||
|
/// <param name="taskID">任务ID</param>
|
||||||
/// <param name="timeout">超时时间(毫秒)</param>
|
/// <param name="timeout">超时时间(毫秒)</param>
|
||||||
public HdmiIn(string address, int port, int timeout = 500)
|
public HdmiIn(string address, int port, int taskID, int timeout = 500)
|
||||||
{
|
{
|
||||||
if (timeout < 0)
|
if (timeout < 0)
|
||||||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||||||
|
this.taskID = taskID;
|
||||||
this.timeout = timeout;
|
this.timeout = timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ static class AnalyzerAddr
|
|||||||
public const UInt32 DMA1_START_WRITE_ADDR = DMA1_BASE + 0x0000_0012;
|
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_END_WRITE_ADDR = DMA1_BASE + 0x0000_0013;
|
||||||
public const UInt32 DMA1_CAPTURE_CTRL_ADDR = DMA1_BASE + 0x0000_0014;
|
public const UInt32 DMA1_CAPTURE_CTRL_ADDR = DMA1_BASE + 0x0000_0014;
|
||||||
public const UInt32 STORE_OFFSET_ADDR = DDR_BASE + 0x0010_0000;
|
public const UInt32 STORE_OFFSET_ADDR = DDR_BASE + 0x0100_0000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 0x0100_0000 - 0x0100_03FF 只读 32位波形存储,得到的32位数据中低八位最先捕获,高八位最后捕获。<br/>
|
/// 0x0100_0000 - 0x0100_03FF 只读 32位波形存储,得到的32位数据中低八位最先捕获,高八位最后捕获。<br/>
|
||||||
|
|||||||
234
server/src/Services/HttpHdmiVideoStreamService.cs
Normal file
234
server/src/Services/HttpHdmiVideoStreamService.cs
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Peripherals.HdmiInClient;
|
||||||
|
|
||||||
|
namespace server.Services;
|
||||||
|
|
||||||
|
public class HdmiVideoStreamEndpoint
|
||||||
|
{
|
||||||
|
public string BoardId { get; set; } = "";
|
||||||
|
public string MjpegUrl { get; set; } = "";
|
||||||
|
public string VideoUrl { get; set; } = "";
|
||||||
|
public string SnapshotUrl { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HttpHdmiVideoStreamService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||||
|
private HttpListener? _httpListener;
|
||||||
|
private readonly int _serverPort = 4322;
|
||||||
|
private readonly ConcurrentDictionary<string, HdmiIn> _hdmiInDict = new();
|
||||||
|
private bool _isEnabled = true;
|
||||||
|
|
||||||
|
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_httpListener = new HttpListener();
|
||||||
|
_httpListener.Prefixes.Add($"http://*:{_serverPort}/");
|
||||||
|
_httpListener.Start();
|
||||||
|
logger.Info($"HDMI Video Stream Service started on port {_serverPort}");
|
||||||
|
|
||||||
|
await base.StartAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
HttpListenerContext? context = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
context = await _httpListener.GetContextAsync();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// Listener closed, exit loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (HttpListenerException)
|
||||||
|
{
|
||||||
|
// Listener closed, exit loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Error in GetContextAsync");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (context != null)
|
||||||
|
_ = HandleRequestAsync(context, stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_httpListener?.Close();
|
||||||
|
logger.Info("HDMI Video Stream Service stopped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
logger.Info("Stopping HDMI Video Stream Service...");
|
||||||
|
_isEnabled = false;
|
||||||
|
_httpListener?.Close(); // 立即关闭监听器,唤醒阻塞
|
||||||
|
await base.StopAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取/创建 HdmiIn 实例
|
||||||
|
private HdmiIn? GetOrCreateHdmiIn(string boardId)
|
||||||
|
{
|
||||||
|
if (_hdmiInDict.TryGetValue(boardId, out var hdmiIn))
|
||||||
|
return hdmiIn;
|
||||||
|
|
||||||
|
var db = new Database.AppDataConnection();
|
||||||
|
if (db == null)
|
||||||
|
{
|
||||||
|
logger.Error("Failed to create HdmiIn instance");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var boardRet = db.GetBoardByID(Guid.Parse(boardId));
|
||||||
|
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to get board with ID {boardId}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var board = boardRet.Value.Value;
|
||||||
|
|
||||||
|
hdmiIn = new HdmiIn(board.IpAddr, board.Port, 0); // taskID 可根据实际需求调整
|
||||||
|
_hdmiInDict[boardId] = hdmiIn;
|
||||||
|
return hdmiIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var path = context.Request.Url?.AbsolutePath ?? "/";
|
||||||
|
var boardId = context.Request.QueryString["boardId"];
|
||||||
|
if (string.IsNullOrEmpty(boardId))
|
||||||
|
{
|
||||||
|
await SendErrorAsync(context.Response, "Missing boardId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hdmiIn = GetOrCreateHdmiIn(boardId);
|
||||||
|
if (hdmiIn == null)
|
||||||
|
{
|
||||||
|
await SendErrorAsync(context.Response, "Invalid boardId or board not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path == "/snapshot")
|
||||||
|
{
|
||||||
|
await HandleSnapshotRequestAsync(context.Response, hdmiIn, cancellationToken);
|
||||||
|
}
|
||||||
|
else if (path == "/mjpeg")
|
||||||
|
{
|
||||||
|
await HandleMjpegStreamAsync(context.Response, hdmiIn, cancellationToken);
|
||||||
|
}
|
||||||
|
else if (path == "/video")
|
||||||
|
{
|
||||||
|
await SendVideoHtmlPageAsync(context.Response, boardId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await SendIndexHtmlPageAsync(context.Response, boardId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSnapshotRequestAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var frameResult = await hdmiIn.ReadFrame();
|
||||||
|
if (frameResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
response.ContentType = "image/jpeg";
|
||||||
|
await response.OutputStream.WriteAsync(frameResult.Value, 0, frameResult.Value.Length, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.StatusCode = 500;
|
||||||
|
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes("Failed to get snapshot"));
|
||||||
|
}
|
||||||
|
response.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleMjpegStreamAsync(HttpListenerResponse response, HdmiIn hdmiIn, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
response.ContentType = "multipart/x-mixed-replace; boundary=--frame";
|
||||||
|
while (!cancellationToken.IsCancellationRequested && _isEnabled)
|
||||||
|
{
|
||||||
|
var frameResult = await hdmiIn.ReadFrame();
|
||||||
|
if (frameResult.IsSuccessful)
|
||||||
|
{
|
||||||
|
var header = $"--frame\r\nContent-Type: image/jpeg\r\nContent-Length: {frameResult.Value.Length}\r\n\r\n";
|
||||||
|
await response.OutputStream.WriteAsync(System.Text.Encoding.ASCII.GetBytes(header));
|
||||||
|
await response.OutputStream.WriteAsync(frameResult.Value, 0, frameResult.Value.Length, cancellationToken);
|
||||||
|
await response.OutputStream.WriteAsync(System.Text.Encoding.ASCII.GetBytes("\r\n"));
|
||||||
|
}
|
||||||
|
await Task.Delay(33, cancellationToken); // ~30fps
|
||||||
|
}
|
||||||
|
response.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendVideoHtmlPageAsync(HttpListenerResponse response, string boardId)
|
||||||
|
{
|
||||||
|
string html = $@"<html><body>
|
||||||
|
<h1>HDMI Video Stream for Board {boardId}</h1>
|
||||||
|
<img src='/mjpeg?boardId={boardId}' />
|
||||||
|
</body></html>";
|
||||||
|
response.ContentType = "text/html";
|
||||||
|
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(html));
|
||||||
|
response.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendIndexHtmlPageAsync(HttpListenerResponse response, string boardId)
|
||||||
|
{
|
||||||
|
string html = $@"<html><body>
|
||||||
|
<h1>Welcome to HDMI Video Stream Service</h1>
|
||||||
|
<a href='/video?boardId={boardId}'>View Video Stream for Board {boardId}</a>
|
||||||
|
</body></html>";
|
||||||
|
response.ContentType = "text/html";
|
||||||
|
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(html));
|
||||||
|
response.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendErrorAsync(HttpListenerResponse response, string message)
|
||||||
|
{
|
||||||
|
response.StatusCode = 400;
|
||||||
|
await response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(message));
|
||||||
|
response.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<HdmiVideoStreamEndpoint>? GetAllVideoEndpoints()
|
||||||
|
{
|
||||||
|
var db = new Database.AppDataConnection();
|
||||||
|
var boards = db?.GetAllBoard();
|
||||||
|
if (boards == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var endpoints = new List<HdmiVideoStreamEndpoint>();
|
||||||
|
foreach (var board in boards)
|
||||||
|
{
|
||||||
|
endpoints.Add(new HdmiVideoStreamEndpoint
|
||||||
|
{
|
||||||
|
BoardId = board.ID.ToString(),
|
||||||
|
MjpegUrl = $"http://{Global.localhost}:{_serverPort}/mjpeg?boardId={board.ID}",
|
||||||
|
VideoUrl = $"http://{Global.localhost}:{_serverPort}/video?boardId={board.ID}",
|
||||||
|
SnapshotUrl = $"http://{Global.localhost}:{_serverPort}/snapshot?boardId={board.ID}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HdmiVideoStreamEndpoint GetVideoEndpoint(string boardId)
|
||||||
|
{
|
||||||
|
return new HdmiVideoStreamEndpoint
|
||||||
|
{
|
||||||
|
BoardId = boardId,
|
||||||
|
MjpegUrl = $"http://{Global.localhost}:{_serverPort}/mjpeg?boardId={boardId}",
|
||||||
|
VideoUrl = $"http://{Global.localhost}:{_serverPort}/video?boardId={boardId}",
|
||||||
|
SnapshotUrl = $"http://{Global.localhost}:{_serverPort}/snapshot?boardId={boardId}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
165
src/APIClient.ts
165
src/APIClient.ts
@@ -2944,6 +2944,123 @@ export class ExamClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class HdmiVideoStreamClient {
|
||||||
|
protected instance: AxiosInstance;
|
||||||
|
protected baseUrl: string;
|
||||||
|
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
|
||||||
|
|
||||||
|
constructor(baseUrl?: string, instance?: AxiosInstance) {
|
||||||
|
|
||||||
|
this.instance = instance || axios.create();
|
||||||
|
|
||||||
|
this.baseUrl = baseUrl ?? "http://127.0.0.1:5000";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllEndpoints( cancelToken?: CancelToken): Promise<HdmiVideoStreamEndpoint[]> {
|
||||||
|
let url_ = this.baseUrl + "/api/HdmiVideoStream/AllEndpoints";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: AxiosRequestConfig = {
|
||||||
|
method: "GET",
|
||||||
|
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.processGetAllEndpoints(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processGetAllEndpoints(response: AxiosResponse): Promise<HdmiVideoStreamEndpoint[]> {
|
||||||
|
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;
|
||||||
|
if (Array.isArray(resultData200)) {
|
||||||
|
result200 = [] as any;
|
||||||
|
for (let item of resultData200)
|
||||||
|
result200!.push(HdmiVideoStreamEndpoint.fromJS(item));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result200 = <any>null;
|
||||||
|
}
|
||||||
|
return Promise.resolve<HdmiVideoStreamEndpoint[]>(result200);
|
||||||
|
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
const _responseText = response.data;
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
}
|
||||||
|
return Promise.resolve<HdmiVideoStreamEndpoint[]>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMyEndpoint( cancelToken?: CancelToken): Promise<HdmiVideoStreamEndpoint> {
|
||||||
|
let url_ = this.baseUrl + "/api/HdmiVideoStream/MyEndpoint";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: AxiosRequestConfig = {
|
||||||
|
method: "GET",
|
||||||
|
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.processGetMyEndpoint(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processGetMyEndpoint(response: AxiosResponse): Promise<HdmiVideoStreamEndpoint> {
|
||||||
|
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 = HdmiVideoStreamEndpoint.fromJS(resultData200);
|
||||||
|
return Promise.resolve<HdmiVideoStreamEndpoint>(result200);
|
||||||
|
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
const _responseText = response.data;
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
}
|
||||||
|
return Promise.resolve<HdmiVideoStreamEndpoint>(null as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class JtagClient {
|
export class JtagClient {
|
||||||
protected instance: AxiosInstance;
|
protected instance: AxiosInstance;
|
||||||
protected baseUrl: string;
|
protected baseUrl: string;
|
||||||
@@ -8019,6 +8136,54 @@ export interface ICreateExamRequest {
|
|||||||
isVisibleToUsers: boolean;
|
isVisibleToUsers: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class HdmiVideoStreamEndpoint implements IHdmiVideoStreamEndpoint {
|
||||||
|
boardId!: string;
|
||||||
|
mjpegUrl!: string;
|
||||||
|
videoUrl!: string;
|
||||||
|
snapshotUrl!: string;
|
||||||
|
|
||||||
|
constructor(data?: IHdmiVideoStreamEndpoint) {
|
||||||
|
if (data) {
|
||||||
|
for (var property in data) {
|
||||||
|
if (data.hasOwnProperty(property))
|
||||||
|
(<any>this)[property] = (<any>data)[property];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_data?: any) {
|
||||||
|
if (_data) {
|
||||||
|
this.boardId = _data["boardId"];
|
||||||
|
this.mjpegUrl = _data["mjpegUrl"];
|
||||||
|
this.videoUrl = _data["videoUrl"];
|
||||||
|
this.snapshotUrl = _data["snapshotUrl"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJS(data: any): HdmiVideoStreamEndpoint {
|
||||||
|
data = typeof data === 'object' ? data : {};
|
||||||
|
let result = new HdmiVideoStreamEndpoint();
|
||||||
|
result.init(data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(data?: any) {
|
||||||
|
data = typeof data === 'object' ? data : {};
|
||||||
|
data["boardId"] = this.boardId;
|
||||||
|
data["mjpegUrl"] = this.mjpegUrl;
|
||||||
|
data["videoUrl"] = this.videoUrl;
|
||||||
|
data["snapshotUrl"] = this.snapshotUrl;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHdmiVideoStreamEndpoint {
|
||||||
|
boardId: string;
|
||||||
|
mjpegUrl: string;
|
||||||
|
videoUrl: string;
|
||||||
|
snapshotUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 逻辑分析仪运行状态枚举 */
|
/** 逻辑分析仪运行状态枚举 */
|
||||||
export enum CaptureStatus {
|
export enum CaptureStatus {
|
||||||
None = 0,
|
None = 0,
|
||||||
|
|||||||
@@ -76,28 +76,12 @@ const channelDivOptions = [
|
|||||||
{ value: 32, label: "32通道", description: "启用32个通道 (CH0-CH31)" },
|
{ value: 32, label: "32通道", description: "启用32个通道 (CH0-CH31)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 捕获深度选项
|
// 捕获深度限制常量
|
||||||
const captureLengthOptions = [
|
const CAPTURE_LENGTH_MIN = 1024; // 最小捕获深度 1024
|
||||||
{ value: 256, label: "256" },
|
const CAPTURE_LENGTH_MAX = 0x10000000 - 0x01000000; // 最大捕获深度
|
||||||
{ 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 = [
|
const PRE_CAPTURE_LENGTH_MIN = 0; // 最小预捕获深度 0
|
||||||
{ 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 = [
|
const defaultColors = [
|
||||||
@@ -126,8 +110,8 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
// 触发设置相关状态
|
// 触发设置相关状态
|
||||||
const currentGlobalMode = ref<GlobalCaptureMode>(GlobalCaptureMode.AND);
|
const currentGlobalMode = ref<GlobalCaptureMode>(GlobalCaptureMode.AND);
|
||||||
const currentChannelDiv = ref<number>(8); // 默认启用8个通道
|
const currentChannelDiv = ref<number>(8); // 默认启用8个通道
|
||||||
const captureLength = ref<number>(1024); // 捕获深度,默认1024
|
const captureLength = ref<number>(CAPTURE_LENGTH_MIN); // 捕获深度,默认为最小值
|
||||||
const preCaptureLength = ref<number>(0); // 预捕获深度,默认0
|
const preCaptureLength = ref<number>(PRE_CAPTURE_LENGTH_MIN); // 预捕获深度,默认0
|
||||||
const isApplying = ref(false);
|
const isApplying = ref(false);
|
||||||
const isCapturing = ref(false); // 添加捕获状态标识
|
const isCapturing = ref(false); // 添加捕获状态标识
|
||||||
|
|
||||||
@@ -181,6 +165,64 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 验证捕获深度
|
||||||
|
const validateCaptureLength = (value: number): { valid: boolean; message?: string } => {
|
||||||
|
if (!Number.isInteger(value)) {
|
||||||
|
return { valid: false, message: "捕获深度必须是整数" };
|
||||||
|
}
|
||||||
|
if (value < CAPTURE_LENGTH_MIN) {
|
||||||
|
return { valid: false, message: `捕获深度不能小于 ${CAPTURE_LENGTH_MIN}` };
|
||||||
|
}
|
||||||
|
if (value > CAPTURE_LENGTH_MAX) {
|
||||||
|
return { valid: false, message: `捕获深度不能大于 ${CAPTURE_LENGTH_MAX.toLocaleString()}` };
|
||||||
|
}
|
||||||
|
return { valid: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证预捕获深度
|
||||||
|
const validatePreCaptureLength = (value: number, currentCaptureLength: number): { valid: boolean; message?: string } => {
|
||||||
|
if (!Number.isInteger(value)) {
|
||||||
|
return { valid: false, message: "预捕获深度必须是整数" };
|
||||||
|
}
|
||||||
|
if (value < PRE_CAPTURE_LENGTH_MIN) {
|
||||||
|
return { valid: false, message: `预捕获深度不能小于 ${PRE_CAPTURE_LENGTH_MIN}` };
|
||||||
|
}
|
||||||
|
if (value >= currentCaptureLength) {
|
||||||
|
return { valid: false, message: `预捕获深度不能大于等于捕获深度 (${currentCaptureLength})` };
|
||||||
|
}
|
||||||
|
return { valid: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置捕获深度
|
||||||
|
const setCaptureLength = (value: number) => {
|
||||||
|
const validation = validateCaptureLength(value);
|
||||||
|
if (!validation.valid) {
|
||||||
|
alert?.error(validation.message!, 3000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查预捕获深度是否仍然有效
|
||||||
|
if (preCaptureLength.value >= value) {
|
||||||
|
preCaptureLength.value = Math.max(0, value - 1);
|
||||||
|
alert?.warn(`预捕获深度已自动调整为 ${preCaptureLength.value}`, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
captureLength.value = value;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置预捕获深度
|
||||||
|
const setPreCaptureLength = (value: number) => {
|
||||||
|
const validation = validatePreCaptureLength(value, captureLength.value);
|
||||||
|
if (!validation.valid) {
|
||||||
|
alert?.error(validation.message!, 3000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
preCaptureLength.value = value;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
// 设置通道组
|
// 设置通道组
|
||||||
const setChannelDiv = (channelCount: number) => {
|
const setChannelDiv = (channelCount: number) => {
|
||||||
// 验证通道数量是否有效
|
// 验证通道数量是否有效
|
||||||
@@ -717,8 +759,15 @@ const [useProvideLogicAnalyzer, useLogicAnalyzerState] = createInjectionState(
|
|||||||
operators,
|
operators,
|
||||||
signalValues,
|
signalValues,
|
||||||
channelDivOptions, // 导出通道组选项
|
channelDivOptions, // 导出通道组选项
|
||||||
captureLengthOptions, // 导出捕获深度选项
|
|
||||||
preCaptureLengthOptions, // 导出预捕获深度选项
|
// 捕获深度常量和验证
|
||||||
|
CAPTURE_LENGTH_MIN,
|
||||||
|
CAPTURE_LENGTH_MAX,
|
||||||
|
PRE_CAPTURE_LENGTH_MIN,
|
||||||
|
validateCaptureLength,
|
||||||
|
validatePreCaptureLength,
|
||||||
|
setCaptureLength,
|
||||||
|
setPreCaptureLength,
|
||||||
|
|
||||||
// 触发设置方法
|
// 触发设置方法
|
||||||
setChannelDiv, // 导出设置通道组方法
|
setChannelDiv, // 导出设置通道组方法
|
||||||
|
|||||||
@@ -3,89 +3,182 @@
|
|||||||
<!-- 通道配置 -->
|
<!-- 通道配置 -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<!-- 全局触发模式选择和通道组配置 -->
|
<!-- 全局触发模式选择和通道组配置 -->
|
||||||
<div class="flex flex-col lg:flex-row justify-between gap-4 my-4 mx-2">
|
<div class="flex flex-col gap-6 my-4 mx-2">
|
||||||
<!-- 左侧:全局触发模式和通道组选择 -->
|
<div class="flex flex-col lg:flex-row gap-6">
|
||||||
<div class="flex flex-col lg:flex-row gap-4">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<label class="block text-sm font-semibold antialiased">
|
||||||
<label class="label">
|
全局触发逻辑
|
||||||
<span class="label-text text-sm">全局触发逻辑</span>
|
|
||||||
</label>
|
</label>
|
||||||
<select
|
<div class="relative w-[200px]">
|
||||||
v-model="currentGlobalMode"
|
<button
|
||||||
@change="setGlobalMode(currentGlobalMode)"
|
tabindex="0"
|
||||||
class="select select-sm select-bordered"
|
type="button"
|
||||||
>
|
class="flex items-center gap-4 justify-between h-max outline-none focus:outline-none bg-transparent ring-transparent border border-slate-200 transition-all duration-300 ease-in disabled:opacity-50 disabled:pointer-events-none select-none text-start text-sm rounded-md py-2 px-2.5 ring shadow-sm hover:border-slate-800 hover:ring-slate-800/10 focus:border-slate-800 focus:ring-slate-800/10 w-full"
|
||||||
<option
|
@click="toggleGlobalModeDropdown"
|
||||||
v-for="mode in globalModes"
|
:aria-expanded="showGlobalModeDropdown"
|
||||||
:key="mode.value"
|
aria-haspopup="listbox"
|
||||||
:value="mode.value"
|
role="combobox"
|
||||||
>
|
>
|
||||||
{{ mode.label }} - {{ mode.description }}
|
<span>{{ currentGlobalModeLabel }}</span>
|
||||||
</option>
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-[1em] w-[1em] translate-x-0.5 stroke-[1.5]">
|
||||||
</select>
|
<path d="M17 8L12 3L7 8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M17 16L12 21L7 16" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<input readonly style="display:none" :value="currentGlobalMode" />
|
||||||
|
<!-- 下拉菜单 -->
|
||||||
|
<div v-if="showGlobalModeDropdown" class="absolute top-full left-0 right-0 mt-1 bg-white border border-slate-200 rounded-md shadow-lg z-50">
|
||||||
|
<div
|
||||||
|
v-for="mode in globalModes"
|
||||||
|
:key="mode.value"
|
||||||
|
@click="selectGlobalMode(mode.value)"
|
||||||
|
class="px-3 py-2 text-sm text-slate-700 hover:bg-slate-100 cursor-pointer"
|
||||||
|
:class="{ 'bg-slate-100': mode.value === currentGlobalMode }"
|
||||||
|
>
|
||||||
|
{{ mode.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="flex items-center text-xs text-slate-400">
|
||||||
|
{{ currentGlobalModeDescription }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<label class="block text-sm font-semibold antialiased">
|
||||||
<label class="label">
|
通道组
|
||||||
<span class="label-text text-sm">通道组</span>
|
|
||||||
</label>
|
</label>
|
||||||
<select
|
<div class="relative w-[200px]">
|
||||||
v-model="currentChannelDiv"
|
<button
|
||||||
@change="setChannelDiv(currentChannelDiv)"
|
tabindex="0"
|
||||||
class="select select-sm select-bordered"
|
type="button"
|
||||||
>
|
class="flex items-center gap-4 justify-between h-max outline-none focus:outline-none bg-transparent ring-transparent border border-slate-200 transition-all duration-300 ease-in disabled:opacity-50 disabled:pointer-events-none select-none text-start text-sm rounded-md py-2 px-2.5 ring shadow-sm hover:border-slate-800 hover:ring-slate-800/10 focus:border-slate-800 focus:ring-slate-800/10 w-full"
|
||||||
<option
|
@click="toggleChannelDivDropdown"
|
||||||
v-for="option in channelDivOptions"
|
:aria-expanded="showChannelDivDropdown"
|
||||||
:key="option.value"
|
aria-haspopup="listbox"
|
||||||
:value="option.value"
|
role="combobox"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
<span>{{ currentChannelDivLabel }}</span>
|
||||||
</option>
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="currentColor" class="h-[1em] w-[1em] translate-x-0.5 stroke-[1.5]">
|
||||||
</select>
|
<path d="M17 8L12 3L7 8" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M17 16L12 21L7 16" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<input readonly style="display:none" :value="currentChannelDiv" />
|
||||||
|
<!-- 下拉菜单 -->
|
||||||
|
<div v-if="showChannelDivDropdown" class="absolute top-full left-0 right-0 mt-1 bg-white border border-slate-200 rounded-md shadow-lg z-50">
|
||||||
|
<div
|
||||||
|
v-for="option in channelDivOptions"
|
||||||
|
:key="option.value"
|
||||||
|
@click="selectChannelDiv(option.value)"
|
||||||
|
class="px-3 py-2 text-sm text-slate-700 hover:bg-slate-100 cursor-pointer"
|
||||||
|
:class="{ 'bg-slate-100': option.value === currentChannelDiv }"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="flex items-center text-xs text-slate-400">
|
||||||
|
{{ currentChannelDivDescription }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<label class="block text-sm font-semibold antialiased">
|
||||||
<label class="label">
|
捕获深度
|
||||||
<span class="label-text text-sm">捕获深度</span>
|
|
||||||
</label>
|
</label>
|
||||||
<select
|
<div class="relative w-[200px]">
|
||||||
v-model="captureLength"
|
<button
|
||||||
class="select select-sm select-bordered"
|
@click="decreaseCaptureLength"
|
||||||
>
|
class="absolute right-9 top-1 rounded-md border border-transparent p-1.5 text-center text-sm transition-all hover:bg-slate-100 focus:bg-slate-100 active:bg-slate-100 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none"
|
||||||
<option
|
type="button"
|
||||||
v-for="option in captureLengthOptions"
|
:disabled="captureLength <= CAPTURE_LENGTH_MIN"
|
||||||
:key="option.value"
|
|
||||||
:value="option.value"
|
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4">
|
||||||
</option>
|
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||||
</select>
|
</svg>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
v-model.number="captureLength"
|
||||||
|
@change="handleCaptureLengthChange"
|
||||||
|
type="number"
|
||||||
|
:min="CAPTURE_LENGTH_MIN"
|
||||||
|
:max="CAPTURE_LENGTH_MAX"
|
||||||
|
class="w-full bg-transparent placeholder:text-sm border border-slate-200 rounded-md pl-3 pr-20 py-2 transition duration-300 ease focus:outline-none focus:border-slate-400 ring ring-transparent hover:ring-slate-800/10 focus:ring-slate-800/10 hover:border-slate-800 shadow-sm focus:shadow appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
:placeholder="CAPTURE_LENGTH_MIN.toString()"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="increaseCaptureLength"
|
||||||
|
class="absolute right-1 top-1 rounded-md border border-transparent p-1.5 text-center text-sm transition-all hover:bg-slate-100 focus:bg-slate-100 active:bg-slate-100 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none"
|
||||||
|
type="button"
|
||||||
|
:disabled="captureLength >= CAPTURE_LENGTH_MAX"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4">
|
||||||
|
<path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="flex items-center text-xs text-slate-400">
|
||||||
|
范围: {{ CAPTURE_LENGTH_MIN.toLocaleString() }} - {{ CAPTURE_LENGTH_MAX.toLocaleString() }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<label class="block text-sm font-semibold antialiased">
|
||||||
<label class="label">
|
预捕获深度
|
||||||
<span class="label-text text-sm">预捕获深度</span>
|
|
||||||
</label>
|
</label>
|
||||||
<select
|
<div class="relative w-[200px]">
|
||||||
v-model="preCaptureLength"
|
<button
|
||||||
class="select select-sm select-bordered"
|
@click="decreasePreCaptureLength"
|
||||||
>
|
class="absolute right-9 top-1 rounded-md border border-transparent p-1.5 text-center text-sm transition-all hover:bg-slate-100 focus:bg-slate-100 active:bg-slate-100 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none"
|
||||||
<option
|
type="button"
|
||||||
v-for="option in preCaptureLengthOptions"
|
:disabled="preCaptureLength <= PRE_CAPTURE_LENGTH_MIN"
|
||||||
:key="option.value"
|
|
||||||
:value="option.value"
|
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4">
|
||||||
</option>
|
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||||
</select>
|
</svg>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
v-model.number="preCaptureLength"
|
||||||
|
@change="handlePreCaptureLengthChange"
|
||||||
|
type="number"
|
||||||
|
:min="PRE_CAPTURE_LENGTH_MIN"
|
||||||
|
:max="Math.max(0, captureLength - 1)"
|
||||||
|
class="w-full bg-transparent placeholder:text-sm border border-slate-200 rounded-md pl-3 pr-20 py-2 transition duration-300 ease focus:outline-none focus:border-slate-400 ring ring-transparent hover:ring-slate-800/10 focus:ring-slate-800/10 hover:border-slate-800 shadow-sm focus:shadow appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
:placeholder="PRE_CAPTURE_LENGTH_MIN.toString()"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="increasePreCaptureLength"
|
||||||
|
class="absolute right-1 top-1 rounded-md border border-transparent p-1.5 text-center text-sm transition-all hover:bg-slate-100 focus:bg-slate-100 active:bg-slate-100 disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none"
|
||||||
|
type="button"
|
||||||
|
:disabled="preCaptureLength >= Math.max(0, captureLength - 1)"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4">
|
||||||
|
<path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="flex items-center text-xs text-slate-400">
|
||||||
|
范围: {{ PRE_CAPTURE_LENGTH_MIN }} - {{ Math.max(0, captureLength - 1) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="block text-sm font-semibold antialiased">
|
||||||
|
重置配置
|
||||||
|
</label>
|
||||||
|
<div class="relative w-[200px]">
|
||||||
|
<button
|
||||||
|
@click="resetConfiguration"
|
||||||
|
class="w-10 h-10 bg-transparent text-red-600 text-sm border border-red-200 rounded-md py-2 px-2.5 transition duration-300 ease ring ring-transparent hover:ring-red-600/10 focus:ring-red-600/10 hover:border-red-600 shadow-sm focus:shadow flex items-center justify-center"
|
||||||
|
type="button"
|
||||||
|
title="重置配置"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" class="w-4 h-4">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="flex items-center text-xs text-slate-400">
|
||||||
|
恢复所有设置到默认值
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧:操作按钮 -->
|
|
||||||
<div class="flex flex-row gap-2">
|
|
||||||
<button @click="resetConfiguration" class="btn btn-outline btn-sm">
|
|
||||||
重置配置
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 通道列表 -->
|
<!-- 通道列表 -->
|
||||||
@@ -177,6 +270,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onMounted, onUnmounted } from "vue";
|
||||||
import { useRequiredInjection } from "@/utils/Common";
|
import { useRequiredInjection } from "@/utils/Common";
|
||||||
import { useLogicAnalyzerState } from "./LogicAnalyzerManager";
|
import { useLogicAnalyzerState } from "./LogicAnalyzerManager";
|
||||||
|
|
||||||
@@ -193,10 +287,121 @@ const {
|
|||||||
operators,
|
operators,
|
||||||
signalValues,
|
signalValues,
|
||||||
channelDivOptions,
|
channelDivOptions,
|
||||||
captureLengthOptions,
|
CAPTURE_LENGTH_MIN,
|
||||||
preCaptureLengthOptions,
|
CAPTURE_LENGTH_MAX,
|
||||||
|
PRE_CAPTURE_LENGTH_MIN,
|
||||||
|
validateCaptureLength,
|
||||||
|
validatePreCaptureLength,
|
||||||
|
setCaptureLength,
|
||||||
|
setPreCaptureLength,
|
||||||
setChannelDiv,
|
setChannelDiv,
|
||||||
setGlobalMode,
|
setGlobalMode,
|
||||||
resetConfiguration,
|
resetConfiguration,
|
||||||
} = useRequiredInjection(useLogicAnalyzerState);
|
} = useRequiredInjection(useLogicAnalyzerState);
|
||||||
|
|
||||||
|
// 下拉菜单状态
|
||||||
|
const showGlobalModeDropdown = ref(false);
|
||||||
|
const showChannelDivDropdown = ref(false);
|
||||||
|
|
||||||
|
// 处理捕获深度变化
|
||||||
|
const handleCaptureLengthChange = () => {
|
||||||
|
setCaptureLength(captureLength.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理预捕获深度变化
|
||||||
|
const handlePreCaptureLengthChange = () => {
|
||||||
|
setPreCaptureLength(preCaptureLength.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 增加捕获深度
|
||||||
|
const increaseCaptureLength = () => {
|
||||||
|
const newValue = Math.min(captureLength.value + 1024, CAPTURE_LENGTH_MAX);
|
||||||
|
setCaptureLength(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 减少捕获深度
|
||||||
|
const decreaseCaptureLength = () => {
|
||||||
|
const newValue = Math.max(captureLength.value - 1024, CAPTURE_LENGTH_MIN);
|
||||||
|
setCaptureLength(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 增加预捕获深度
|
||||||
|
const increasePreCaptureLength = () => {
|
||||||
|
const maxValue = Math.max(0, captureLength.value - 1);
|
||||||
|
const newValue = Math.min(preCaptureLength.value + 64, maxValue);
|
||||||
|
setPreCaptureLength(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 减少预捕获深度
|
||||||
|
const decreasePreCaptureLength = () => {
|
||||||
|
const newValue = Math.max(preCaptureLength.value - 64, PRE_CAPTURE_LENGTH_MIN);
|
||||||
|
setPreCaptureLength(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算属性:获取当前全局模式的标签
|
||||||
|
const currentGlobalModeLabel = computed(() => {
|
||||||
|
const mode = globalModes.find(m => m.value === currentGlobalMode.value);
|
||||||
|
return mode ? mode.label : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:获取当前全局模式的描述
|
||||||
|
const currentGlobalModeDescription = computed(() => {
|
||||||
|
const mode = globalModes.find(m => m.value === currentGlobalMode.value);
|
||||||
|
return mode ? mode.description : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:获取当前通道组的标签
|
||||||
|
const currentChannelDivLabel = computed(() => {
|
||||||
|
const option = channelDivOptions.find(opt => opt.value === currentChannelDiv.value);
|
||||||
|
return option ? option.label : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:获取当前通道组的描述
|
||||||
|
const currentChannelDivDescription = computed(() => {
|
||||||
|
const option = channelDivOptions.find(opt => opt.value === currentChannelDiv.value);
|
||||||
|
return option ? option.description : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 全局模式下拉菜单相关函数
|
||||||
|
const toggleGlobalModeDropdown = () => {
|
||||||
|
showGlobalModeDropdown.value = !showGlobalModeDropdown.value;
|
||||||
|
if (showGlobalModeDropdown.value) {
|
||||||
|
showChannelDivDropdown.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectGlobalMode = (mode: any) => {
|
||||||
|
setGlobalMode(mode);
|
||||||
|
showGlobalModeDropdown.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通道组下拉菜单相关函数
|
||||||
|
const toggleChannelDivDropdown = () => {
|
||||||
|
showChannelDivDropdown.value = !showChannelDivDropdown.value;
|
||||||
|
if (showChannelDivDropdown.value) {
|
||||||
|
showGlobalModeDropdown.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectChannelDiv = (value: number) => {
|
||||||
|
setChannelDiv(value);
|
||||||
|
showChannelDivDropdown.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击其他地方关闭下拉菜单
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (!target.closest('.relative')) {
|
||||||
|
showGlobalModeDropdown.value = false;
|
||||||
|
showChannelDivDropdown.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -61,13 +61,6 @@
|
|||||||
<Settings class="w-5 h-5" />
|
<Settings class="w-5 h-5" />
|
||||||
触发设置
|
触发设置
|
||||||
</div>
|
</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>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<!-- 状态指示 -->
|
<!-- 状态指示 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user